Enhance graph visualization and data polling in Process.vue

- Added real-time graph data visualization with loading and waiting states to improve user feedback during graph construction.
- Implemented a polling mechanism to fetch graph data every 3 seconds, ensuring the graph updates dynamically as data becomes available.
- Improved error handling and rendering logic to manage different graph states effectively.
- Enhanced styling for loading indicators and graph elements for better user experience.
This commit is contained in:
666ghj 2025-12-10 18:34:49 +08:00
parent 5a27b7ca71
commit 23927dc64b

View file

@ -30,8 +30,18 @@
</div> </div>
<div class="graph-container" ref="graphContainer"> <div class="graph-container" ref="graphContainer">
<!-- 图谱可视化只要有数据就显示 -->
<div v-if="graphData" class="graph-view">
<svg ref="graphSvg" class="graph-svg"></svg>
<!-- 构建中提示 -->
<div v-if="currentPhase === 1" class="graph-building-hint">
<span class="building-dot"></span>
实时更新中...
</div>
</div>
<!-- 加载状态 --> <!-- 加载状态 -->
<div v-if="graphLoading" class="graph-loading"> <div v-else-if="graphLoading" class="graph-loading">
<div class="loading-animation"> <div class="loading-animation">
<div class="loading-ring"></div> <div class="loading-ring"></div>
<div class="loading-ring"></div> <div class="loading-ring"></div>
@ -41,7 +51,7 @@
</div> </div>
<!-- 等待构建 --> <!-- 等待构建 -->
<div v-else-if="!graphData && currentPhase < 2" class="graph-waiting"> <div v-else-if="currentPhase < 1" class="graph-waiting">
<div class="waiting-icon"> <div class="waiting-icon">
<svg viewBox="0 0 100 100" class="network-icon"> <svg viewBox="0 0 100 100" class="network-icon">
<circle cx="50" cy="20" r="8" fill="none" stroke="#000" stroke-width="1.5"/> <circle cx="50" cy="20" r="8" fill="none" stroke="#000" stroke-width="1.5"/>
@ -55,13 +65,19 @@
<line x1="50" y1="72" x2="74" y2="66" stroke="#000" stroke-width="1"/> <line x1="50" y1="72" x2="74" y2="66" stroke="#000" stroke-width="1"/>
</svg> </svg>
</div> </div>
<p class="waiting-text">等待图谱构建完</p> <p class="waiting-text">等待本体生</p>
<p class="waiting-hint">完成本体生成后将自动开始构建</p> <p class="waiting-hint">成完成后将自动开始构建图谱</p>
</div> </div>
<!-- 图谱可视化 --> <!-- 构建中但还没有数据 -->
<div v-else-if="graphData" class="graph-view"> <div v-else-if="currentPhase === 1 && !graphData" class="graph-waiting">
<svg ref="graphSvg" class="graph-svg"></svg> <div class="loading-animation">
<div class="loading-ring"></div>
<div class="loading-ring"></div>
<div class="loading-ring"></div>
</div>
<p class="waiting-text">图谱构建中</p>
<p class="waiting-hint">数据即将显示...</p>
</div> </div>
<!-- 错误状态 --> <!-- 错误状态 -->
@ -497,7 +513,15 @@ const startBuildGraph = async () => {
if (response.success) { if (response.success) {
buildProgress.value.message = '图谱构建任务已启动...' buildProgress.value.message = '图谱构建任务已启动...'
startPollingTask(response.data.task_id)
// task_id
const taskId = response.data.task_id
//
startGraphPolling()
//
startPollingTask(taskId)
} else { } else {
error.value = response.error || '启动图谱构建失败' error.value = response.error || '启动图谱构建失败'
buildProgress.value = null buildProgress.value = null
@ -509,6 +533,58 @@ const startBuildGraph = async () => {
} }
} }
//
let graphPollTimer = null
//
const startGraphPolling = () => {
// 3
graphPollTimer = setInterval(async () => {
await fetchGraphData()
}, 3000)
}
//
const stopGraphPolling = () => {
if (graphPollTimer) {
clearInterval(graphPollTimer)
graphPollTimer = null
}
}
//
const fetchGraphData = async () => {
try {
// graph_id
const projectResponse = await getProject(currentProjectId.value)
if (projectResponse.success && projectResponse.data.graph_id) {
const graphId = projectResponse.data.graph_id
projectData.value = projectResponse.data
//
const graphResponse = await getGraphData(graphId)
if (graphResponse.success && graphResponse.data) {
const newData = graphResponse.data
const newNodeCount = newData.node_count || newData.nodes?.length || 0
const oldNodeCount = graphData.value?.node_count || graphData.value?.nodes?.length || 0
console.log('Fetching graph data, nodes:', newNodeCount, 'edges:', newData.edge_count || newData.edges?.length || 0)
//
if (newNodeCount !== oldNodeCount || !graphData.value) {
graphData.value = newData
await nextTick()
renderGraph()
}
}
}
} catch (err) {
console.log('Graph data fetch:', err.message || 'not ready')
}
}
// //
const startPollingTask = (taskId) => { const startPollingTask = (taskId) => {
// //
@ -538,6 +614,7 @@ const pollTaskStatus = async (taskId) => {
if (task.status === 'completed') { if (task.status === 'completed') {
stopPolling() stopPolling()
stopGraphPolling()
currentPhase.value = 2 currentPhase.value = 2
// graph_id // graph_id
@ -545,13 +622,14 @@ const pollTaskStatus = async (taskId) => {
if (projectResponse.success) { if (projectResponse.success) {
projectData.value = projectResponse.data projectData.value = projectResponse.data
// //
if (projectResponse.data.graph_id) { if (projectResponse.data.graph_id) {
await loadGraph(projectResponse.data.graph_id) await loadGraph(projectResponse.data.graph_id)
} }
} }
} else if (task.status === 'failed') { } else if (task.status === 'failed') {
stopPolling() stopPolling()
stopGraphPolling()
error.value = '图谱构建失败: ' + (task.error || '未知错误') error.value = '图谱构建失败: ' + (task.error || '未知错误')
buildProgress.value = null buildProgress.value = null
} }
@ -588,76 +666,137 @@ const loadGraph = async (graphId) => {
// (D3.js) // (D3.js)
const renderGraph = () => { const renderGraph = () => {
if (!graphSvg.value || !graphData.value) return if (!graphSvg.value || !graphData.value) {
console.log('Cannot render: svg or data missing')
return
}
const container = graphContainer.value const container = graphContainer.value
const width = container.clientWidth if (!container) {
const height = container.clientHeight - 60 console.log('Cannot render: container missing')
return
}
//
const rect = container.getBoundingClientRect()
const width = rect.width || 800
const height = (rect.height || 600) - 60
if (width <= 0 || height <= 0) {
console.log('Cannot render: invalid dimensions', width, height)
return
}
console.log('Rendering graph:', width, 'x', height)
const svg = d3.select(graphSvg.value) const svg = d3.select(graphSvg.value)
.attr('width', width) .attr('width', width)
.attr('height', height) .attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`)
svg.selectAll('*').remove() svg.selectAll('*').remove()
const nodes = graphData.value.nodes.map(n => ({ //
const nodesData = graphData.value.nodes || []
const edgesData = graphData.value.edges || []
if (nodesData.length === 0) {
console.log('No nodes to render')
//
svg.append('text')
.attr('x', width / 2)
.attr('y', height / 2)
.attr('text-anchor', 'middle')
.attr('fill', '#999')
.text('等待图谱数据...')
return
}
const nodes = nodesData.map(n => ({
id: n.uuid, id: n.uuid,
name: n.name, name: n.name || '未命名',
type: n.labels?.find(l => l !== 'Entity') || 'Entity' type: n.labels?.find(l => l !== 'Entity' && l !== 'Node') || 'Entity'
})) }))
const edges = graphData.value.edges.map(e => ({ // ID
source: e.source_node_uuid, const nodeIds = new Set(nodes.map(n => n.id))
target: e.target_node_uuid,
type: e.fact_type || 'RELATED_TO' const edges = edgesData
})) .filter(e => nodeIds.has(e.source_node_uuid) && nodeIds.has(e.target_node_uuid))
.map(e => ({
source: e.source_node_uuid,
target: e.target_node_uuid,
type: e.fact_type || e.name || 'RELATED_TO'
}))
console.log('Nodes:', nodes.length, 'Edges:', edges.length)
// //
const types = [...new Set(nodes.map(n => n.type))]
const colorScale = d3.scaleOrdinal() const colorScale = d3.scaleOrdinal()
.domain([...new Set(nodes.map(n => n.type))]) .domain(types)
.range(['#FF6B35', '#004E89', '#7B2D8E', '#1A936F', '#C5283D', '#E9724C']) .range(['#FF6B35', '#004E89', '#7B2D8E', '#1A936F', '#C5283D', '#E9724C', '#2D3436', '#6C5CE7'])
// //
const simulation = d3.forceSimulation(nodes) const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(edges).id(d => d.id).distance(80)) .force('link', d3.forceLink(edges).id(d => d.id).distance(100).strength(0.5))
.force('charge', d3.forceManyBody().strength(-200)) .force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2)) .force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(30)) .force('collision', d3.forceCollide().radius(40))
.force('x', d3.forceX(width / 2).strength(0.05))
.force('y', d3.forceY(height / 2).strength(0.05))
//
const g = svg.append('g')
svg.call(d3.zoom()
.extent([[0, 0], [width, height]])
.scaleExtent([0.2, 4])
.on('zoom', (event) => {
g.attr('transform', event.transform)
}))
// //
const link = svg.append('g') const link = g.append('g')
.attr('class', 'links') .attr('class', 'links')
.selectAll('line') .selectAll('line')
.data(edges) .data(edges)
.enter() .enter()
.append('line') .append('line')
.attr('stroke', '#ddd') .attr('stroke', '#ccc')
.attr('stroke-width', 1) .attr('stroke-width', 1.5)
.attr('stroke-opacity', 0.6)
// //
const node = svg.append('g') const node = g.append('g')
.attr('class', 'nodes') .attr('class', 'nodes')
.selectAll('g') .selectAll('g')
.data(nodes) .data(nodes)
.enter() .enter()
.append('g') .append('g')
.style('cursor', 'pointer')
.call(d3.drag() .call(d3.drag()
.on('start', dragstarted) .on('start', dragstarted)
.on('drag', dragged) .on('drag', dragged)
.on('end', dragended)) .on('end', dragended))
node.append('circle') node.append('circle')
.attr('r', 8) .attr('r', 10)
.attr('fill', d => colorScale(d.type)) .attr('fill', d => colorScale(d.type))
.attr('stroke', '#fff') .attr('stroke', '#fff')
.attr('stroke-width', 2) .attr('stroke-width', 2)
node.append('text') node.append('text')
.attr('dx', 12) .attr('dx', 14)
.attr('dy', 4) .attr('dy', 4)
.text(d => d.name?.substring(0, 10) || '') .text(d => d.name?.substring(0, 12) || '')
.attr('font-size', '10px') .attr('font-size', '11px')
.attr('fill', '#333') .attr('fill', '#333')
.attr('font-family', 'JetBrains Mono, monospace')
// tooltip
node.append('title')
.text(d => `${d.name}\n类型: ${d.type}`)
simulation.on('tick', () => { simulation.on('tick', () => {
link link
@ -701,6 +840,7 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
stopPolling() stopPolling()
stopGraphPolling()
}) })
</script> </script>
@ -937,11 +1077,35 @@ onUnmounted(() => {
.graph-view { .graph-view {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative;
} }
.graph-svg { .graph-svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block;
}
.graph-building-hint {
position: absolute;
bottom: 16px;
left: 16px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgba(255, 107, 53, 0.1);
border: 1px solid #FF6B35;
font-size: 0.8rem;
color: #FF6B35;
}
.building-dot {
width: 8px;
height: 8px;
background: #FF6B35;
border-radius: 50%;
animation: pulse 1s infinite;
} }
.error-icon { .error-icon {