Compare commits
4 Commits
84cdf9ac0e
...
51d9007f14
| Author | SHA1 | Date |
|---|---|---|
|
|
51d9007f14 | 3 months ago |
|
|
16a59e40c6 | 3 months ago |
|
|
3fc816100d | 3 months ago |
|
|
3b53b2d7d1 | 3 months ago |
10 changed files with 1482 additions and 288 deletions
@ -0,0 +1,260 @@ |
|||
<template> |
|||
<div ref="toolbarContainer" class="custom-graph-toolbar"></div> |
|||
</template> |
|||
|
|||
<script> |
|||
import { ElMessageBox, ElMessage } from 'element-plus'; |
|||
|
|||
export default { |
|||
name: 'GraphToolbar', |
|||
props: ['graph'], |
|||
data() { |
|||
return { |
|||
observer: null // 增加样式监听器 |
|||
}; |
|||
}, |
|||
watch: { |
|||
graph: { |
|||
handler(newGraph) { |
|||
if (newGraph) { |
|||
this.$nextTick(() => { |
|||
// 给插件一点点初始化时间,确保 DOM 已存在 |
|||
setTimeout(() => this.mountG6Toolbar(), 200); |
|||
}); |
|||
} |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
beforeUnmount() { |
|||
// 组件销毁时断开监听,防止内存泄漏 |
|||
if (this.observer) this.observer.disconnect(); |
|||
}, |
|||
methods: { |
|||
mountG6Toolbar() { |
|||
const container = this.$refs.toolbarContainer; |
|||
const g6ToolbarElement = document.querySelector('.g6-toolbar'); |
|||
|
|||
// 【日志 1:输入状态检查】 |
|||
console.log('===[Toolbar Check Start]==='); |
|||
console.log('1. Graph 实例状态:', !!this.graph); |
|||
console.log('2. Vue 容器 (ref):', !!container); |
|||
console.log('3. 是否抓取到原生 G6 DOM:', !!g6ToolbarElement); |
|||
|
|||
if (!this.graph || !container || !g6ToolbarElement) { |
|||
console.warn('!!! [Toolbar] 挂载终止:由于缺少以上必要条件,搬家逻辑未执行。'); |
|||
return; |
|||
} |
|||
|
|||
// 1. 【搬家】将 G6 插件生成的 DOM 移动到 Vue 容器内 |
|||
container.appendChild(g6ToolbarElement); |
|||
|
|||
// 2. 【核心逻辑】锁死样式函数 |
|||
const lockStyle = () => { |
|||
g6ToolbarElement.style.setProperty('position', 'static', 'important'); |
|||
g6ToolbarElement.style.setProperty('inset', 'unset', 'important'); |
|||
g6ToolbarElement.style.setProperty('display', 'flex', 'important'); |
|||
g6ToolbarElement.style.setProperty('height', '100%', 'important'); |
|||
g6ToolbarElement.style.setProperty('background', 'transparent', 'important'); |
|||
g6ToolbarElement.style.setProperty('box-shadow', 'none', 'important'); |
|||
g6ToolbarElement.style.setProperty('border', 'none', 'important'); |
|||
g6ToolbarElement.style.setProperty('width', 'auto', 'important'); |
|||
}; |
|||
|
|||
// 3. 【强效监控】防止刷新或 Resize 时 G6 自动改回样式 |
|||
if (this.observer) this.observer.disconnect(); |
|||
this.observer = new MutationObserver(() => lockStyle()); |
|||
this.observer.observe(g6ToolbarElement, {attributes: true, attributeFilter: ['style']}); |
|||
|
|||
// 执行初始锁死 |
|||
lockStyle(); |
|||
|
|||
// 4. 【修复报错】阻止冒泡,不使用 capture |
|||
const stopEvent = (e) => e.stopPropagation(); |
|||
['mousedown', 'click', 'dblclick', 'contextmenu', 'wheel'].forEach(type => { |
|||
g6ToolbarElement.addEventListener(type, stopEvent); |
|||
}); |
|||
}, |
|||
|
|||
handleToolbarAction(id) { |
|||
if (!this.graph) return; |
|||
const historyPlugin = this.graph.getPluginInstance('history'); |
|||
switch (id) { |
|||
case 'zoom-in': |
|||
this.graph.zoomBy(1.2); |
|||
break; |
|||
case 'zoom-out': |
|||
this.graph.zoomBy(0.8); |
|||
break; |
|||
case 'undo': |
|||
if (historyPlugin?.canUndo()) historyPlugin.undo(); |
|||
break; |
|||
case 'redo': |
|||
if (historyPlugin?.canRedo()) historyPlugin.redo(); |
|||
break; |
|||
case 'auto-fit': |
|||
this.graph.fitView(); |
|||
break; |
|||
case 'reset': |
|||
break; |
|||
case 'export': |
|||
this.handleExportClick(); |
|||
break; |
|||
} |
|||
}, |
|||
|
|||
async handleExportClick() { |
|||
ElMessageBox.confirm('请选择您要导出的图片格式:', '导出图谱', { |
|||
confirmButtonText: '导出为 PNG', |
|||
cancelButtonText: '导出为 SVG', |
|||
distinguishCancelAndClose: true, |
|||
type: 'info', |
|||
draggable: true, |
|||
}).then(async () => { |
|||
try { |
|||
const dataURL = await this.graph.toDataURL({type: 'image/png', backgroundColor: '#ffffff'}); |
|||
const link = document.createElement('a'); |
|||
link.href = dataURL; |
|||
link.download = `图谱_${Date.now()}.png`; |
|||
link.click(); |
|||
ElMessage.success('PNG 导出成功'); |
|||
} catch (e) { |
|||
ElMessage.error('PNG 导出失败'); |
|||
} |
|||
}).catch((action) => { |
|||
if (action === 'cancel') this.exportToSVGManual(); |
|||
}); |
|||
}, |
|||
|
|||
exportToSVGManual() { |
|||
const graph = this.graph; |
|||
if (!graph) return; |
|||
const { nodes, edges } = graph.getData(); |
|||
const bBox = graph.getCanvas().getRoot().getRenderBounds(); |
|||
const padding = 60; |
|||
const minX = bBox.min[0] - padding; |
|||
const minY = bBox.min[1] - padding; |
|||
const maxX = bBox.max[0] + padding; |
|||
const maxY = bBox.max[1] + padding; |
|||
const vWidth = maxX - minX; |
|||
const vHeight = maxY - minY; |
|||
|
|||
// 1. 定义与问答页面一致的颜色映射(用于兜底和逻辑计算) |
|||
const colorMap = { |
|||
'Disease': { fill: '#EF4444', stroke: '#B91C1C', edge: '#EF4444' }, |
|||
'Drug': { fill: '#91cc75', stroke: '#047857', edge: '#91cc75' }, |
|||
'Check': { fill: '#336eee', stroke: '#1D4ED8', edge: '#336eee' }, |
|||
'Symptom': { fill: '#fac858', stroke: '#B45309', edge: '#fac858' }, |
|||
'Other': { fill: '#59d1d4', stroke: '#40999b', edge: '#59d1d4' } |
|||
}; |
|||
|
|||
let svgContent = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="${vWidth}" height="${vHeight}" viewBox="${minX} ${minY} ${vWidth} ${vHeight}">`; |
|||
svgContent += `<rect x="${minX}" y="${minY}" width="${vWidth}" height="${vHeight}" fill="#ffffff" />`; |
|||
|
|||
// 2. 渲染边 |
|||
edges.forEach(edge => { |
|||
const s = nodes.find(n => n.id === edge.source); |
|||
const t = nodes.find(n => n.id === edge.target); |
|||
if (!s || !t) return; |
|||
|
|||
// 获取坐标 (从 style 中取,G6 会把布局后的坐标存这) |
|||
const x1 = s.style?.x || 0; |
|||
const y1 = s.style?.y || 0; |
|||
const x2 = t.style?.x || 0; |
|||
const y2 = t.style?.y || 0; |
|||
|
|||
// --- 核心修正:根据起点节点的类型来确定边的颜色 --- |
|||
// 这对应了你问答页面里 edge: { style: { stroke: (d) => ... } } 的逻辑 |
|||
const sourceType = s.data?.label || 'Other'; // 获取起点类型 (Disease/Drug等) |
|||
const strokeColor = colorMap[sourceType]?.edge || '#cccccc'; |
|||
|
|||
// 绘制线条 (增加 stroke-width 确保可见) |
|||
svgContent += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${strokeColor}" stroke-width="2" opacity="0.4" />`; |
|||
|
|||
// 渲染边文字 |
|||
const labelText = edge.data?.relationship?.properties?.label || edge.data?.label || ""; |
|||
if (labelText) { |
|||
const mx = (x1 + x2) / 2; |
|||
const my = (y1 + y2) / 2; |
|||
svgContent += `<text x="${mx}" y="${my - 4}" fill="#666666" font-size="10" font-family="Microsoft YaHei" text-anchor="middle">${labelText}</text>`; |
|||
} |
|||
}); |
|||
|
|||
// 3. 渲染节点 |
|||
nodes.forEach(node => { |
|||
const type = node.data?.label || 'Other'; |
|||
const colors = colorMap[type] || colorMap['Other']; |
|||
const radius = (node.style?.size || 50) / 2; |
|||
const x = node.style?.x || 0; |
|||
const y = node.style?.y || 0; |
|||
|
|||
svgContent += `<circle cx="${x}" cy="${y}" r="${radius}" fill="${colors.fill}" stroke="${colors.stroke}" stroke-width="1.5" />`; |
|||
|
|||
if (node.data?.name) { |
|||
svgContent += `<text x="${x}" y="${y}" fill="#ffffff" font-size="12" font-family="Microsoft YaHei" text-anchor="middle" dominant-baseline="middle">${node.data.name}</text>`; |
|||
} |
|||
}); |
|||
|
|||
svgContent += `</svg>`; |
|||
|
|||
// 保存文件逻辑保持不变... |
|||
try { |
|||
const blob = new Blob([svgContent], {type: 'image/svg+xml;charset=utf-8'}); |
|||
const url = URL.createObjectURL(blob); |
|||
const link = document.createElement('a'); |
|||
link.href = url; |
|||
link.download = `矢量图谱_${Date.now()}.svg`; |
|||
link.click(); |
|||
URL.revokeObjectURL(url); |
|||
this.$message.success('全量矢量图导出成功'); |
|||
} catch (err) { |
|||
this.$message.error('导出失败'); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.custom-graph-toolbar { |
|||
display: inline-flex !important; |
|||
width: auto !important; |
|||
min-width: 100px; |
|||
background: #ffffff; |
|||
border: 1px solid #e2e8f0; |
|||
border-radius: 8px; |
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); |
|||
align-items: center; |
|||
height: 42px !important; |
|||
padding: 0 4px; |
|||
z-index: 1001; |
|||
pointer-events: auto; |
|||
} |
|||
|
|||
:deep(.g6-toolbar) { |
|||
display: inline-flex !important; |
|||
height: 35px !important; |
|||
width: auto !important; |
|||
align-items: center !important; |
|||
background: transparent !important; |
|||
} |
|||
|
|||
:deep(.g6-toolbar-item) { |
|||
width: 20px !important; |
|||
height: 35px !important; |
|||
font-size: 15px !important; |
|||
margin: 0 2px !important; |
|||
display: flex !important; |
|||
align-items: center !important; |
|||
justify-content: center !important; |
|||
border-radius: 6px; |
|||
cursor: pointer; |
|||
color: #838383 !important; |
|||
transition: all 0.2s; |
|||
} |
|||
|
|||
:deep(.g6-toolbar-item:hover) { |
|||
background: #f1f5f9 !important; |
|||
color: #1e293b !important; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue