knowledge-mindmap.blade.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <x-filament::page>
  2. <div
  3. class="space-y-6 bg-white p-4 rounded-xl"
  4. x-data="{
  5. graphInstance: null,
  6. stats: { nodes: 0, extraEdges: 0 },
  7. livewireId: '{{ $this->getId() }}',
  8. selectedNode: null,
  9. initEventListener() {
  10. window.addEventListener('mindmap-node-selected', (evt) => {
  11. const model = evt.detail || {};
  12. const code = model?.meta?.code || model?.id;
  13. const mastery = model?.meta?.mastery_level ?? this.graphInstance?.masteryData?.[code]?.mastery_level ?? 0;
  14. const accuracy = model?.meta?.accuracy_rate ?? this.graphInstance?.masteryData?.[code]?.accuracy_rate ?? null;
  15. const attempts = model?.meta?.total_attempts ?? this.graphInstance?.masteryData?.[code]?.total_attempts ?? null;
  16. this.selectedNode = {
  17. id: model?.id,
  18. code,
  19. label: model?.label,
  20. mastery,
  21. accuracy,
  22. attempts,
  23. recommended: model?.meta?.recommended ?? false,
  24. skills: model?.meta?.skills || this.graphInstance?.masteryData?.[code]?.skills || [],
  25. };
  26. });
  27. },
  28. async initMindmap() {
  29. try {
  30. // 等待G6和自定义组件加载
  31. await this.waitForComponents();
  32. if (!window.G6 || !window.KnowledgeMindmapGraph) {
  33. console.error('G6组件未加载');
  34. return;
  35. }
  36. this.graphInstance = new window.KnowledgeMindmapGraph({
  37. containerId: 'knowledge-mindmap',
  38. livewireMethod: 'openDrawer',
  39. highlightLowMastery: true,
  40. livewireId: this.livewireId,
  41. onNodeSelect: (model) => {
  42. const code = model?.meta?.code || model?.id;
  43. const mastery = model?.meta?.mastery_level ?? this.graphInstance?.masteryData?.[code]?.mastery_level ?? 0;
  44. const accuracy = model?.meta?.accuracy_rate ?? this.graphInstance?.masteryData?.[code]?.accuracy_rate ?? null;
  45. const attempts = model?.meta?.total_attempts ?? this.graphInstance?.masteryData?.[code]?.total_attempts ?? null;
  46. this.selectedNode = {
  47. id: model?.id,
  48. code,
  49. label: model?.label,
  50. mastery,
  51. accuracy,
  52. attempts,
  53. recommended: model?.meta?.recommended ?? false,
  54. skills: model?.meta?.skills || this.graphInstance?.masteryData?.[code]?.skills || [],
  55. };
  56. },
  57. });
  58. await this.graphInstance.init();
  59. this.stats = this.graphInstance.stats;
  60. } catch (error) {
  61. console.error('知识图谱初始化失败:', error);
  62. }
  63. },
  64. async waitForComponents() {
  65. let attempts = 0;
  66. const maxAttempts = 50;
  67. while ((!window.G6 || !window.KnowledgeMindmapGraph) && attempts < maxAttempts) {
  68. await new Promise(resolve => setTimeout(resolve, 100));
  69. attempts++;
  70. }
  71. if (attempts >= maxAttempts) {
  72. throw new Error('G6组件加载超时');
  73. }
  74. }
  75. }"
  76. x-init="initEventListener(); initMindmap()"
  77. data-knowledge-mindmap-root
  78. >
  79. <div class="rounded-xl border border-slate-200 bg-white shadow-sm p-5">
  80. <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
  81. <div class="space-y-2 text-slate-900">
  82. <h2 class="text-3xl font-bold">初中数学知识图谱</h2>
  83. <p class="text-lg text-slate-600">掌握度、互动与视觉反馈同步升级,点击任意节点查看学习路径与练习推荐。</p>
  84. <div class="flex flex-wrap items-center gap-4 text-base text-slate-600">
  85. <span>知识点 <span class="font-semibold text-slate-900" x-text="stats.nodes"></span></span>
  86. <span>额外关联 <span class="font-semibold text-slate-900" x-text="stats.extraEdges"></span></span>
  87. <span>已选学生:<span class="font-semibold text-slate-900">{{ $selectedStudentName ?: '未选择' }}</span></span>
  88. </div>
  89. </div>
  90. <div class="grid w-full max-w-xl grid-cols-1 gap-3 rounded-lg border border-slate-200 bg-slate-50 p-4 sm:grid-cols-2 text-slate-900">
  91. <div>
  92. <label class="mb-1 block text-base font-medium text-slate-700">选择老师</label>
  93. <select
  94. wire:model.live="selectedTeacherId"
  95. class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2.5 text-base text-slate-900 placeholder:text-slate-500 focus:border-sky-300 focus:ring-2 focus:ring-sky-200/50"
  96. >
  97. <option value="" class="text-slate-800">请选择老师...</option>
  98. @foreach($teachers as $teacher)
  99. <option value="{{ $teacher['teacher_id'] }}" class="text-slate-900">{{ $teacher['name'] }}</option>
  100. @endforeach
  101. </select>
  102. </div>
  103. <div>
  104. <label class="mb-1 block text-base font-medium text-slate-700">选择学生</label>
  105. <select
  106. wire:model.live="selectedStudentId"
  107. class="w-full rounded-lg border border-slate-200 bg-white px-3 py-2.5 text-base text-slate-900 placeholder:text-slate-500 focus:border-amber-300 focus:ring-2 focus:ring-amber-200/50"
  108. {{ empty($selectedTeacherId) ? 'disabled' : '' }}
  109. >
  110. <option value="" class="text-slate-800">
  111. {{ empty($selectedTeacherId) ? '请先选择老师...' : '请选择学生...' }}
  112. </option>
  113. @foreach($students as $student)
  114. <option value="{{ $student['student_id'] }}" class="text-slate-900">{{ $student['name'] }}</option>
  115. @endforeach
  116. </select>
  117. </div>
  118. </div>
  119. </div>
  120. </div>
  121. <div class="relative overflow-hidden rounded-2xl border border-slate-200 shadow-sm bg-white text-slate-900 knowledge-mindmap-card">
  122. <div
  123. wire:ignore
  124. id="knowledge-mindmap"
  125. class="knowledge-mindmap-canvas relative h-[82vh] min-h-[720px] w-full"
  126. >
  127. <div class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_20%_20%,rgba(255,255,255,0.08),transparent_40%),radial-gradient(circle_at_80%_60%,rgba(255,255,255,0.06),transparent_45%)]"></div>
  128. </div>
  129. </div>
  130. <template x-if="selectedNode">
  131. <div class="mt-4 grid grid-cols-1 gap-4 lg:grid-cols-3">
  132. <div class="lg:col-span-2 rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
  133. <div class="flex items-start justify-between">
  134. <div>
  135. <p class="text-sm text-slate-500">选中知识点</p>
  136. <h3 class="text-xl font-semibold text-slate-900" x-text="selectedNode.label || selectedNode.code"></h3>
  137. <p class="text-sm text-slate-500 mt-1" x-text="`ID: ${selectedNode.id || ''} · Code: ${selectedNode.code || ''}`"></p>
  138. </div>
  139. <div class="text-right">
  140. <p class="text-sm text-slate-500">掌握度</p>
  141. <p class="text-2xl font-bold" x-text="(selectedNode.mastery * 100).toFixed(1) + '%'"></p>
  142. <p class="text-xs text-slate-500" x-show="selectedNode.accuracy">准确率 <span x-text="(selectedNode.accuracy * 100).toFixed(1) + '%'"></span></p>
  143. <p class="text-xs text-slate-500" x-show="selectedNode.attempts">练习次数 <span x-text="selectedNode.attempts"></span></p>
  144. </div>
  145. </div>
  146. <div class="mt-4">
  147. <p class="text-sm font-semibold text-slate-700 mb-2">技能要点</p>
  148. <div class="flex flex-wrap gap-2">
  149. <template x-if="selectedNode.skills && selectedNode.skills.length">
  150. <template x-for="skill in selectedNode.skills" :key="skill">
  151. <span class="rounded-full bg-slate-100 px-3 py-1 text-xs font-medium text-slate-700" x-text="skill"></span>
  152. </template>
  153. </template>
  154. <span x-show="!selectedNode.skills || !selectedNode.skills.length" class="text-sm text-slate-500">暂无技能要点</span>
  155. </div>
  156. <div class="mt-4">
  157. <a
  158. class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-700"
  159. :href="selectedNode.code ? `/admin/knowledge-point-detail?kp_code=${encodeURIComponent(selectedNode.code)}` : '#'"
  160. target="_blank"
  161. >
  162. 查看知识点详情
  163. </a>
  164. </div>
  165. </div>
  166. </div>
  167. <div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm space-y-2">
  168. <p class="text-sm font-semibold text-slate-700">快速信息</p>
  169. <div class="text-sm text-slate-600 space-y-1">
  170. <p><span class="font-medium text-slate-900">ID:</span><span x-text="selectedNode.id"></span></p>
  171. <p><span class="font-medium text-slate-900">Code:</span><span x-text="selectedNode.code"></span></p>
  172. <p><span class="font-medium text-slate-900">推荐关注:</span><span x-text="selectedNode.recommended ? '是' : '否'"></span></p>
  173. </div>
  174. </div>
  175. </div>
  176. </template>
  177. <x-mindmap.detail-drawer
  178. :open="$drawerOpen"
  179. :details="$nodeDetails"
  180. panelTitle="知识点详情"
  181. />
  182. </div>
  183. @push('styles')
  184. <style>
  185. .knowledge-mindmap-canvas {
  186. background: #ffffff;
  187. color: #0f172a;
  188. }
  189. .g6-grid {
  190. opacity: 0.08;
  191. }
  192. </style>
  193. @endpush
  194. @push('scripts')
  195. <script src="https://gw.alipayobjects.com/os/lib/antv/g6/4.8.24/dist/g6.min.js"></script>
  196. <script src="{{ asset('js/g6-custom-node.js') }}"></script>
  197. <script src="{{ asset('js/knowledge-mindmap-graph.js') }}"></script>
  198. @endpush
  199. </x-filament::page>