knowledge-dependency-graph.blade.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. <div>
  2. {{-- 加载状态 --}}
  3. @if ($isLoading)
  4. <div class="flex items-center justify-center h-96">
  5. <svg class="animate-spin h-8 w-8 text-indigo-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
  6. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
  7. <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>
  8. </svg>
  9. <span class="ml-3 text-gray-600">正在加载依赖关系图...</span>
  10. </div>
  11. @elseif ($errorMessage)
  12. <div class="rounded-md bg-red-50 p-4">
  13. <div class="flex">
  14. <div class="flex-shrink-0">
  15. <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
  16. <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
  17. </svg>
  18. </div>
  19. <div class="ml-3">
  20. <h3 class="text-sm font-medium text-red-800">加载失败</h3>
  21. <div class="mt-2 text-sm text-red-700">
  22. <p>{{ $errorMessage }}</p>
  23. </div>
  24. </div>
  25. </div>
  26. </div>
  27. @elseif (empty($graphData['nodes']))
  28. <div class="text-center py-12">
  29. <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  30. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
  31. </svg>
  32. <p class="mt-2 text-sm text-gray-500">暂无依赖关系数据</p>
  33. </div>
  34. @else
  35. <div class="space-y-4">
  36. {{-- 图例 --}}
  37. <div class="bg-gray-50 rounded-lg p-4">
  38. <h4 class="text-sm font-medium text-gray-900 mb-3">图例说明</h4>
  39. <div class="flex flex-wrap gap-4">
  40. <div class="flex items-center">
  41. <div class="w-4 h-4 bg-red-500 rounded-full mr-2"></div>
  42. <span class="text-xs text-gray-600">薄弱 (0-30%)</span>
  43. </div>
  44. <div class="flex items-center">
  45. <div class="w-4 h-4 bg-orange-500 rounded-full mr-2"></div>
  46. <span class="text-xs text-gray-600">入门 (30-50%)</span>
  47. </div>
  48. <div class="flex items-center">
  49. <div class="w-4 h-4 bg-yellow-500 rounded-full mr-2"></div>
  50. <span class="text-xs text-gray-600">一般 (50-70%)</span>
  51. </div>
  52. <div class="flex items-center">
  53. <div class="w-4 h-4 bg-green-500 rounded-full mr-2"></div>
  54. <span class="text-xs text-gray-600">良好 (70-85%)</span>
  55. </div>
  56. <div class="flex items-center">
  57. <div class="w-4 h-4 bg-blue-500 rounded-full mr-2"></div>
  58. <span class="text-xs text-gray-600">掌握 (85%+)</span>
  59. </div>
  60. </div>
  61. </div>
  62. {{-- 图形容器 --}}
  63. <div class="relative bg-white rounded-lg border border-gray-200" style="height: 500px;">
  64. <div id="knowledgeGraph" class="w-full h-full"></div>
  65. {{-- 节点详情面板 --}}
  66. @if ($selectedNode)
  67. <div class="absolute top-4 right-4 w-64 bg-white rounded-lg shadow-lg border border-gray-200 p-4">
  68. <div class="flex items-center justify-between mb-3">
  69. <h4 class="text-sm font-medium text-gray-900">节点详情</h4>
  70. <button wire:click="selectNode(null)" class="text-gray-400 hover:text-gray-600">
  71. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  72. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
  73. </svg>
  74. </button>
  75. </div>
  76. @php
  77. $node = collect($graphData['nodes'])->firstWhere('id', $selectedNode);
  78. @endphp
  79. @if ($node)
  80. <div class="space-y-2">
  81. <div>
  82. <span class="text-xs text-gray-500">编码</span>
  83. <div class="text-sm font-medium text-gray-900">{{ $node['id'] }}</div>
  84. </div>
  85. <div>
  86. <span class="text-xs text-gray-500">名称</span>
  87. <div class="text-sm font-medium text-gray-900">{{ $node['label'] }}</div>
  88. </div>
  89. <div>
  90. <span class="text-xs text-gray-500">掌握度</span>
  91. <div class="text-sm font-medium text-gray-900">{{ number_format($node['mastery'] * 100, 1) }}%</div>
  92. </div>
  93. <div class="pt-2 border-t border-gray-200">
  94. @php
  95. $incomingEdges = collect($graphData['edges'])->where('to', $selectedNode);
  96. $outgoingEdges = collect($graphData['edges'])->where('from', $selectedNode);
  97. @endphp
  98. <div class="text-xs text-gray-600">
  99. 前置知识点: {{ $incomingEdges->count() }} 个
  100. </div>
  101. <div class="text-xs text-gray-600">
  102. 依赖知识点: {{ $outgoingEdges->count() }} 个
  103. </div>
  104. </div>
  105. </div>
  106. @endif
  107. </div>
  108. @endif
  109. </div>
  110. </div>
  111. {{-- vis.js 网络图脚本 --}}
  112. <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
  113. <script>
  114. document.addEventListener('DOMContentLoaded', function() {
  115. const container = document.getElementById('knowledgeGraph');
  116. if (!container) return;
  117. const graphData = @json($graphData);
  118. const nodes = new vis.DataSet(graphData.nodes.map(node => ({
  119. id: node.id,
  120. label: node.label,
  121. color: {
  122. background: node.color,
  123. border: '#ffffff',
  124. highlight: {
  125. background: node.color,
  126. border: '#000000'
  127. }
  128. },
  129. size: node.size,
  130. font: {
  131. color: '#ffffff',
  132. size: 12,
  133. face: 'arial'
  134. },
  135. borderWidth: 2,
  136. borderWidthSelected: 3,
  137. shadow: true,
  138. })));
  139. const edges = new vis.DataSet(graphData.edges.map(edge => ({
  140. from: edge.from,
  141. to: edge.to,
  142. width: edge.width,
  143. color: {
  144. color: edge.color,
  145. highlight: '#000000'
  146. },
  147. arrows: 'to',
  148. smooth: {
  149. type: 'continuous',
  150. roundness: 0.2
  151. },
  152. label: edge.label,
  153. font: {
  154. size: 10,
  155. color: '#666666',
  156. strokeWidth: 0
  157. }
  158. })));
  159. const data = {
  160. nodes: nodes,
  161. edges: edges
  162. };
  163. const options = {
  164. physics: {
  165. enabled: true,
  166. stabilization: {
  167. enabled: true,
  168. iterations: 100
  169. },
  170. barnesHut: {
  171. gravitationalConstant: -8000,
  172. centralGravity: 0.3,
  173. springLength: 120,
  174. springConstant: 0.04,
  175. damping: 0.09
  176. }
  177. },
  178. interaction: {
  179. hover: true,
  180. hoverConnectedEdges: true,
  181. selectConnectedEdges: false,
  182. tooltipDelay: 200
  183. },
  184. nodes: {
  185. shape: 'dot',
  186. borderWidth: 2,
  187. borderWidthSelected: 3
  188. },
  189. edges: {
  190. arrows: {
  191. to: { enabled: true, scaleFactor: 1, type: 'arrow' }
  192. },
  193. color: {
  194. hover: '#000000'
  195. },
  196. smooth: {
  197. enabled: true,
  198. type: 'continuous'
  199. }
  200. },
  201. layout: {
  202. improvedLayout: true,
  203. hierarchical: {
  204. enabled: false
  205. }
  206. }
  207. };
  208. const network = new vis.Network(container, data, options);
  209. // 节点点击事件
  210. network.on('click', function(params) {
  211. if (params.nodes.length > 0) {
  212. const nodeId = params.nodes[0];
  213. @this.call('selectNode', nodeId);
  214. } else {
  215. @this.call('selectNode', null);
  216. }
  217. });
  218. // 节点悬停事件
  219. network.on('hoverNode', function(params) {
  220. container.style.cursor = 'pointer';
  221. });
  222. network.on('blurNode', function(params) {
  223. container.style.cursor = 'default';
  224. });
  225. // 监听 Livewire 的数据更新
  226. if (window.Livewire) {
  227. Livewire.on('graphDataUpdated', (newGraphData) => {
  228. nodes.clear();
  229. edges.clear();
  230. nodes.add(newGraphData.nodes);
  231. edges.add(newGraphData.edges);
  232. network.redraw();
  233. });
  234. }
  235. });
  236. </script>
  237. @endif
  238. </div>