knowledge-graph-visualization.blade.php 12 KB

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