StudentKnowledgeGraph.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. <?php
  2. namespace App\Livewire;
  3. use Livewire\Component;
  4. use App\Models\Student;
  5. use Illuminate\Support\Facades\Http;
  6. use Illuminate\Support\Facades\DB;
  7. class StudentKnowledgeGraph extends Component
  8. {
  9. public $selectedStudentId = null;
  10. public $selectedStudent = null;
  11. public $knowledgePoints = [];
  12. public $dependencies = [];
  13. public $masteryData = [];
  14. public $statistics = [];
  15. public $learningPath = [];
  16. public $isLoading = false;
  17. public $students = [];
  18. public bool $showNodeDetails = true;
  19. public string $detailLayout = 'inline';
  20. protected $rules = [
  21. 'selectedStudentId' => 'required|exists:students,student_id',
  22. ];
  23. public function mount(bool $showNodeDetails = true, string $detailLayout = 'inline')
  24. {
  25. $this->showNodeDetails = $showNodeDetails;
  26. $this->detailLayout = in_array($detailLayout, ['inline', 'drawer'], true)
  27. ? $detailLayout
  28. : 'inline';
  29. $this->loadStudents();
  30. }
  31. public function updatedSelectedStudentId($value)
  32. {
  33. if ($value) {
  34. $this->loadStudentData($value);
  35. } else {
  36. $this->resetData();
  37. }
  38. }
  39. public function loadStudents()
  40. {
  41. $this->students = DB::table('students')
  42. ->select('student_id', 'name', 'grade', 'class_name')
  43. ->orderBy('grade')
  44. ->orderBy('class_name')
  45. ->orderBy('name')
  46. ->get()
  47. ->map(function ($student) {
  48. return [
  49. 'id' => $student->student_id,
  50. 'label' => "{$student->name} ({$student->grade}-{$student->class_name})",
  51. ];
  52. })
  53. ->toArray();
  54. }
  55. public function loadStudentData($studentId)
  56. {
  57. $this->isLoading = true;
  58. try {
  59. // 获取学生信息
  60. $this->selectedStudent = DB::table('students')
  61. ->where('student_id', $studentId)
  62. ->first();
  63. // 调用LearningAnalytics API获取知识图谱数据
  64. $this->fetchKnowledgeGraphData($studentId);
  65. } catch (\Exception $e) {
  66. session()->flash('error', '加载数据失败:' . $e->getMessage());
  67. \Log::error('加载学生知识图谱失败', [
  68. 'student_id' => $studentId,
  69. 'error' => $e->getMessage(),
  70. ]);
  71. }
  72. $this->isLoading = false;
  73. }
  74. private function fetchKnowledgeGraphData($studentId)
  75. {
  76. $baseUrl = config('services.learning_analytics.url', 'http://localhost:5010');
  77. $masteryPayload = [];
  78. try {
  79. // 获取掌握度数据
  80. $masteryResponse = Http::timeout(10)->get($baseUrl . '/api/mastery/' . $studentId);
  81. if ($masteryResponse->successful()) {
  82. $masteryPayload = $masteryResponse->json();
  83. }
  84. // 获取依赖关系
  85. $dependencyResponse = Http::timeout(10)->get($baseUrl . '/api/knowledge/dependencies');
  86. if ($dependencyResponse->successful()) {
  87. $this->dependencies = $dependencyResponse->json();
  88. }
  89. // 获取统计信息
  90. $statsResponse = Http::timeout(10)->get($baseUrl . '/api/mastery/' . $studentId . '/statistics');
  91. if ($statsResponse->successful()) {
  92. $this->statistics = $statsResponse->json();
  93. }
  94. // 获取学习路径
  95. $pathResponse = Http::timeout(10)->get($baseUrl . '/api/learning-path/' . $studentId);
  96. if ($pathResponse->successful()) {
  97. $this->learningPath = $pathResponse->json();
  98. }
  99. $this->setMasteryData($masteryPayload);
  100. // 构建知识点图谱数据
  101. $this->buildKnowledgeGraphData();
  102. } catch (\Exception $e) {
  103. \Log::warning('LearningAnalytics API调用失败,使用本地数据', [
  104. 'error' => $e->getMessage(),
  105. ]);
  106. // 如果API调用失败,使用本地模拟数据
  107. $this->loadMockData($studentId);
  108. }
  109. }
  110. private function buildKnowledgeGraphData()
  111. {
  112. $nodes = [];
  113. $links = [];
  114. $masteries = $this->masteryData['masteries'] ?? [];
  115. // 处理掌握度数据,构建节点
  116. foreach ($masteries as $mastery) {
  117. if (!isset($mastery['mastery_level'])) {
  118. continue;
  119. }
  120. $masteryLevel = (float) $mastery['mastery_level'];
  121. $kpCode = $mastery['kp_code'] ?? null;
  122. if (!$kpCode) {
  123. continue;
  124. }
  125. $nodes[] = [
  126. 'id' => $kpCode,
  127. 'label' => $mastery['kp_name'] ?? $kpCode,
  128. 'mastery' => $masteryLevel,
  129. 'color' => $this->getMasteryColor($masteryLevel),
  130. 'size' => $this->getMasterySize($masteryLevel),
  131. ];
  132. }
  133. // 处理依赖关系,构建边
  134. if (isset($this->dependencies['dependencies'])) {
  135. foreach ($this->dependencies['dependencies'] as $dep) {
  136. $links[] = [
  137. 'source' => $dep['prerequisite_kp'],
  138. 'target' => $dep['dependent_kp'],
  139. 'strength' => $dep['influence_weight'],
  140. 'type' => $dep['dependency_type'],
  141. ];
  142. }
  143. }
  144. $this->knowledgePoints = [
  145. 'nodes' => $nodes,
  146. 'links' => $links,
  147. ];
  148. $this->dispatchGraphUpdated();
  149. }
  150. private function loadMockData($studentId)
  151. {
  152. // 模拟数据,用于演示
  153. $mockKnowledgePoints = [
  154. 'R01' => ['name' => '有理数', 'mastery' => 0.85],
  155. 'R02' => ['name' => '整式运算', 'mastery' => 0.72],
  156. 'R03' => ['name' => '一元一次方程', 'mastery' => 0.65],
  157. 'R04' => ['name' => '因式分解', 'mastery' => 0.45],
  158. 'R05' => ['name' => '二次方程', 'mastery' => 0.30],
  159. 'R06' => ['name' => '二次函数', 'mastery' => 0.25],
  160. 'R07' => ['name' => '几何图形', 'mastery' => 0.78],
  161. 'R08' => ['name' => '三角形', 'mastery' => 0.68],
  162. ];
  163. $nodes = [];
  164. foreach ($mockKnowledgePoints as $code => $data) {
  165. $nodes[] = [
  166. 'id' => $code,
  167. 'label' => $data['name'],
  168. 'mastery' => $data['mastery'],
  169. 'color' => $this->getMasteryColor($data['mastery']),
  170. 'size' => $this->getMasterySize($data['mastery']),
  171. ];
  172. }
  173. $links = [
  174. ['source' => 'R01', 'target' => 'R02', 'strength' => 0.9, 'type' => 'must'],
  175. ['source' => 'R02', 'target' => 'R03', 'strength' => 0.8, 'type' => 'must'],
  176. ['source' => 'R02', 'target' => 'R04', 'strength' => 0.7, 'type' => 'should'],
  177. ['source' => 'R03', 'target' => 'R05', 'strength' => 0.9, 'type' => 'must'],
  178. ['source' => 'R04', 'target' => 'R05', 'strength' => 0.8, 'type' => 'should'],
  179. ['source' => 'R05', 'target' => 'R06', 'strength' => 0.9, 'type' => 'must'],
  180. ['source' => 'R07', 'target' => 'R08', 'strength' => 0.8, 'type' => 'should'],
  181. ];
  182. $this->knowledgePoints = [
  183. 'nodes' => $nodes,
  184. 'links' => $links,
  185. ];
  186. $this->setMasteryData([
  187. 'masteries' => array_map(function ($code, $data) use ($studentId) {
  188. return [
  189. 'student_id' => $studentId,
  190. 'kp_code' => $code,
  191. 'mastery_level' => $data['mastery'],
  192. 'confidence_level' => 0.8,
  193. ];
  194. }, array_keys($mockKnowledgePoints), $mockKnowledgePoints),
  195. ]);
  196. $this->statistics = [
  197. 'total_knowledge_points' => count($mockKnowledgePoints),
  198. 'average_mastery' => array_sum(array_column($mockKnowledgePoints, 'mastery')) / count($mockKnowledgePoints),
  199. 'high_mastery_count' => count(array_filter($mockKnowledgePoints, fn($d) => $d['mastery'] >= 0.7)),
  200. 'medium_mastery_count' => count(array_filter($mockKnowledgePoints, fn($d) => $d['mastery'] >= 0.4 && $d['mastery'] < 0.7)),
  201. 'low_mastery_count' => count(array_filter($mockKnowledgePoints, fn($d) => $d['mastery'] < 0.4)),
  202. ];
  203. $this->dispatchGraphUpdated();
  204. }
  205. private function getMasteryColor($mastery)
  206. {
  207. if ($mastery >= 0.8) return '#10b981'; // 绿色 - 优秀
  208. if ($mastery >= 0.6) return '#3b82f6'; // 蓝色 - 良好
  209. if ($mastery >= 0.4) return '#f59e0b'; // 黄色 - 中等
  210. if ($mastery >= 0.2) return '#f97316'; // 橙色 - 待提高
  211. return '#ef4444'; // 红色 - 薄弱
  212. }
  213. private function getMasterySize($mastery)
  214. {
  215. return max(10, $mastery * 40); // 最小10px,最大40px
  216. }
  217. private function resetData()
  218. {
  219. $this->selectedStudent = null;
  220. $this->knowledgePoints = [];
  221. $this->dependencies = [];
  222. $this->masteryData = [];
  223. $this->statistics = [];
  224. $this->learningPath = [];
  225. $this->dispatchGraphUpdated();
  226. }
  227. private function dispatchGraphUpdated(): void
  228. {
  229. // 通知前端重新渲染图谱(更新节点颜色/大小等)
  230. $this->dispatch('knowledgeGraphUpdated', $this->knowledgePoints);
  231. }
  232. private function setMasteryData(array $payload): void
  233. {
  234. $masteries = $this->normalizeMasteries($payload);
  235. $this->masteryData = array_merge($payload, [
  236. 'masteries' => $masteries,
  237. 'mastery_map' => $this->buildMasteryMap($masteries),
  238. ]);
  239. }
  240. private function normalizeMasteries(array $raw): array
  241. {
  242. if (isset($raw['masteries']) && is_array($raw['masteries'])) {
  243. return array_values($raw['masteries']);
  244. }
  245. $candidates = [];
  246. if (isset($raw['data']) && is_array($raw['data'])) {
  247. $candidates[] = $raw['data'];
  248. }
  249. if (isset($raw['Target']) && is_array($raw['Target'])) {
  250. $candidates[] = $raw['Target'];
  251. }
  252. if ($this->isAssociativeArray($raw) && $this->looksLikeMasteryRecord(reset($raw))) {
  253. $candidates[] = $raw;
  254. }
  255. foreach ($candidates as $candidate) {
  256. $normalized = $this->normalizeCandidateMasteries($candidate);
  257. if (!empty($normalized)) {
  258. return $normalized;
  259. }
  260. }
  261. return [];
  262. }
  263. private function normalizeCandidateMasteries(array $candidate): array
  264. {
  265. $normalized = [];
  266. foreach ($candidate as $key => $entry) {
  267. if (!is_array($entry) || !isset($entry['mastery_level'])) {
  268. continue;
  269. }
  270. if (!isset($entry['kp_code']) && is_string($key)) {
  271. $entry['kp_code'] = $key;
  272. }
  273. if (isset($entry['kp_code'])) {
  274. $normalized[] = $entry;
  275. }
  276. }
  277. return $normalized;
  278. }
  279. private function isAssociativeArray(array $array): bool
  280. {
  281. return array_keys($array) !== range(0, count($array) - 1);
  282. }
  283. private function looksLikeMasteryRecord($entry): bool
  284. {
  285. return is_array($entry) && array_key_exists('mastery_level', $entry);
  286. }
  287. private function buildMasteryMap(array $masteries): array
  288. {
  289. $map = [];
  290. foreach ($masteries as $mastery) {
  291. if (!isset($mastery['kp_code'])) {
  292. continue;
  293. }
  294. $map[$mastery['kp_code']] = $mastery;
  295. }
  296. return $map;
  297. }
  298. public function render()
  299. {
  300. return view('livewire.student-knowledge-graph');
  301. }
  302. }