| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- <div>
- {{-- 加载状态 --}}
- @if ($isLoading)
- <div class="flex items-center justify-center h-96">
- <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">
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
- <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>
- </svg>
- <span class="ml-3 text-gray-600">正在加载依赖关系图...</span>
- </div>
- @elseif ($errorMessage)
- <div class="rounded-md bg-red-50 p-4">
- <div class="flex">
- <div class="flex-shrink-0">
- <svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
- <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" />
- </svg>
- </div>
- <div class="ml-3">
- <h3 class="text-sm font-medium text-red-800">加载失败</h3>
- <div class="mt-2 text-sm text-red-700">
- <p>{{ $errorMessage }}</p>
- </div>
- </div>
- </div>
- </div>
- @elseif (empty($graphData['nodes']))
- <div class="text-center py-12">
- <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <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>
- </svg>
- <p class="mt-2 text-sm text-gray-500">暂无依赖关系数据</p>
- </div>
- @else
- <div class="space-y-4">
- {{-- 图例 --}}
- <div class="bg-gray-50 rounded-lg p-4">
- <h4 class="text-sm font-medium text-gray-900 mb-3">图例说明</h4>
- <div class="flex flex-wrap gap-4">
- <div class="flex items-center">
- <div class="w-4 h-4 bg-red-500 rounded-full mr-2"></div>
- <span class="text-xs text-gray-600">薄弱 (0-30%)</span>
- </div>
- <div class="flex items-center">
- <div class="w-4 h-4 bg-orange-500 rounded-full mr-2"></div>
- <span class="text-xs text-gray-600">入门 (30-50%)</span>
- </div>
- <div class="flex items-center">
- <div class="w-4 h-4 bg-yellow-500 rounded-full mr-2"></div>
- <span class="text-xs text-gray-600">一般 (50-70%)</span>
- </div>
- <div class="flex items-center">
- <div class="w-4 h-4 bg-green-500 rounded-full mr-2"></div>
- <span class="text-xs text-gray-600">良好 (70-85%)</span>
- </div>
- <div class="flex items-center">
- <div class="w-4 h-4 bg-blue-500 rounded-full mr-2"></div>
- <span class="text-xs text-gray-600">掌握 (85%+)</span>
- </div>
- </div>
- </div>
- {{-- 图形容器 --}}
- <div class="relative bg-white rounded-lg border border-gray-200" style="height: 500px;">
- <div id="knowledgeGraph" class="w-full h-full"></div>
- {{-- 节点详情面板 --}}
- @if ($selectedNode)
- <div class="absolute top-4 right-4 w-64 bg-white rounded-lg shadow-lg border border-gray-200 p-4">
- <div class="flex items-center justify-between mb-3">
- <h4 class="text-sm font-medium text-gray-900">节点详情</h4>
- <button wire:click="selectNode(null)" class="text-gray-400 hover:text-gray-600">
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
- </svg>
- </button>
- </div>
- @php
- $node = collect($graphData['nodes'])->firstWhere('id', $selectedNode);
- @endphp
- @if ($node)
- <div class="space-y-2">
- <div>
- <span class="text-xs text-gray-500">编码</span>
- <div class="text-sm font-medium text-gray-900">{{ $node['id'] }}</div>
- </div>
- <div>
- <span class="text-xs text-gray-500">名称</span>
- <div class="text-sm font-medium text-gray-900">{{ $node['label'] }}</div>
- </div>
- <div>
- <span class="text-xs text-gray-500">掌握度</span>
- <div class="text-sm font-medium text-gray-900">{{ number_format($node['mastery'] * 100, 1) }}%</div>
- </div>
- <div class="pt-2 border-t border-gray-200">
- @php
- $incomingEdges = collect($graphData['edges'])->where('to', $selectedNode);
- $outgoingEdges = collect($graphData['edges'])->where('from', $selectedNode);
- @endphp
- <div class="text-xs text-gray-600">
- 前置知识点: {{ $incomingEdges->count() }} 个
- </div>
- <div class="text-xs text-gray-600">
- 依赖知识点: {{ $outgoingEdges->count() }} 个
- </div>
- </div>
- </div>
- @endif
- </div>
- @endif
- </div>
- </div>
- {{-- vis.js 网络图脚本 --}}
- <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- const container = document.getElementById('knowledgeGraph');
- if (!container) return;
- const graphData = @json($graphData);
- const nodes = new vis.DataSet(graphData.nodes.map(node => ({
- id: node.id,
- label: node.label,
- color: {
- background: node.color,
- border: '#ffffff',
- highlight: {
- background: node.color,
- border: '#000000'
- }
- },
- size: node.size,
- font: {
- color: '#ffffff',
- size: 12,
- face: 'arial'
- },
- borderWidth: 2,
- borderWidthSelected: 3,
- shadow: true,
- })));
- const edges = new vis.DataSet(graphData.edges.map(edge => ({
- from: edge.from,
- to: edge.to,
- width: edge.width,
- color: {
- color: edge.color,
- highlight: '#000000'
- },
- arrows: 'to',
- smooth: {
- type: 'continuous',
- roundness: 0.2
- },
- label: edge.label,
- font: {
- size: 10,
- color: '#666666',
- strokeWidth: 0
- }
- })));
- const data = {
- nodes: nodes,
- edges: edges
- };
- const options = {
- physics: {
- enabled: true,
- stabilization: {
- enabled: true,
- iterations: 100
- },
- barnesHut: {
- gravitationalConstant: -8000,
- centralGravity: 0.3,
- springLength: 120,
- springConstant: 0.04,
- damping: 0.09
- }
- },
- interaction: {
- hover: true,
- hoverConnectedEdges: true,
- selectConnectedEdges: false,
- tooltipDelay: 200
- },
- nodes: {
- shape: 'dot',
- borderWidth: 2,
- borderWidthSelected: 3
- },
- edges: {
- arrows: {
- to: { enabled: true, scaleFactor: 1, type: 'arrow' }
- },
- color: {
- hover: '#000000'
- },
- smooth: {
- enabled: true,
- type: 'continuous'
- }
- },
- layout: {
- improvedLayout: true,
- hierarchical: {
- enabled: false
- }
- }
- };
- const network = new vis.Network(container, data, options);
- // 节点点击事件
- network.on('click', function(params) {
- if (params.nodes.length > 0) {
- const nodeId = params.nodes[0];
- @this.call('selectNode', nodeId);
- } else {
- @this.call('selectNode', null);
- }
- });
- // 节点悬停事件
- network.on('hoverNode', function(params) {
- container.style.cursor = 'pointer';
- });
- network.on('blurNode', function(params) {
- container.style.cursor = 'default';
- });
- // 监听 Livewire 的数据更新
- if (window.Livewire) {
- Livewire.on('graphDataUpdated', (newGraphData) => {
- nodes.clear();
- edges.clear();
- nodes.add(newGraphData.nodes);
- edges.add(newGraphData.edges);
- network.redraw();
- });
- }
- });
- </script>
- @endif
- </div>
|