student-knowledge-graph.blade.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. <div>
  2. <div class="space-y-6">
  3. <!-- 标题和控制 -->
  4. <div class="bg-white shadow rounded-lg p-6">
  5. <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
  6. <div>
  7. <h2 class="text-2xl font-bold text-gray-900">学生知识图谱</h2>
  8. <p class="text-sm text-gray-600 mt-1">可视化展示学生的知识点掌握情况和依赖关系</p>
  9. </div>
  10. <div class="flex items-center gap-3">
  11. <select
  12. wire:model.live="selectedStudentId"
  13. class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
  14. >
  15. <option value="">-- 选择学生 --</option>
  16. @foreach ($students as $student)
  17. <option value="{{ $student['id'] }}">{{ $student['label'] }}</option>
  18. @endforeach
  19. </select>
  20. @if ($selectedStudent)
  21. <button
  22. wire:click="loadStudentData('{{ $selectedStudentId }}')"
  23. class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
  24. >
  25. <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  26. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
  27. </svg>
  28. 刷新
  29. </button>
  30. <button
  31. onclick="exportGraph()"
  32. class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
  33. >
  34. <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  35. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
  36. </svg>
  37. 导出PNG
  38. </button>
  39. @endif
  40. </div>
  41. </div>
  42. @error('selectedStudentId')
  43. <p class="mt-2 text-sm text-red-600">{{ $message }}</p>
  44. @enderror
  45. </div>
  46. @if ($selectedStudent)
  47. <!-- 学生信息 -->
  48. <div class="bg-white shadow rounded-lg p-6">
  49. <div class="flex items-center gap-4">
  50. <div class="flex-shrink-0">
  51. <div class="w-16 h-16 rounded-full bg-indigo-100 flex items-center justify-center">
  52. <span class="text-2xl font-bold text-indigo-600">{{ substr($selectedStudent->name, 0, 1) }}</span>
  53. </div>
  54. </div>
  55. <div>
  56. <h3 class="text-lg font-semibold text-gray-900">{{ $selectedStudent->name }}</h3>
  57. <p class="text-sm text-gray-600">{{ $selectedStudent->grade }} {{ $selectedStudent->class_name }}</p>
  58. </div>
  59. </div>
  60. </div>
  61. @if ($isLoading)
  62. <!-- 加载状态 -->
  63. <div class="bg-white shadow rounded-lg p-12">
  64. <div class="flex flex-col items-center justify-center">
  65. <svg class="animate-spin h-12 w-12 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
  66. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
  67. <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
  68. </svg>
  69. <p class="mt-4 text-sm text-gray-600">正在加载知识图谱数据...</p>
  70. </div>
  71. </div>
  72. @else
  73. <!-- 统计信息 -->
  74. @if (!empty($statistics))
  75. <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
  76. <div class="bg-white shadow rounded-lg p-6">
  77. <div class="flex items-center">
  78. <div class="flex-shrink-0">
  79. <div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
  80. <svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  81. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
  82. </svg>
  83. </div>
  84. </div>
  85. <div class="ml-4">
  86. <p class="text-sm font-medium text-gray-500">平均掌握度</p>
  87. <p class="text-2xl font-semibold text-gray-900">{{ number_format($statistics['average_mastery'] * 100, 1) }}%</p>
  88. </div>
  89. </div>
  90. </div>
  91. <div class="bg-white shadow rounded-lg p-6">
  92. <div class="flex items-center">
  93. <div class="flex-shrink-0">
  94. <div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
  95. <svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  96. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
  97. </svg>
  98. </div>
  99. </div>
  100. <div class="ml-4">
  101. <p class="text-sm font-medium text-gray-500">优秀 (≥80%)</p>
  102. <p class="text-2xl font-semibold text-gray-900">{{ $statistics['high_mastery_count'] }}</p>
  103. </div>
  104. </div>
  105. </div>
  106. <div class="bg-white shadow rounded-lg p-6">
  107. <div class="flex items-center">
  108. <div class="flex-shrink-0">
  109. <div class="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
  110. <svg class="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  111. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
  112. </svg>
  113. </div>
  114. </div>
  115. <div class="ml-4">
  116. <p class="text-sm font-medium text-gray-500">中等 (40-80%)</p>
  117. <p class="text-2xl font-semibold text-gray-900">{{ $statistics['medium_mastery_count'] }}</p>
  118. </div>
  119. </div>
  120. </div>
  121. <div class="bg-white shadow rounded-lg p-6">
  122. <div class="flex items-center">
  123. <div class="flex-shrink-0">
  124. <div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
  125. <svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  126. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
  127. </svg>
  128. </div>
  129. </div>
  130. <div class="ml-4">
  131. <p class="text-sm font-medium text-gray-500">待提高 (<40%)</p>
  132. <p class="text-2xl font-semibold text-gray-900">{{ $statistics['low_mastery_count'] }}</p>
  133. </div>
  134. </div>
  135. </div>
  136. </div>
  137. @endif
  138. <!-- 知识图谱 -->
  139. <div class="bg-white shadow rounded-lg p-6">
  140. <div class="flex items-center justify-between mb-4">
  141. <h3 class="text-lg font-semibold text-gray-900">知识点依赖关系图</h3>
  142. <div class="flex items-center gap-4 text-xs text-gray-500">
  143. <div class="flex items-center gap-1">
  144. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  145. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"></path>
  146. </svg>
  147. <span>拖拽移动</span>
  148. </div>
  149. <div class="flex items-center gap-1">
  150. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  151. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
  152. </svg>
  153. <span>滚轮缩放</span>
  154. </div>
  155. <div class="flex items-center gap-1">
  156. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  157. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
  158. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
  159. </svg>
  160. <span>悬浮查看</span>
  161. </div>
  162. </div>
  163. </div>
  164. <div class="relative">
  165. <div id="knowledge-graph" class="w-full h-96 border border-gray-200 rounded-lg"></div>
  166. <!-- 图例 -->
  167. <div class="absolute top-4 right-4 bg-white p-4 rounded-lg shadow-lg border border-gray-200">
  168. <p class="text-xs font-semibold text-gray-700 mb-3">掌握度</p>
  169. <div class="space-y-2">
  170. <div class="flex items-center gap-2">
  171. <div class="w-4 h-4 rounded-full bg-green-500 border-2 border-white shadow-sm"></div>
  172. <span class="text-xs text-gray-700 font-medium">优秀 (≥80%)</span>
  173. </div>
  174. <div class="flex items-center gap-2">
  175. <div class="w-4 h-4 rounded-full bg-blue-500 border-2 border-white shadow-sm"></div>
  176. <span class="text-xs text-gray-700 font-medium">良好 (60-80%)</span>
  177. </div>
  178. <div class="flex items-center gap-2">
  179. <div class="w-4 h-4 rounded-full bg-yellow-500 border-2 border-white shadow-sm"></div>
  180. <span class="text-xs text-gray-700 font-medium">中等 (40-60%)</span>
  181. </div>
  182. <div class="flex items-center gap-2">
  183. <div class="w-4 h-4 rounded-full bg-orange-500 border-2 border-white shadow-sm"></div>
  184. <span class="text-xs text-gray-700 font-medium">待提高 (20-40%)</span>
  185. </div>
  186. <div class="flex items-center gap-2">
  187. <div class="w-4 h-4 rounded-full bg-red-500 border-2 border-white shadow-sm"></div>
  188. <span class="text-xs text-gray-700 font-medium">薄弱 (<20%)</span>
  189. </div>
  190. </div>
  191. <div class="mt-3 pt-3 border-t border-gray-200">
  192. <p class="text-xs text-gray-500">节点大小表示掌握程度</p>
  193. </div>
  194. </div>
  195. </div>
  196. </div>
  197. <!-- 掌握度分布图 -->
  198. @if (!empty($masteryData['masteries']))
  199. <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
  200. <div class="bg-white shadow rounded-lg p-6">
  201. <h3 class="text-lg font-semibold text-gray-900 mb-4">掌握度分布</h3>
  202. <canvas id="mastery-distribution" class="w-full h-64"></canvas>
  203. </div>
  204. <div class="bg-white shadow rounded-lg p-6">
  205. <h3 class="text-lg font-semibold text-gray-900 mb-4">知识点列表</h3>
  206. <div class="space-y-3 max-h-64 overflow-y-auto">
  207. @foreach ($masteryData['masteries'] as $mastery)
  208. <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
  209. <div>
  210. <p class="text-sm font-medium text-gray-900">{{ $mastery['kp_code'] }}</p>
  211. <p class="text-xs text-gray-500">置信度: {{ number_format($mastery['confidence_level'] * 100, 1) }}%</p>
  212. </div>
  213. <div class="text-right">
  214. <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
  215. style="background-color: {{ $this->getMasteryColor($mastery['mastery_level']) }}20; color: {{ $this->getMasteryColor($mastery['mastery_level']) }}">
  216. {{ number_format($mastery['mastery_level'] * 100, 1) }}%
  217. </span>
  218. </div>
  219. </div>
  220. @endforeach
  221. </div>
  222. </div>
  223. </div>
  224. @endif
  225. @if($showNodeDetails && $detailLayout === 'inline')
  226. <div id="kg-node-detail" class="mt-6 bg-white shadow rounded-lg p-6 hidden">
  227. <div class="flex items-center justify-between">
  228. <div>
  229. <p class="text-sm text-gray-500">选中知识点</p>
  230. <h4 class="text-xl font-bold text-gray-900" id="kg-detail-title">-</h4>
  231. <p class="text-sm text-gray-500" id="kg-detail-code">-</p>
  232. </div>
  233. <div class="text-right">
  234. <p class="text-sm text-gray-500">掌握度</p>
  235. <p class="text-2xl font-bold text-gray-900" id="kg-detail-mastery">-</p>
  236. <p class="text-xs text-gray-500" id="kg-detail-accuracy">-</p>
  237. <p class="text-xs text-gray-500" id="kg-detail-attempts">-</p>
  238. </div>
  239. </div>
  240. <div class="mt-4">
  241. <p class="text-sm font-semibold text-gray-700 mb-2">技能要点</p>
  242. <div id="kg-detail-skills" class="flex flex-wrap gap-2 text-sm text-gray-700">
  243. <span class="text-gray-500">暂无技能要点</span>
  244. </div>
  245. </div>
  246. </div>
  247. @endif
  248. @endif
  249. @else
  250. <!-- 选择提示 -->
  251. <div class="bg-white shadow rounded-lg p-12">
  252. <div class="text-center">
  253. <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  254. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
  255. </svg>
  256. <h3 class="mt-4 text-lg font-medium text-gray-900">选择学生查看知识图谱</h3>
  257. <p class="mt-2 text-sm text-gray-500">从上方下拉列表中选择一个学生,系统将自动加载其知识图谱数据</p>
  258. </div>
  259. </div>
  260. @endif
  261. </div>
  262. <!-- 知识图谱脚本 -->
  263. @push('scripts')
  264. <script src="https://d3js.org/d3.v7.min.js"></script>
  265. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  266. <script>
  267. window.kgDetailLayout = '{{ $detailLayout ?? 'inline' }}';
  268. document.addEventListener('livewire:initialized', () => {
  269. const knowledgeGraph = @this.knowledgePoints;
  270. if (knowledgeGraph && knowledgeGraph.nodes && knowledgeGraph.nodes.length > 0) {
  271. renderKnowledgeGraph(knowledgeGraph);
  272. renderMasteryChart(@this.masteryData.masteries);
  273. }
  274. // 监听数据更新
  275. Livewire.on('knowledgeGraphUpdated', (data) => {
  276. renderKnowledgeGraph(data);
  277. });
  278. });
  279. function renderKnowledgeGraph(data) {
  280. const container = document.getElementById('knowledge-graph');
  281. if (!container) return;
  282. // 清空容器
  283. container.innerHTML = '';
  284. const width = container.clientWidth;
  285. const height = container.clientHeight;
  286. const svg = d3.select('#knowledge-graph')
  287. .append('svg')
  288. .attr('width', width)
  289. .attr('height', height);
  290. // 创建力导向图
  291. const simulation = d3.forceSimulation(data.nodes)
  292. .force('link', d3.forceLink(data.links).id(d => d.id).distance(100))
  293. .force('charge', d3.forceManyBody().strength(-300))
  294. .force('center', d3.forceCenter(width / 2, height / 2));
  295. // 绘制边
  296. const link = svg.append('g')
  297. .selectAll('line')
  298. .data(data.links)
  299. .enter().append('line')
  300. .attr('stroke', '#999')
  301. .attr('stroke-opacity', 0.6)
  302. .attr('stroke-width', d => Math.sqrt(d.strength * 5));
  303. // 创建tooltip
  304. const tooltip = d3.select('body').append('div')
  305. .attr('class', 'knowledge-graph-tooltip')
  306. .style('position', 'absolute')
  307. .style('visibility', 'hidden')
  308. .style('background-color', 'rgba(0, 0, 0, 0.8)')
  309. .style('color', '#fff')
  310. .style('padding', '8px 12px')
  311. .style('border-radius', '6px')
  312. .style('font-size', '12px')
  313. .style('pointer-events', 'none')
  314. .style('z-index', '9999');
  315. // 绘制节点
  316. const node = svg.append('g')
  317. .selectAll('circle')
  318. .data(data.nodes)
  319. .enter().append('circle')
  320. .attr('r', d => d.size)
  321. .attr('fill', d => d.color)
  322. .attr('stroke', '#fff')
  323. .attr('stroke-width', 2)
  324. .style('cursor', 'pointer')
  325. .on('mouseover', function(event, d) {
  326. tooltip.style('visibility', 'visible')
  327. .html(`<strong>${d.label}</strong><br/>
  328. 掌握度: ${(d.mastery * 100).toFixed(1)}%<br/>
  329. 节点ID: ${d.id}`);
  330. })
  331. .on('mousemove', function(event) {
  332. tooltip.style('top', (event.pageY - 10) + 'px')
  333. .style('left', (event.pageX + 10) + 'px');
  334. })
  335. .on('mouseout', function() {
  336. tooltip.style('visibility', 'hidden');
  337. })
  338. .on('click', function(event, d) {
  339. showNodeDetails(d);
  340. })
  341. .call(d3.drag()
  342. .on('start', dragstarted)
  343. .on('drag', dragged)
  344. .on('end', dragended));
  345. // 添加标签
  346. const label = svg.append('g')
  347. .selectAll('text')
  348. .data(data.nodes)
  349. .enter().append('text')
  350. .text(d => d.label)
  351. .attr('font-size', '12px')
  352. .attr('fill', '#333')
  353. .attr('text-anchor', 'middle')
  354. .attr('dy', '.35em');
  355. // 更新位置
  356. simulation.on('tick', () => {
  357. link
  358. .attr('x1', d => d.source.x)
  359. .attr('y1', d => d.source.y)
  360. .attr('x2', d => d.target.x)
  361. .attr('y2', d => d.target.y);
  362. node
  363. .attr('cx', d => d.x)
  364. .attr('cy', d => d.y);
  365. label
  366. .attr('x', d => d.x)
  367. .attr('y', d => d.y + d.size + 15);
  368. });
  369. function dragstarted(event, d) {
  370. if (!event.active) simulation.alphaTarget(0.3).restart();
  371. d.fx = d.x;
  372. d.fy = d.y;
  373. }
  374. function dragged(event, d) {
  375. d.fx = event.x;
  376. d.fy = event.y;
  377. }
  378. function dragended(event, d) {
  379. if (!event.active) simulation.alphaTarget(0);
  380. d.fx = null;
  381. d.fy = null;
  382. }
  383. // 添加缩放功能
  384. const zoom = d3.zoom()
  385. .scaleExtent([0.5, 3])
  386. .on('zoom', (event) => {
  387. svg.selectAll('g').attr('transform', event.transform);
  388. });
  389. svg.call(zoom);
  390. // 节点详情展示函数
  391. function showNodeDetails(nodeData) {
  392. if (window.kgDetailLayout === 'inline') {
  393. const panel = document.getElementById('kg-node-detail');
  394. if (!panel) return;
  395. panel.classList.remove('hidden');
  396. document.getElementById('kg-detail-title').textContent = nodeData.label || '-';
  397. document.getElementById('kg-detail-code').textContent = `ID: ${nodeData.id}`;
  398. document.getElementById('kg-detail-mastery').textContent = `${(nodeData.mastery * 100).toFixed(1)}%`;
  399. document.getElementById('kg-detail-accuracy').textContent = nodeData.accuracy ? `准确率: ${(nodeData.accuracy * 100).toFixed(1)}%` : '';
  400. document.getElementById('kg-detail-attempts').textContent = nodeData.attempts ? `练习次数: ${nodeData.attempts}` : '';
  401. const skillsWrap = document.getElementById('kg-detail-skills');
  402. if (skillsWrap) {
  403. skillsWrap.innerHTML = '';
  404. if (nodeData.skills && nodeData.skills.length) {
  405. nodeData.skills.forEach((s) => {
  406. const span = document.createElement('span');
  407. span.className = 'rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700';
  408. span.textContent = s;
  409. skillsWrap.appendChild(span);
  410. });
  411. } else {
  412. const span = document.createElement('span');
  413. span.className = 'text-gray-500';
  414. span.textContent = '暂无技能要点';
  415. skillsWrap.appendChild(span);
  416. }
  417. }
  418. } else {
  419. alert(`知识点详情:\n\n名称: ${nodeData.label}\n代码: ${nodeData.id}\n掌握度: ${(nodeData.mastery * 100).toFixed(1)}%\n\n点击确定继续浏览图谱`);
  420. }
  421. }
  422. // 导出PNG功能
  423. window.exportGraph = function() {
  424. const container = document.getElementById('knowledge-graph');
  425. if (!container) return;
  426. const svgElement = container.querySelector('svg');
  427. if (!svgElement) return;
  428. // 创建canvas
  429. const canvas = document.createElement('canvas');
  430. const ctx = canvas.getContext('2d');
  431. const data = (new XMLSerializer()).serializeToString(svgElement);
  432. const DOMURL = window.URL || window.webkitURL || window;
  433. const img = new Image();
  434. const svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
  435. const url = DOMURL.createObjectURL(svgBlob);
  436. img.onload = function () {
  437. canvas.width = svgElement.clientWidth;
  438. canvas.height = svgElement.clientHeight;
  439. ctx.fillStyle = '#ffffff';
  440. ctx.fillRect(0, 0, canvas.width, canvas.height);
  441. ctx.drawImage(img, 0, 0);
  442. DOMURL.revokeObjectURL(url);
  443. // 下载图片
  444. const link = document.createElement('a');
  445. link.download = `知识图谱_${new Date().getTime()}.png`;
  446. link.href = canvas.toDataURL('image/png');
  447. link.click();
  448. };
  449. img.src = url;
  450. };
  451. }
  452. function renderMasteryChart(masteries) {
  453. const ctx = document.getElementById('mastery-distribution');
  454. if (!ctx) return;
  455. // 分类数据
  456. const ranges = {
  457. '优秀 (≥80%)': 0,
  458. '良好 (60-80%)': 0,
  459. '中等 (40-60%)': 0,
  460. '待提高 (20-40%)': 0,
  461. '薄弱 (<20%)': 0
  462. };
  463. masteries.forEach(m => {
  464. const mastery = m.mastery_level * 100;
  465. if (mastery >= 80) ranges['优秀 (≥80%)']++;
  466. else if (mastery >= 60) ranges['良好 (60-80%)']++;
  467. else if (mastery >= 40) ranges['中等 (40-60%)']++;
  468. else if (mastery >= 20) ranges['待提高 (20-40%)']++;
  469. else ranges['薄弱 (<20%)']++;
  470. });
  471. new Chart(ctx, {
  472. type: 'bar',
  473. data: {
  474. labels: Object.keys(ranges),
  475. datasets: [{
  476. label: '知识点数量',
  477. data: Object.values(ranges),
  478. backgroundColor: [
  479. '#10b981',
  480. '#3b82f6',
  481. '#f59e0b',
  482. '#f97316',
  483. '#ef4444'
  484. ]
  485. }]
  486. },
  487. options: {
  488. responsive: true,
  489. maintainAspectRatio: false,
  490. plugins: {
  491. legend: {
  492. display: false
  493. }
  494. },
  495. scales: {
  496. y: {
  497. beginAtZero: true,
  498. ticks: {
  499. stepSize: 1
  500. }
  501. }
  502. }
  503. }
  504. });
  505. }
  506. </script>
  507. @endpush
  508. </div>