| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- <div class="space-y-4">
- {{-- 控制栏 --}}
- <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
- <div class="flex items-center justify-between">
- <div class="flex items-center gap-4">
- {{-- 布局切换 --}}
- <div class="flex items-center gap-2">
- <span class="text-sm font-medium text-gray-700 dark:text-gray-300">布局:</span>
- <div class="inline-flex rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
- <button
- wire:click="setLayoutType('full')"
- class="px-3 py-1 text-sm {{ $layoutType === 'full' ? 'bg-indigo-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700' }}"
- >
- 全图模式
- </button>
- <button
- wire:click="setLayoutType('selected')"
- class="px-3 py-1 text-sm {{ $layoutType === 'selected' ? 'bg-indigo-600 text-white' : 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700' }}"
- >
- 选中模式
- </button>
- </div>
- </div>
- {{-- 筛选器 --}}
- <div class="flex items-center gap-2">
- <select
- wire:model.live="filterPhase"
- class="text-sm border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-100"
- >
- <option value="">所有学段</option>
- @foreach($this->filterOptions['phases'] as $phase)
- <option value="{{ $phase }}">{{ $phase }}</option>
- @endforeach
- </select>
- <select
- wire:model.live="filterCategory"
- class="text-sm border-gray-300 dark:border-gray-600 rounded-lg dark:bg-gray-700 dark:text-gray-100"
- >
- <option value="">所有类别</option>
- @foreach($this->filterOptions['categories'] as $category)
- <option value="{{ $category }}">{{ $category }}</option>
- @endforeach
- </select>
- </div>
- </div>
- <div class="flex items-center gap-2">
- @if($selectedKpCode)
- <div class="flex items-center gap-2 px-3 py-1 bg-indigo-100 dark:bg-indigo-900 rounded-lg">
- <span class="text-sm font-medium text-indigo-900 dark:text-indigo-300">
- 已选择: {{ $selectedKpCode }}
- </span>
- <button
- wire:click="clearSelection"
- class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-200"
- >
- <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>
- @else
- <span class="text-sm text-gray-500 dark:text-gray-400">点击节点查看详情</span>
- @endif
- <button
- wire:click="loadGraphData"
- class="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:text-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700"
- >
- <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="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>
- </svg>
- 刷新
- </button>
- </div>
- </div>
- </div>
- {{-- 图谱可视化区域 --}}
- <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden" style="height: 500px;" x-data="knowledgeGraph()">
- @if($isLoading)
- <div class="absolute inset-0 flex items-center justify-center bg-white/80 dark:bg-gray-800/80 z-10">
- <div class="flex items-center gap-3">
- <svg class="animate-spin h-6 w-6 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="text-gray-600 dark:text-gray-300">加载知识图谱...</span>
- </div>
- </div>
- @endif
- <div id="knowledge-graph-viz" class="w-full h-full"></div>
- {{-- 图例 --}}
- <div class="absolute top-4 right-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4 shadow-lg">
- <h4 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">图例</h4>
- <div class="space-y-2 text-xs">
- <div class="flex items-center gap-2">
- <div class="w-3 h-3 rounded-full bg-blue-500"></div>
- <span class="text-gray-600 dark:text-gray-400">知识点</span>
- </div>
- <div class="flex items-center gap-2">
- <div class="w-3 h-3 rounded-full bg-green-500"></div>
- <span class="text-gray-600 dark:text-gray-400">已掌握</span>
- </div>
- <div class="flex items-center gap-2">
- <div class="w-3 h-3 rounded-full bg-yellow-500"></div>
- <span class="text-gray-600 dark:text-gray-400">需加强</span>
- </div>
- </div>
- </div>
- {{-- 统计信息 --}}
- <div class="absolute bottom-4 left-4 bg-white dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 p-4 shadow-lg">
- <div class="text-xs text-gray-600 dark:text-gray-400 space-y-1">
- <div>节点: <span x-text="stats.nodes || 0">0</span></div>
- <div>边: <span x-text="stats.edges || 0">0</span></div>
- </div>
- </div>
- </div>
- {{-- 知识点详情面板 --}}
- <livewire:integrations.knowledge-point-details />
- </div>
- @push('styles')
- <style>
- /* G6 节点样式覆盖 */
- .g6-tooltip {
- border-radius: 8px !important;
- background: rgba(15, 23, 42, 0.95) !important;
- color: #e2e8f0 !important;
- padding: 10px 12px !important;
- font-size: 12px !important;
- box-shadow: 0 10px 30px rgba(0,0,0,0.18) !important;
- }
- </style>
- @endpush
- @push('scripts')
- <script>
- // 全局变量存储图谱实例
- window.knowledgeGraphInstance = null;
- // Alpine 组件定义
- function knowledgeGraph() {
- return {
- graph: null,
- data: @json($graphData),
- stats: {
- nodes: {{ count($graphData['nodes'] ?? []) }},
- edges: {{ count($graphData['edges'] ?? []) }}
- },
- selectedNode: '{{ $selectedKpCode ?? '' }}',
- init() {
- console.log('初始化知识图谱...', this.data);
- this.initGraph();
- this.bindEvents();
- },
- initGraph() {
- if (typeof window.KnowledgeMindmapGraph === 'undefined') {
- console.error('KnowledgeMindmapGraph 类未加载');
- return;
- }
- const container = document.getElementById('knowledge-graph-viz');
- if (!container) {
- console.error('找不到容器元素 #knowledge-graph-viz');
- return;
- }
- // 转换 API 数据为 tree.json 格式
- const treeData = this.transformApiDataToTree(this.data);
- // 实例化现有的 KnowledgeMindmapGraph
- this.graphInstance = new window.KnowledgeMindmapGraph({
- containerId: 'knowledge-graph-viz',
- livewireMethod: 'handleNodeSelected',
- livewireId: @this.id,
- emitSelection: true,
- showEdges: true,
- showRelationEdges: true,
- highlightLowMastery: false,
- });
- // 手动设置数据(绕过文件加载)
- this.graphInstance.rawTree = treeData;
- this.graphInstance.relationEdges = this.transformEdgesForKnowledgeMindmap(this.data.edges || []);
- this.graphInstance.treeData = this.graphInstance.transformNode(this.graphInstance.rawTree);
- this.graphInstance.buildParentMap(this.graphInstance.treeData);
- // 构建节点ID集合
- const flatIds = [];
- this.graphInstance.collectIds(this.graphInstance.treeData, flatIds);
- this.graphInstance.nodeIdSet = new Set(flatIds);
- // 渲染图谱
- this.graphInstance.renderGraph();
- // 暴露到全局
- window.knowledgeGraphInstance = this.graphInstance;
- window.knowledgeGraphG6Graph = this.graphInstance.graph;
- },
- transformDataForG6(apiData) {
- const nodes = apiData.nodes || [];
- const edges = apiData.edges || [];
- // 构建树形结构
- const nodeMap = {};
- nodes.forEach(node => {
- nodeMap[node.kp_code] = {
- id: node.kp_code,
- label: `${node.kp_code} · ${node.cn_name || node.kp_code}`,
- children: [],
- meta: {
- code: node.kp_code,
- name: node.cn_name || node.kp_code,
- phase: node.phase || '',
- category: node.category || '',
- importance: node.importance || 0,
- description: node.description || '',
- question_count: node.question_count || 0,
- has_mastery: node.question_count > 0,
- }
- };
- });
- // 构建父子关系
- const rootNodes = [];
- edges.forEach(edge => {
- const source = nodeMap[edge.from];
- const target = nodeMap[edge.to];
- if (source && target) {
- // 将 target 添加为 source 的子节点
- source.children.push(target);
- }
- });
- // 找出根节点(没有父节点的节点)
- const childCodes = new Set();
- edges.forEach(edge => {
- childCodes.add(edge.to);
- });
- nodes.forEach(node => {
- if (!childCodes.has(node.kp_code)) {
- rootNodes.push(nodeMap[node.kp_code]);
- }
- });
- // 如果只有一个根节点,直接返回;否则包装成虚拟根节点
- if (rootNodes.length === 1) {
- return rootNodes[0];
- } else if (rootNodes.length > 1) {
- return {
- id: 'root',
- label: '知识点根节点',
- children: rootNodes,
- meta: { isVirtualRoot: true }
- };
- } else {
- // 如果没有根节点,返回第一个节点
- return nodes.length > 0 ? nodeMap[nodes[0].kp_code] : null;
- }
- },
- // 转换 API 数据为 KnowledgeMindmapGraph 期望的 tree.json 格式
- transformApiDataToTree(apiData) {
- const nodes = apiData.nodes || [];
- const edges = apiData.edges || [];
- // 构建节点映射
- const nodeMap = {};
- nodes.forEach(node => {
- nodeMap[node.kp_code] = {
- id: node.kp_code,
- name: node.cn_name || node.kp_code,
- label: node.cn_name || node.kp_code,
- code: node.kp_code,
- children: [],
- };
- });
- // 构建父子关系(使用 children 属性)
- edges.forEach(edge => {
- const source = nodeMap[edge.from];
- const target = nodeMap[edge.to];
- if (source && target) {
- source.children.push(target);
- }
- });
- // 找出根节点(没有父节点的节点)
- const childCodes = new Set();
- edges.forEach(edge => {
- childCodes.add(edge.to);
- });
- const rootNodes = nodes
- .filter(node => !childCodes.has(node.kp_code))
- .map(node => nodeMap[node.kp_code]);
- // 构建最终的树形结构
- if (rootNodes.length === 1) {
- return rootNodes[0];
- } else if (rootNodes.length > 1) {
- return {
- id: 'root',
- name: '知识点根节点',
- label: '知识点根节点',
- code: 'root',
- children: rootNodes,
- };
- } else {
- // 如果没有根节点,返回第一个节点
- return nodes.length > 0 ? nodeMap[nodes[0].kp_code] : null;
- }
- },
- // 转换边数据为 KnowledgeMindmapGraph 期望的格式
- transformEdgesForKnowledgeMindmap(edges) {
- return edges.map((edge, index) => ({
- id: `rel-${index}`,
- source: edge.from,
- target: edge.to,
- type: edge.type || 'successor',
- edgeType: edge.type || 'successor',
- comment: edge.comment || '',
- }));
- },
- bindEvents() {
- // KnowledgeMindmapGraph 已经在构造函数中绑定了事件
- // 这里不需要额外绑定,因为节点点击事件会通过 notifySelection 传递到 Livewire
- // 监听自定义事件(从 KnowledgeMindmapGraph 发出)
- window.addEventListener('mindmap-node-selected', (evt) => {
- const model = evt.detail;
- console.log('节点选中事件:', model);
- });
- },
- // 更新图谱数据
- updateData(newData) {
- this.data = newData;
- this.stats = {
- nodes: (newData.nodes || []).length,
- edges: (newData.edges || []).length
- };
- if (this.graphInstance && this.graphInstance.graph) {
- // 转换新数据
- const treeData = this.transformApiDataToTree(newData);
- const edgesData = this.transformEdgesForKnowledgeMindmap(newData.edges || []);
- // 更新数据
- this.graphInstance.rawTree = treeData;
- this.graphInstance.relationEdges = edgesData;
- // 刷新图谱
- this.graphInstance.refreshGraph();
- }
- }
- }
- }
- // Livewire 事件监听
- document.addEventListener('livewire:initialized', () => {
- // 监听图谱数据更新事件
- Livewire.on('graphDataUpdated', (data) => {
- console.log('收到图谱数据更新:', data);
- const graphElement = document.querySelector('#knowledge-graph-viz');
- if (graphElement && graphElement._x_dataStack) {
- const graphComponent = graphElement._x_dataStack[0];
- if (graphComponent && graphComponent.updateData) {
- graphComponent.updateData(data);
- }
- }
- });
- // 节点选择事件
- Livewire.on('nodeSelected', (event) => {
- console.log('节点被选中:', event);
- // 通知详情面板
- Livewire.dispatch('kpSelected', { kpCode: event });
- });
- // 监听 Livewire 的数据更新
- Livewire.on('refreshGraph', () => {
- console.log('刷新图谱');
- @this.call('loadGraphData');
- });
- });
- // 页面加载完成后初始化
- document.addEventListener('DOMContentLoaded', () => {
- console.log('DOM 加载完成');
- });
- </script>
- @endpush
|