import { getGraph, syncGraphFromTables } from './manager.js'; import { showHtmlModal } from '../../ui/page-window.js'; let echartsLoaded = false; async function loadECharts() { if (echartsLoaded) return; if (window.echarts) { echartsLoaded = true; return; } return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js'; script.onload = () => { echartsLoaded = true; resolve(); }; script.onerror = reject; document.head.appendChild(script); }); } export async function showGraphVisualization() { // 确保数据是最新的 syncGraphFromTables(); const modalHtml = `
正在加载图谱可视化引擎...
滚轮缩放 / 拖拽平移 / 点击节点查看详情
`; // Open Modal showHtmlModal('人物关系图谱', modalHtml, { wide: true, large: true, onShow: async (dialogElement) => { try { await loadECharts(); const container = dialogElement.find('#amily2-graph-container')[0]; container.innerHTML = ''; // Clear loading // Initialize Chart const chart = echarts.init(container); renderChart(chart); // Bind Buttons dialogElement.find('#amily2-refresh-graph-btn').on('click', () => { syncGraphFromTables(); renderChart(chart); toastr.success('图谱数据已刷新'); }); dialogElement.find('#amily2-reorganize-graph-btn').on('click', () => { chart.setOption({ series: [{ layout: 'force', force: { initLayout: null, repulsion: 300 + Math.random() * 50 // Slight change to trigger re-layout } }] }); }); // Handle resize new ResizeObserver(() => chart.resize()).observe(container); } catch (e) { console.error('ECharts loading failed', e); dialogElement.find('#amily2-graph-container').html('

图谱引擎加载失败,请检查网络连接。

'); } } }); } function renderChart(chart) { const graphData = getGraph(); if (!graphData.nodes || graphData.nodes.length === 0) { chart.clear(); chart.setOption({ title: { text: '暂无数据', subtext: '请确保已启用“角色”相关表格,并且表格中有数据。', left: 'center', top: 'center', textStyle: { color: '#888' } } }); return; } // Transform data for ECharts const nodes = graphData.nodes.map(n => ({ id: n.id, name: n.label, symbolSize: n.type === 'user' ? 40 : 25, itemStyle: { color: n.type === 'user' ? '#9e8aff' : '#4caf50', shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' }, label: { show: true, position: 'right', color: '#eee', formatter: '{b}' }, // Store original metadata data: n })); const links = graphData.edges.map(e => ({ source: e.source, target: e.target, value: e.relation, label: { show: true, formatter: '{c}', color: '#aaa', fontSize: 10 }, lineStyle: { curveness: 0.2, color: 'rgba(255,255,255,0.4)' } })); const option = { backgroundColor: 'transparent', tooltip: { trigger: 'item', backgroundColor: 'rgba(50,50,50,0.9)', borderColor: '#777', textStyle: { color: '#fff' }, formatter: function (params) { if (params.dataType === 'node') { const meta = params.data.data.metadata || {}; let info = meta.info || '暂无信息'; // Truncate long info if (info.length > 100) info = info.substring(0, 100) + '...'; return `
${params.name}
类型: ${params.data.data.type}
${info}
`; } else { return `${params.data.source} -> ${params.data.target}
关系: ${params.data.value}`; } } }, legend: { show: false }, animationDurationUpdate: 1500, animationEasingUpdate: 'quinticInOut', series: [ { type: 'graph', layout: 'force', data: nodes, links: links, roam: true, draggable: true, label: { position: 'right', formatter: '{b}' }, lineStyle: { color: 'source', curveness: 0.3 }, force: { repulsion: 400, edgeLength: 120, gravity: 0.1 }, emphasis: { focus: 'adjacency', itemStyle: { shadowBlur: 20, shadowColor: '#fff' }, lineStyle: { width: 4, color: '#fff' } } } ] }; chart.setOption(option); }