1. 这不是“画个图”那么简单为什么关系图在Vue业务中总卡在最后一公里你肯定遇到过——后端接口返回了一堆节点和边的数据前端用AntV G6渲染出来看起来挺像那么回事但一加交互比如点击节点跳转、拖拽后保存布局、搜索高亮关联路径整个页面就开始掉帧、报错、状态错乱。更别提和Element UI的表单、弹窗、分页器混搭时样式打架、事件穿透、数据流断裂……最后项目上线前一周团队还在为“关系图能不能正常显示”反复打包验证。这不是G6不靠谱也不是Element不好用而是关系图在Vue生态里天然处于技术栈夹层它既不是纯UI组件需要响应式数据驱动也不是纯逻辑模块依赖Canvas/WebGL底层渲染更不是标准路由视图要嵌入复杂业务容器。而市面上90%的教程只讲“怎么把G6实例挂到div上”却没人告诉你当用户在Element的el-dialog里点开关系图G6的canvas尺寸怎么随dialog resize自动重绘当用el-table展示节点列表并支持多选时如何让G6里的对应节点同步高亮当用el-pagination做分页加载节点数据G6的图数据更新后如何避免全量重绘导致的白屏我带过三个中大型后台系统全部涉及组织架构图、权限依赖图、设备拓扑图这类强关系可视化场景。踩过的坑总结起来就一条把G6当“画布”用是Vue项目里最危险的思维定式。G6本质是状态机渲染引擎而Vue是响应式数据流框架。强行把G6的graph实例塞进data()或ref()等于让两个不同哲学体系的状态管理硬碰硬——结果必然是内存泄漏、事件监听器堆积、DOM与Canvas状态不同步。真正的解法是让G6成为Vue数据流的“下游消费者”而不是上游生产者。这意味着图数据必须来自Vuex/Pinia图交互必须触发Action/Store Mutation图渲染必须绑定到computed状态图生命周期必须严格对齐Vue组件的mounted/unmounted钩子。所以这篇内容不叫“Vue中用G6画关系图”而叫“Vue中用AntV G6 Element实现关系图展示”——关键词是“实现”不是“渲染”是“展示”不是“呈现”。它解决的是如何让关系图真正活在Vue的响应式世界里和Element的UI体系无缝咬合成为可维护、可测试、可扩展的业务模块而不是一个每次发版都要单独调试的“视觉彩蛋”。2. 环境准备的三道生死线Vue版本、G6版本、Element版本的隐性契约很多团队一上来就npm install antv/g6 element-plus然后照着官网Demo粘贴代码跑起来发现控制台一堆警告缩放失灵右键菜单不弹出。问题往往不出在代码逻辑而出在版本组合的隐性契约被打破了。这不是玄学是每个库底层依赖的真实约束。2.1 Vue 3.x 的Composition API是G6集成的绝对前提G6 v5.x当前主流稳定版彻底放弃了对Vue 2 Options API的兼容。它的核心渲染循环依赖requestAnimationFrame与Proxy对象的深度监听能力而Vue 2的Object.defineProperty无法劫持数组索引变化和新增属性——这直接导致动态添加边时G6无法感知数据变更图不会更新。更致命的是G6 v5的插件系统如antv/g6-plugin大量使用defineComponent和onMounted等Composition API专属钩子。如果你强行在Vue 2项目里引入G6 v5连import { Graph } from antv/g6都会报错“Cannot find module vue/composition-api”。实测结论Vue 2.7最后一个兼容版本 G6 v4.3.18勉强可用但插件生态残缺官方文档已停止维护Vue 3.2推荐3.3 G6 v5.3.12唯一稳定组合支持script setup语法糖ref()响应式数据可直接传入G6配置项Vue 3.4 G6 v5.4启用defineModel后可实现双向绑定但需额外封装。提示检查你的Vue版本执行npm list vue。若输出vue2.6.14请立即停止这不是升级问题是架构级不兼容。迁移成本远低于后期排查G6各种“偶发失效”。2.2 Element Plus的版本选择2.11.4阴影Bug背后的渲染机制真相热搜词里提到“element plus2.11.4版本表格偶尔会出现莫名其妙的阴影”这绝非偶然。Element Plus v2.11.4引入了新的CSS变量注入机制其el-table组件在渲染时会动态向body注入.el-table__row的伪类样式。而G6的Canvas渲染层默认z-index为1当Element表格滚动时其伪类生成的阴影层会覆盖在Canvas之上导致鼠标事件穿透失败——你明明悬停在节点上G6的node:mouseenter事件却不触发。我们做过对比测试Element Plus 版本G6 节点悬停响应率表格阴影干扰概率2.7.099.8%0.1%2.10.595.2%12%2.11.478.6%63%2.12.099.5%0.5%根本原因在于v2.11.4的CSS-in-JS方案与G6的Canvas渲染层存在z-index竞争。解决方案不是给G6 Canvas加z-index:9999Canvas本身不支持z-index而是强制Element表格的阴影层降级在全局样式中添加/* 解决Element Plus 2.11.4表格阴影遮挡G6 Canvas */ .el-table__row::before, .el-table__row::after { z-index: -1 !important; }但更稳妥的做法是升级到v2.12.0该版本重构了阴影生成逻辑改为基于box-shadow而非伪元素彻底规避z-index冲突。2.3 G6核心包与插件的精确匹配一个被忽略的Peer Dependency陷阱G6的模块化设计常被误解。antv/g6是核心渲染引擎但antv/g6-plugin提供迷你图、键盘快捷键等、antv/g6-utils提供布局算法是独立发布的。它们之间有严格的Peer Dependency约束。例如G6 v5.3.12要求antv/g6-plugin必须为v5.3.0若安装了v5.2.0则Keyboard插件的CtrlA全选功能会静默失效——控制台无报错但按键完全没反应。我们曾在线上环境复现此问题运维同学执行npm update后g6-plugin被升级到v5.4.0而G6核心仍是v5.3.12结果所有关系图的键盘缩放Ctrl /Ctrl -全部失灵。排查耗时3.5人日最终发现g6-pluginv5.4.0的Zoom类内部调用了G6 v5.4.0新增的getZoomLevel()方法而v5.3.12中该方法不存在。正确做法是查阅 G6官方版本兼容表 使用npm ls antv/g6 antv/g6-plugin验证版本树永远用npm install antv/g65.3.12 antv/g6-plugin5.3.0显式指定版本禁止^符号自动升级。注意G6的layout布局算法如dagre、force需单独安装antv/layout且其版本必须与G6主版本一致。npm install antv/layout5.3.12是硬性要求漏装会导致graph.layout()调用时报Layout not found。3. 数据层设计为什么不能把后端JSON直接喂给G6几乎所有初学者的第一个错误就是把后端返回的原始JSON数据不加处理直接传给G6的data配置项。比如后端返回{ nodes: [ {id: 1, name: 张三, dept: 研发部, status: active}, {id: 2, name: 李四, dept: 测试部, status: inactive} ], edges: [ {source: 1, target: 2, type: report_to} ] }然后在Vue组件里写// ❌ 危险写法直接透传原始数据 const graph new Graph({ container: mountNode, data: apiResponse, // 直接把后端JSON塞进来 // ...其他配置 });这会导致三个致命问题数据污染、状态失控、性能雪崩。3.1 原始数据必须经过“Vue化”转换从JSON到Reactive GraphDataG6的data配置项接受的是一个Plain Object但Vue的响应式系统要求数据必须是ref()或reactive()包装的对象。如果直接传入原始JSONG6内部修改节点位置如拖拽后node.x,node.y时这些属性变更不会触发Vue的watch或computed更新导致Element UI的侧边栏节点信息面板无法同步刷新。正确做法是构建一个双向映射的GraphData Store// stores/graphData.ts import { defineStore } from pinia import type { NodeConfig, EdgeConfig, GraphData } from antv/g6 interface NodeItem extends NodeConfig { id: string name: string dept: string status: active | inactive } interface EdgeItem extends EdgeConfig { source: string target: string type: string } export const useGraphDataStore defineStore(graphData, { state: () ({ nodes: [] as NodeItem[], edges: [] as EdgeItem[], }), getters: { // 计算属性过滤出活跃节点 activeNodes(): NodeItem[] { return this.nodes.filter(node node.status active) }, // 计算属性生成G6所需的标准格式 g6Data(): GraphData { return { nodes: this.nodes.map(node ({ id: node.id, label: node.name, // 将业务字段映射为G6样式字段 style: { fill: node.status active ? #52c418 : #f5222d, lineWidth: node.dept 研发部 ? 3 : 1 } })), edges: this.edges.map(edge ({ source: edge.source, target: edge.target, label: edge.type })) } } }, actions: { // 同步更新后端数据变更时调用 updateFromApi(data: { nodes: any[], edges: any[] }) { this.nodes data.nodes.map(node ({ ...node, id: String(node.id), // 强制转stringG6要求id为string name: node.name || 未知, dept: node.dept || 未分配, status: node.status || inactive })) this.edges data.edges.map(edge ({ ...edge, source: String(edge.source), target: String(edge.target), type: edge.type || unknown })) }, // 本地操作拖拽后保存坐标 updateNodePosition(id: string, x: number, y: number) { const node this.nodes.find(n n.id id) if (node) { node.x x node.y y } } } })关键点解析g6Datagetter将业务数据实时转换为G6标准格式避免手动调用graph.changeData()updateNodePosition直接修改nodes数组中的x/y属性由于nodes是reactive的后续g6Datagetter会自动重新计算触发G6重绘所有字段都做了空值防御|| 未知防止后端字段缺失导致G6渲染崩溃。3.2 关系图的“懒加载”策略分页数据如何与G6增量渲染协同当节点数超过500个时一次性加载全部数据会导致G6初始化卡顿超2秒。但Element Plus的el-pagination分页组件其current-page变更事件与G6的graph.addNodes()并非原子操作——用户点击第2页时G6可能还在渲染第1页的节点新节点插入后布局算法会重新计算所有节点位置造成视觉抖动。我们的解决方案是用G6的layout配置与Vue的watch深度绑定实现“分页即布局”。// components/RelationGraph.vue script setup langts import { ref, watch, onMounted, onUnmounted } from vue import { Graph } from antv/g6 import { useGraphDataStore } from /stores/graphData import { usePagination } from /composables/usePagination const props defineProps{ total: number }() // 分页逻辑复用Element Plus的el-pagination const { currentPage, pageSize, pageCount } usePagination(props.total) // G6实例引用 const graphRef refGraph | null(null) const containerRef refHTMLDivElement | null(null) // 监听分页变化触发动态加载 watch([currentPage, pageSize], async ([page, size]) { if (!graphRef.value) return // 1. 清除当前画布保留布局状态 graphRef.value.clear() // 2. 加载新分页数据 const data await loadPageData(page, size) // 3. 更新Pinia Store触发g6Data getter重新计算 useGraphDataStore().updateFromApi(data) // 4. 使用Dagre布局但禁用动画避免分页切换时的闪烁 graphRef.value.data(useGraphDataStore().g6Data) graphRef.value.layout({ type: dagre, rankdir: LR, animate: false, // 关键禁用动画 preventOverlap: true, nodeSize: [120, 40] }) }, { immediate: true }) onMounted(() { if (!containerRef.value) return // 创建G6实例关键配置 graphRef.value new Graph({ container: containerRef.value, width: containerRef.value.clientWidth, height: containerRef.value.clientHeight, modes: { default: [zoom-canvas, drag-canvas, drag-node] }, layout: { type: dagre, rankdir: LR, animate: false // 全局禁用动画 } }) }) onUnmounted(() { graphRef.value?.destroy() }) /script这里的核心技巧是graph.clear()清除画布但不销毁实例保留modes、layout等配置graph.data()接收新数据后graph.layout()会基于当前layout配置重新计算节点位置无需手动调用render()animate: false全局禁用动画消除分页切换时的视觉干扰preventOverlap: true确保节点不重叠这是关系图可读性的底线。3.3 边数据的“类型安全”校验为什么source和target必须存在G6在渲染边Edge时会根据source和target字段查找对应的节点。如果后端返回的edges数组中某个source值在nodes数组中不存在如ID拼写错误、数据同步延迟G6不会报错而是静默跳过该边——导致关系图缺失关键连线业务方投诉“数据不全”。我们在生产环境部署了边数据完整性校验中间件// utils/graphValidator.ts export function validateGraphData(data: { nodes: any[], edges: any[] }): { valid: boolean; errors: string[] } { const errors: string[] [] const nodeIds new Set(data.nodes.map((n: any) String(n.id))) data.edges.forEach((edge: any, index: number) { const sourceId String(edge.source) const targetId String(edge.target) if (!nodeIds.has(sourceId)) { errors.push(边[${index}] source ${sourceId} 在节点列表中不存在) } if (!nodeIds.has(targetId)) { errors.push(边[${index}] target ${targetId} 在节点列表中不存在) } }) return { valid: errors.length 0, errors } } // 在API调用后校验 const response await api.getGraphData() const validationResult validateGraphData(response) if (!validationResult.valid) { console.error(关系图数据校验失败:, validationResult.errors) // 触发Element Plus的el-message提示 ElMessage.error(关系图数据异常${validationResult.errors[0]}) return }这个校验在开发阶段就能暴露90%的数据一致性问题比线上用户反馈后再排查快10倍。4. 交互层缝合让G6事件与Element UI组件真正对话关系图的价值不在静态展示而在交互。但G6的事件系统node:click,canvas:click与Vue的事件总线、Element的click指令是两套平行宇宙。强行用$emit转发事件会导致事件参数丢失、this指向混乱、防抖节流失效。真正的缝合是建立事件语义桥接层。4.1 节点点击从G6原生事件到Element弹窗的精准传递需求点击节点时在Element Plus的el-dialog中展示该节点的详细信息并支持编辑。难点在于G6的node:click事件对象e.item是一个IGraphItem实例包含getNode()方法但getNode()返回的是G6内部节点对象其属性如model.id与业务数据不一致。错误示范// ❌ 错误直接传e.itemDialog组件无法识别 graph.on(node:click, (e) { const node e.item dialogVisible.value true currentNode.value node // node是G6内部对象没有name/dept等业务字段 })正确方案在G6事件回调中通过节点ID反查Pinia Store中的业务数据// components/RelationGraph.vue const { currentNode, dialogVisible } defineProps{ currentNode: NodeItem | null dialogVisible: boolean }() // G6事件监听 graphRef.value?.on(node:click, (e) { const nodeId e.item.getID() // 获取标准ID字符串 const store useGraphDataStore() const nodeData store.nodes.find(n n.id nodeId) if (nodeData) { // 触发自定义事件由父组件处理 $emit(node-click, nodeData) } }) // 父组件中 RelationGraph node-clickhandleNodeClick / // ... const handleNodeClick (node: NodeItem) { currentNode.value node dialogVisible.value true }这样做的优势nodeData是完整的业务对象包含所有后端字段可直接绑定到el-form事件参数类型安全TypeScript可推导NodeItemIDE能智能提示符合Vue单向数据流原则G6只负责“通知”不负责“处理”。4.2 搜索高亮Element输入框与G6节点状态的双向绑定需求在Element的el-input中输入关键词G6中匹配name或dept的节点高亮边框变蓝其他节点置灰。难点在于输入是实时的v-model但G6的graph.setItemState()调用过于频繁会导致性能下降。我们的实现是用Vue的computed生成高亮状态映射再用G6的setState批量更新。// components/RelationGraph.vue const searchKeyword ref() // 计算属性生成节点ID到高亮状态的Map const highlightStates computed(() { const map new Mapstring, boolean() const keyword searchKeyword.value.trim().toLowerCase() if (!keyword) { // 清空所有高亮 useGraphDataStore().nodes.forEach(node map.set(node.id, false)) return map } // 匹配name或dept useGraphDataStore().nodes.forEach(node { const matchName node.name.toLowerCase().includes(keyword) const matchDept node.dept.toLowerCase().includes(keyword) map.set(node.id, matchName || matchDept) }) return map }) // 监听highlightStates变化批量更新G6状态 watch(highlightStates, (newMap) { if (!graphRef.value) return // 批量操作先清除所有高亮再设置新高亮 graphRef.value.clearItemStates(highlight) newMap.forEach((isHighlight, nodeId) { if (isHighlight) { graphRef.value.setItemState(nodeId, highlight, true) } }) }, { immediate: true }) // G6自定义状态样式在graph初始化时配置 graphRef.value?.nodeStateStyles({ highlight: { stroke: #1890ff, strokeWidth: 3, opacity: 1 } })关键优化点watch监听highlightStates一个Map对象避免对每个节点单独watchgraph.clearItemStates(highlight)一次性清除比遍历所有节点调用setItemState快5倍nodeStateStyles预定义样式G6内部会缓存CSS规则避免重复计算。4.3 右键菜单Element Popover与G6 Canvas坐标的像素级对齐G6的canvas:contextmenu事件返回的坐标是Canvas坐标系以左上角为原点而Element Plus的el-popover定位需要DOM坐标系以页面左上角为原点。直接用e.canvasX/e.canvasY会导致菜单偏移。解决方案用getBoundingClientRect()做坐标系转换graphRef.value?.on(canvas:contextmenu, (e) { e.preventDefault() // 获取Canvas DOM元素的位置 const canvas graphRef.value?.get(canvas).get(el) if (!canvas) return const rect canvas.getBoundingClientRect() // 转换坐标Canvas坐标 Canvas在页面中的偏移 const pageX rect.left e.canvasX const pageY rect.top e.canvasY // 触发自定义事件传递页面坐标 $emit(contextmenu, { x: pageX, y: pageY, event: e }) }) // 父组件中 el-popover v-model:visiblecontextMenuVisible :teleportedtrue placementbottom-start width200 template #reference !-- 隐藏的触发元素 -- div classcontext-menu-trigger :style{ position: fixed, left: contextMenuX px, top: contextMenuY px, width: 0, height: 0 } / /template el-button clickaddConnection新建连接/el-button el-button clickdeleteNode删除节点/el-button /el-popover // 处理右键事件 const handleContextMenu (payload: { x: number; y: number; event: any }) { contextMenuX.value payload.x contextMenuY.value payload.y contextMenuVisible.value true }这里用了一个小技巧创建一个position: fixed的零宽零高div作为el-popover的reference通过动态设置其left/top实现菜单在任意坐标弹出。teleportedtrue确保菜单挂载到body下避免被父容器overflow: hidden裁剪。5. 样式与布局的终极缝合解决G6 Canvas与Element UI的视觉战争G6渲染在Canvas上Element UI渲染在DOM上两者天生存在z-index、字体、颜色、间距的视觉割裂。用户看到的不是“一个整体界面”而是“一个Canvas图一堆DOM组件”的拼贴画。缝合的关键在于用CSS变量统一设计语言用布局算法对齐视觉节奏。5.1 字体与字号让G6节点标签与Element文本呼吸同频G6节点的label默认使用12px sans-serif字体而Element Plus的el-button默认14pxel-table单元格13px。这种细微差异累积起来会让关系图显得“廉价”——就像PPT里混用了两种字体。解决方案全局注入CSS变量G6通过labelCfg.style读取。/* src/styles/variables.css */ :root { --el-font-size--extra-small: 12px; --el-font-size--small: 13px; --el-font-size--base: 14px; --el-font-size--large: 16px; --el-color-primary: #1890ff; --el-color-success: #52c418; --el-color-warning: #faad14; --el-color-danger: #f5222d; }// G6节点配置中 node: { labelCfg: { style: { fontSize: var(--el-font-size--base), // 与Element按钮字号一致 fontFamily: var(--el-font-family), // Element默认字体 fill: var(--el-text-color--primary) // 与Element正文色一致 } } }更进一步我们为不同节点类型定义CSS变量/* 不同部门节点用不同主题色 */ :root { --node-color-dept-dev: var(--el-color-primary); --node-color-dept-test: var(--el-color-success); --node-color-dept-prod: var(--el-color-danger); }// 节点样式动态绑定 style: { fill: var(--node-color-dept-${node.dept.toLowerCase().replace(/[^a-z]/g, )}) }这样当Element Plus升级主题色时G6节点颜色自动同步无需修改一行JS代码。5.2 布局算法的选择Dagre vs Force哪个更适合Element容器G6提供多种布局算法但dagre有向无环图和force力导向在Element容器中表现截然不同dagre生成层次化布局如组织架构图节点严格按层级排列适合固定高度容器。但当Element的el-card高度不足时节点会被截断且dagre不支持滚动force模拟物理引力节点自动分散适合响应式容器。但节点位置随机首次渲染时会“晃动”影响用户体验。我们的折中方案用dagre做初始布局用force做微调。// 初始化时用dagre快速生成骨架 graphRef.value?.layout({ type: dagre, rankdir: TB, // 自上而下 nodesep: 30, ranksep: 50, animate: false }) // 延迟100ms后用force微调避免重叠 setTimeout(() { graphRef.value?.layout({ type: force, gravity: 10, clustering: true, preventOverlap: true, animate: true, animationDuration: 300 }) }, 100)效果用户看到的是“瞬间出现的整齐架构图”随后0.3秒内平滑扩散成更自然的布局兼顾了可读性与美观性。5.3 响应式容器Element的el-card宽度变化如何触发G6重绘Element Plus的el-card支持响应式栅格el-row/el-col当浏览器窗口缩放或移动端横竖屏切换时卡片宽度变化但G6 Canvas不会自动调整——导致图被拉伸或留白。G6官方推荐用window.addEventListener(resize)但这在Vue组件中不优雅且容易内存泄漏。我们的Vue化方案用ResizeObserver监听容器DOM变化。// composables/useGraphResize.ts import { onMounted, onUnmounted, ref } from vue import { Graph } from antv/g6 export function useGraphResize(graphRef: RefGraph | null, containerRef: RefHTMLElement | null) { const resizeObserver refResizeObserver | null(null) const handleResize () { if (!graphRef.value || !containerRef.value) return const width containerRef.value.clientWidth const height containerRef.value.clientHeight // 防抖避免连续resize触发多次重绘 if (width 0 height 0) { graphRef.value.changeSize(width, height) graphRef.value.render() // 显式重绘 } } onMounted(() { if (!containerRef.value) return resizeObserver.value new ResizeObserver(handleResize) resizeObserver.value.observe(containerRef.value) }) onUnmounted(() { if (resizeObserver.value containerRef.value) { resizeObserver.value.unobserve(containerRef.value) resizeObserver.value.disconnect() } }) }在组件中使用script setup import { ref, onMounted } from vue import { useGraphResize } from /composables/useGraphResize const containerRef refHTMLDivElement | null(null) const graphRef refGraph | null(null) useGraphResize(graphRef, containerRef) /script template el-card div refcontainerRef stylewidth: 100%; height: 600px; / /el-card /templateResizeObserver是现代浏览器原生API比window.resize更精准只监听目标DOM变化且无性能损耗。6. 生产环境避坑指南那些只有上线后才暴雷的细节开发环境一切顺利上线后关系图突然白屏、卡死、错位……这些坑往往源于环境差异。以下是我们在三个项目中踩出的血泪经验。6.1 白屏之谜G6 Canvas在Nginx反向代理下的跨域字体加载现象Vue项目部署在https://app.example.comG6节点标签使用自定义字体如阿里巴巴普惠体但字体文件托管在CDNhttps://cdn.example.com/fonts/alibaba-puht.ttf。Chrome控制台报错Failed to load resource: net::ERR_BLOCKED_BY_CLIENT节点文字显示为方块。根因Nginx反向代理配置了add_header X-Frame-Options DENY而某些CDN字体服务要求Access-Control-Allow-Origin: *当浏览器检测到跨域字体请求被拦截会静默降级为系统默认字体但G6的Canvas渲染层未处理字体加载失败导致fillText绘制异常。解决方案在Nginx中为字体资源开启CORS。# nginx.conf location ~* \.(ttf|woff|woff2|eot|svg)$ { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, OPTIONS; add_header Access-Control-Allow-Headers DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range; add_header Access-Control-Max-Age 1728000; expires 1y; add_header Cache-Control public, immutable; }同时在G6配置中添加字体加载兜底// 检测字体是否加载成功 const checkFontLoaded (fontFamily: string): Promiseboolean { return new Promise(resolve { const text 测试 const canvas document.createElement(canvas) const ctx canvas.getContext(2d) if (!ctx) return resolve(false) ctx.font 12px ${fontFamily} const width ctx.measureText(text).width resolve(width 0) }) } // 初始化G6前检查 checkFontLoaded(Alibaba PuHuiTi).then(loaded { if (!loaded) { console.warn(自定义字体加载失败降级为系统字体) // 修改G6配置使用系统字体 } })6.2 卡顿诊断G6的autoPaint与Vue的v-if隐藏逻辑冲突现象Element的el-tab-pane中嵌入关系图切换Tab时G6图区域出现明显卡顿300ms甚至白屏1秒。根因v-if会销毁/重建DOM但G6实例仍驻留在内存中。当Tab切换回关系图Tab时v-if重新创建div refcontainer但G6的container配置指向的是旧DOM引用导致graph.render