knowledge-graph-visualization-simple.blade.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. <div>
  2. @push('styles')
  3. <style>
  4. .graph-container {
  5. width: 100%;
  6. height: 700px;
  7. border: 1px solid #e5e7eb;
  8. border-radius: 8px;
  9. background: #f9fafb;
  10. }
  11. </style>
  12. @endpush
  13. <div class="space-y-6">
  14. <!-- 页面标题和操作 -->
  15. <div class="flex justify-between items-center">
  16. <div>
  17. <h2 class="text-2xl font-bold text-gray-900">知识图谱可视化</h2>
  18. <p class="mt-1 text-sm text-gray-500">
  19. 基于学生掌握度的知识节点热力图,点击节点查看详细信息
  20. </p>
  21. </div>
  22. <div class="flex gap-3">
  23. <button
  24. wire:click="$refresh"
  25. type="button"
  26. class="filament-button filament-button-size-sm filament-button-color-gray filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
  27. >
  28. 刷新图谱
  29. </button>
  30. </div>
  31. </div>
  32. <!-- 学生选择器 -->
  33. <div class="bg-white p-6 rounded-lg border shadow-sm">
  34. <div class="flex items-center gap-4">
  35. <div class="flex-1">
  36. <select
  37. wire:model.live="selectedStudentId"
  38. class="form-select w-full px-3 py-2 border rounded-lg"
  39. >
  40. <option value="">-- 显示全部知识点(灰色) --</option>
  41. @foreach($this->getStudents() as $student)
  42. <option value="{{ $student->student_id }}">
  43. {{ $student->name ?? $student->student_id }}
  44. </option>
  45. @endforeach
  46. </select>
  47. </div>
  48. @if($selectedStudentId)
  49. <div class="text-sm text-gray-600">
  50. 当前查看:<span class="font-semibold">{{ $selectedStudentId }}</span> 的掌握度
  51. </div>
  52. @endif
  53. </div>
  54. </div>
  55. <!-- 图例 -->
  56. <div class="bg-white p-6 rounded-lg border shadow-sm">
  57. <h3 class="text-sm font-medium text-gray-900 mb-3">掌握度图例</h3>
  58. <div class="flex flex-wrap gap-3">
  59. <div class="flex items-center gap-2 px-3 py-1 rounded">
  60. <div style="width: 20px; height: 20px; background: #d1d5db; border-radius: 4px; border: 1px solid #d1d5db;"></div>
  61. <span class="text-sm text-gray-700">未学习</span>
  62. </div>
  63. <div class="flex items-center gap-2 px-3 py-1 rounded">
  64. <div style="width: 20px; height: 20px; background: #ef4444; border-radius: 4px; border: 1px solid #dc2626;"></div>
  65. <span class="text-sm text-gray-700">< 60% 需提升</span>
  66. </div>
  67. <div class="flex items-center gap-2 px-3 py-1 rounded">
  68. <div style="width: 20px; height: 20px; background: #fb923c; border-radius: 4px; border: 1px solid #f97316;"></div>
  69. <span class="text-sm text-gray-700">60-69% 及格</span>
  70. </div>
  71. <div class="flex items-center gap-2 px-3 py-1 rounded">
  72. <div style="width: 20px; height: 20px; background: #fbbf24; border-radius: 4px; border: 1px solid #f59e0b;"></div>
  73. <span class="text-sm text-gray-700">70-79% 中等</span>
  74. </div>
  75. <div class="flex items-center gap-2 px-3 py-1 rounded">
  76. <div style="width: 20px; height: 20px; background: #34d399; border-radius: 4px; border: 1px solid #10b981;"></div>
  77. <span class="text-sm text-gray-700">80-89% 良好</span>
  78. </div>
  79. <div class="flex items-center gap-2 px-3 py-1 rounded">
  80. <div style="width: 20px; height: 20px; background: #10b981; border-radius: 4px; border: 1px solid #059669;"></div>
  81. <span class="text-sm text-gray-700">≥ 90% 优秀</span>
  82. </div>
  83. </div>
  84. </div>
  85. <!-- 图谱容器 -->
  86. <div class="bg-white p-6 rounded-lg border shadow-sm">
  87. <div id="mountNode" class="graph-container"></div>
  88. </div>
  89. <!-- 操作提示 -->
  90. <div class="text-sm text-gray-600 bg-blue-50 p-4 rounded-lg">
  91. <div class="font-medium text-blue-900 mb-2">💡 操作提示</div>
  92. <ul class="space-y-1 text-blue-800">
  93. <li>• 鼠标拖拽画布移动视图,滚轮缩放</li>
  94. <li>• 拖拽节点调整位置</li>
  95. <li>• 鼠标悬停节点查看掌握度详情</li>
  96. <li>• 点击节点查看练习建议</li>
  97. </ul>
  98. </div>
  99. </div>
  100. @push('scripts')
  101. <script src="https://gw.alipayobjects.com/os/lib/antv/g6/4.8.21/dist/g6.min.js"></script>
  102. <script>
  103. document.addEventListener('livewire:initialized', () => {
  104. const graphData = @js($graphData);
  105. const studentMasteryData = @js($studentMasteryData);
  106. const selectedStudentId = @js($selectedStudentId);
  107. // 构建掌握度映射
  108. const masteryMap = {};
  109. if (selectedStudentId && studentMasteryData) {
  110. studentMasteryData.forEach(item => {
  111. masteryMap[item.kp_code] = item.mastery;
  112. });
  113. }
  114. // 转换数据格式
  115. const nodes = graphData.nodes.map(node => {
  116. const kpCode = node.id;
  117. const mastery = masteryMap[kpCode];
  118. // 根据掌握度获取颜色
  119. let fillColor = '#d1d5db'; // 默认灰色(未学习)
  120. let strokeColor = '#9ca3af';
  121. if (mastery !== undefined) {
  122. if (mastery >= 0.9) {
  123. fillColor = '#10b981';
  124. strokeColor = '#059669';
  125. } else if (mastery >= 0.8) {
  126. fillColor = '#34d399';
  127. strokeColor = '#10b981';
  128. } else if (mastery >= 0.7) {
  129. fillColor = '#fbbf24';
  130. strokeColor = '#f59e0b';
  131. } else if (mastery >= 0.6) {
  132. fillColor = '#fb923c';
  133. strokeColor = '#f97316';
  134. } else {
  135. fillColor = '#ef4444';
  136. strokeColor = '#dc2626';
  137. }
  138. }
  139. return {
  140. ...node,
  141. style: {
  142. fill: fillColor,
  143. stroke: strokeColor,
  144. lineWidth: 2,
  145. radius: 6,
  146. },
  147. mastery: mastery,
  148. };
  149. });
  150. const edges = graphData.edges.map(edge => ({
  151. ...edge,
  152. style: {
  153. stroke: '#94a3b8',
  154. lineWidth: 1.5,
  155. opacity: 0.6,
  156. endArrow: true,
  157. },
  158. }));
  159. const container = document.getElementById('mountNode');
  160. const width = container.scrollWidth;
  161. const height = container.scrollHeight;
  162. // 创建图表
  163. const graph = new G6.Graph({
  164. container: 'mountNode',
  165. width,
  166. height,
  167. fitView: true,
  168. fitViewPadding: [20, 20, 20, 20],
  169. modes: {
  170. default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
  171. },
  172. layout: {
  173. type: 'dagre',
  174. rankdir: 'LR',
  175. align: 'UL',
  176. controlPoints: true,
  177. nodesep: 20,
  178. ranksep: 60,
  179. },
  180. defaultNode: {
  181. type: 'rect',
  182. size: [160, 50],
  183. labelCfg: {
  184. position: 'center',
  185. style: {
  186. fontSize: 12,
  187. },
  188. },
  189. },
  190. defaultEdge: {
  191. type: 'polyline',
  192. style: {
  193. radius: 10,
  194. offset: 30,
  195. },
  196. },
  197. });
  198. // 渲染图表
  199. graph.data({
  200. nodes: nodes,
  201. edges: edges,
  202. });
  203. graph.render();
  204. // 绑定事件
  205. graph.on('node:click', (evt) => {
  206. const node = evt.item;
  207. const model = node.getModel();
  208. const kpCode = model.id;
  209. const kpName = model.label || kpCode;
  210. const mastery = model.mastery;
  211. // 显示详细信息
  212. let message = `知识点:${kpName}\n`;
  213. message += `代码:${kpCode}\n`;
  214. if (mastery !== undefined) {
  215. const percentage = (mastery * 100).toFixed(1);
  216. message += `掌握度:${percentage}%\n`;
  217. if (mastery < 0.7) {
  218. message += `\n建议:需要加强练习,建议进行针对性训练`;
  219. } else if (mastery >= 0.9) {
  220. message += `\n表现优秀!可以挑战更高难度题目`;
  221. }
  222. } else {
  223. message += `\n状态:未学习\n建议:开始学习该知识点`;
  224. }
  225. alert(message);
  226. });
  227. // 窗口大小变化时重新适应
  228. window.addEventListener('resize', () => {
  229. if (!graph || graph.get('destroyed')) return;
  230. const width = container.scrollWidth;
  231. const height = container.scrollHeight;
  232. graph.changeSize(width, height);
  233. });
  234. });
  235. </script>
  236. @endpush
  237. </div>