diff --git a/src/pages/sjtb/configTask/dataTable/antvX6.ts b/src/pages/sjtb/configTask/dataTable/antvX6.ts index 2fa42418..f631260f 100644 --- a/src/pages/sjtb/configTask/dataTable/antvX6.ts +++ b/src/pages/sjtb/configTask/dataTable/antvX6.ts @@ -1,9 +1,9 @@ -import type { Edge, Node, NodeConfig } from '@antv/x6'; -import { Graph } from '@antv/x6'; -import { v4 as uuidv4 } from 'uuid'; +import type { Edge, Node, NodeConfig } from "@antv/x6"; +import { Graph } from "@antv/x6"; +import { v4 as uuidv4 } from "uuid"; // 层级类型定义 -export type NodeLevel = 'level2' | 'level3' | 'level4'; +export type NodeLevel = "level2" | "level3" | "level4"; // 目标灌库数据的类型定义 interface ImportDataItem { @@ -50,14 +50,14 @@ export class X6Graph { private readonly nodeStyle = { width: 100, height: 40, - fill: '#0a4a5c', // 深色背景(示例中的深青色) - stroke: '#0a4a5c', // 边框与背景同色,视觉上隐藏边框 + fill: "#0a4a5c", // 深色背景(示例中的深青色) + stroke: "#0a4a5c", // 边框与背景同色,视觉上隐藏边框 strokeWidth: 2, radius: 4, label: { - fill: '#93CDD5', // 白色文字 + fill: "#93CDD5", // 白色文字 fontSize: 14, - fontWeight: 'bold', // 可选:文字加粗 + fontWeight: "bold", // 可选:文字加粗 }, }; @@ -69,16 +69,16 @@ export class X6Graph { // 层级X坐标映射 private readonly levelXMap = new Map([ - ['level2', 300], - ['level3', 550], - ['level4', 1000], + ["level2", 300], + ["level3", 550], + ["level4", 1000], ]); // 层级节点数量记录 private readonly levelCountMap = new Map([ - ['level2', 0], - ['level3', 0], - ['level4', 0], + ["level2", 0], + ["level3", 0], + ["level4", 0], ]); constructor(containerId: string) { @@ -87,7 +87,12 @@ export class X6Graph { } private createMenuElement() { - this.menuElement = document.createElement('div'); + this.menuElement = document.getElementById("ant-x6-menu4"); + if(this.menuElement){ + this.menuElement.remove(); + } + this.menuElement = document.createElement("div"); + this.menuElement.id = "ant-x6-menu4"; this.menuElement.style.cssText = ` position: absolute; width: 120px; @@ -101,18 +106,22 @@ export class X6Graph { font-family: sans-serif; `; - // 1. 配置同步字段:新增“配置”菜单项 - const configItem = document.createElement('div'); - configItem.id = 'ant-x6-menu-1'; - configItem.style.cssText = ` + // 3. 配置 + let pzItem = document.getElementById("ant-x6-menu"); + if (pzItem) { + pzItem.remove(); + } + pzItem = document.createElement("div"); + pzItem.id = "ant-x6-menu"; + pzItem.style.cssText = ` padding: 6px 12px; cursor: pointer; font-size: 14px; border-bottom: 1px solid #f0f0f0; /* 分隔线,区分两个选项 */ `; - configItem.textContent = '配置同步字段'; - configItem.addEventListener('click', () => { - const nodeId = this.menuElement?.getAttribute('data-node-id'); + pzItem.textContent = "配置"; + pzItem.addEventListener("click", () => { + const nodeId = this.menuElement?.getAttribute("data-node-id"); if (nodeId) { const graph = this.getGraph(); const targetNode = graph.getCellById(nodeId) as Node; @@ -122,35 +131,41 @@ export class X6Graph { // 2. 高亮样式(保持原有) targetNode.attr({ body: { - stroke: 'rgb(12,129,123)', + stroke: "rgb(12,129,123)", strokeWidth: 2, - strokeDasharray: '5, 5', + strokeDasharray: "5, 5", }, }); // ======================== 新增:获取所有父级信息 ======================== // 1. 定义追溯父级的方法 - const getAllParents = (targetNodeId: string): Array<{ id: string; label: string; level?: NodeLevel }> => { - const parents: Array<{ id: string; label: string; level?: NodeLevel }> = []; + const getAllParents = ( + targetNodeId: string, + ): Array<{ id: string; label: string; level?: NodeLevel }> => { + const parents: Array<{ + id: string; + label: string; + level?: NodeLevel; + }> = []; let currentNodeId = targetNodeId; // 循环追溯:直到找不到父节点(根节点) while (true) { // 找到指向当前节点的连线(来源即父节点) - const parentEdge = graph.getEdges().find(edge => edge.getTargetCellId() === currentNodeId); - if (!parentEdge) - break; // 无父节点,终止循环 + const parentEdge = graph + .getEdges() + .find((edge) => edge.getTargetCellId() === currentNodeId); + if (!parentEdge) break; // 无父节点,终止循环 // 获取父节点ID和父节点实例 const parentNodeId = parentEdge.getSourceCellId(); const parentNode = graph.getCellById(parentNodeId) as Node; - if (!parentNode) - break; + if (!parentNode) break; // 收集父节点信息(ID、标签、层级) parents.push({ id: parentNodeId, - label: parentNode.attr('label/text') || '', // 节点显示的文字 + label: parentNode.attr("label/text") || "", // 节点显示的文字 level: parentNode.getData().level, // 父节点的层级 }); @@ -169,45 +184,155 @@ export class X6Graph { // 关键:添加当前节点信息(含label),拼接成“父级+当前节点”的完整链 const currentNodeInfo = { id: nodeId, - label: targetNode.attr('label/text') || '', // 从当前节点获取label + label: targetNode.attr("label/text") || "", // 从当前节点获取label level: nodeLevel, }; allParentsOrdered = [...allParentsOrdered, currentNodeInfo]; - console.log('目标节点(父级+自身)完整链:', allParentsOrdered); + console.log("目标节点(父级+自身)完整链:", allParentsOrdered); // ======================================================================== // 3. 事件总线传递:携带完整链信息 - EventBusTool.getEventBus().publish('antvX6Event', { - key: 'configNode', + EventBusTool.getEventBus().publish("antvX6Event", { + key: "configNode", id: nodeId, - label: targetNode.attr('label/text') || '', + label: targetNode.attr("label/text") || "", level: nodeLevel, allParents: allParentsOrdered, // 包含父级+当前节点(均有label) }); } this.hideMenu(); }); - // 配置项悬停效果 - configItem.addEventListener('mouseenter', () => { - configItem.style.background = '#f0f0f0'; + // 配置菜单悬停效果 + pzItem.addEventListener("mouseenter", () => { + pzItem.style.background = "#f0f0f0"; }); - configItem.addEventListener('mouseleave', () => { - configItem.style.background = 'white'; + pzItem.addEventListener("mouseleave", () => { + pzItem.style.background = "white"; + }); + + // 1. 配置同步字段:新增“配置”菜单项 + let configItem = document.getElementById("ant-x6-menu-1"); + if (configItem) { + configItem.remove(); + } + configItem = document.createElement("div"); + configItem.id = "ant-x6-menu-1"; + configItem.style.cssText = ` + padding: 6px 12px; + cursor: pointer; + font-size: 14px; + border-bottom: 1px solid #f0f0f0; /* 分隔线,区分两个选项 */ + `; + configItem.textContent = "配置同步字段"; + configItem.addEventListener("click", () => { + const nodeId = this.menuElement?.getAttribute("data-node-id"); + if (nodeId) { + const graph = this.getGraph(); + const targetNode = graph.getCellById(nodeId) as Node; + const nodeData = targetNode.getData(); + const nodeLevel = nodeData.level; + + // 2. 高亮样式(保持原有) + targetNode.attr({ + body: { + stroke: "rgb(12,129,123)", + strokeWidth: 2, + strokeDasharray: "5, 5", + }, + }); + + // ======================== 新增:获取所有父级信息 ======================== + // 1. 定义追溯父级的方法 + const getAllParents = ( + targetNodeId: string, + ): Array<{ id: string; label: string; level?: NodeLevel }> => { + const parents: Array<{ + id: string; + label: string; + level?: NodeLevel; + }> = []; + let currentNodeId = targetNodeId; + + // 循环追溯:直到找不到父节点(根节点) + while (true) { + // 找到指向当前节点的连线(来源即父节点) + const parentEdge = graph + .getEdges() + .find((edge) => edge.getTargetCellId() === currentNodeId); + if (!parentEdge) break; // 无父节点,终止循环 + + // 获取父节点ID和父节点实例 + const parentNodeId = parentEdge.getSourceCellId(); + const parentNode = graph.getCellById(parentNodeId) as Node; + if (!parentNode) break; + + // 收集父节点信息(ID、标签、层级) + parents.push({ + id: parentNodeId, + label: parentNode.attr("label/text") || "", // 节点显示的文字 + level: parentNode.getData().level, // 父节点的层级 + }); + + // 下一轮追溯:以父节点为当前节点,找它的父节点 + currentNodeId = parentNodeId; + } + + return parents; // 返回所有父级(顺序:直接父级 → 祖父级 → ... → 根节点父级) + }; + + // 2. 执行追溯,获取所有父级 + const allParentNodes = getAllParents(nodeId); + // 反转数组,让顺序变为“根节点父级 → 祖父级 → 直接父级” + let allParentsOrdered = allParentNodes.reverse(); + + // 关键:添加当前节点信息(含label),拼接成“父级+当前节点”的完整链 + const currentNodeInfo = { + id: nodeId, + label: targetNode.attr("label/text") || "", // 从当前节点获取label + level: nodeLevel, + }; + allParentsOrdered = [...allParentsOrdered, currentNodeInfo]; + + console.log("目标节点(父级+自身)完整链:", allParentsOrdered); + // ======================================================================== + + // 3. 事件总线传递:携带完整链信息 + EventBusTool.getEventBus().publish("antvX6Event", { + key: "configNode", + id: nodeId, + label: targetNode.attr("label/text") || "", + level: nodeLevel, + allParents: allParentsOrdered, // 包含父级+当前节点(均有label) + isTbsl:false, + }); + } + this.hideMenu(); + }); + // 配置项悬停效果 + configItem.addEventListener("mouseenter", () => { + configItem.style.background = "#f0f0f0"; + }); + configItem.addEventListener("mouseleave", () => { + configItem.style.background = "white"; }); // 2. 配置同步数量 - const syncNumItem = document.createElement('div'); - syncNumItem.id = 'ant-x6-menu-2'; + let syncNumItem = document.getElementById("ant-x6-menu-2"); + if (syncNumItem) { + syncNumItem.remove(); + } + syncNumItem = document.createElement("div"); + syncNumItem.id = "ant-x6-menu-2"; syncNumItem.style.cssText = ` padding: 6px 12px; cursor: pointer; font-size: 14px; border-bottom: 1px solid #f0f0f0; /* 分隔线,区分两个选项 */ `; - syncNumItem.textContent = '配置同步数量'; - syncNumItem.addEventListener('click', () => { - const nodeId = this.menuElement?.getAttribute('data-node-id'); + syncNumItem.textContent = "配置同步数量"; + syncNumItem.addEventListener("click", () => { + const nodeId = this.menuElement?.getAttribute("data-node-id"); if (nodeId) { const graph = this.getGraph(); const targetNode = graph.getCellById(nodeId) as Node; @@ -215,28 +340,40 @@ export class X6Graph { // TODO:配置同步数量逻辑 + // 3. 事件总线传递:携带完整链信息 + EventBusTool.getEventBus().publish("antvX6Event", { + key: "configNode", + id: nodeId, + label: targetNode.attr("label/text") || "", + level: nodeLevel, + isTbsl:true, + }); } this.hideMenu(); }); // 配置同步数量菜单悬停效果 - syncNumItem.addEventListener('mouseenter', () => { - syncNumItem.style.background = '#f0f0f0'; + syncNumItem.addEventListener("mouseenter", () => { + syncNumItem.style.background = "#f0f0f0"; }); - syncNumItem.addEventListener('mouseleave', () => { - syncNumItem.style.background = 'white'; + syncNumItem.addEventListener("mouseleave", () => { + syncNumItem.style.background = "white"; }); // 3. 原有“删除”菜单项(保持不变,调整顺序在配置项下方) - const deleteItem = document.createElement('div'); - deleteItem.id = 'ant-x6-menu-3'; + let deleteItem = document.getElementById("ant-x6-menu-3"); + if (deleteItem) { + deleteItem.remove(); + } + deleteItem = document.createElement("div"); + deleteItem.id = "ant-x6-menu-3"; deleteItem.style.cssText = ` padding: 6px 12px; cursor: pointer; font-size: 14px; `; - deleteItem.textContent = '删除'; - deleteItem.addEventListener('click', () => { - const nodeId = this.menuElement?.getAttribute('data-node-id'); + deleteItem.textContent = "删除"; + deleteItem.addEventListener("click", () => { + const nodeId = this.menuElement?.getAttribute("data-node-id"); if (nodeId) { // 获取图形实例和目标节点(和配置项逻辑一致) const graph = this.getGraph(); @@ -244,8 +381,8 @@ export class X6Graph { // 从节点数据中提取level const nodeLevel = targetNode.getData().level; this.removeNode(nodeId); - EventBusTool.getEventBus().publish('antvX6Event', { - key: 'removeNode', + EventBusTool.getEventBus().publish("antvX6Event", { + key: "removeNode", id: nodeId, level: nodeLevel, // 新增:传递level信息 }); @@ -253,14 +390,15 @@ export class X6Graph { this.hideMenu(); }); // 删除项悬停效果 - deleteItem.addEventListener('mouseenter', () => { - deleteItem.style.background = '#f0f0f0'; + deleteItem.addEventListener("mouseenter", () => { + deleteItem.style.background = "#f0f0f0"; }); - deleteItem.addEventListener('mouseleave', () => { - deleteItem.style.background = 'white'; + deleteItem.addEventListener("mouseleave", () => { + deleteItem.style.background = "white"; }); // 3. 将两个菜单项添加到菜单容器(配置项在上,删除项在下) + this.menuElement.appendChild(pzItem); this.menuElement.appendChild(configItem); this.menuElement.appendChild(syncNumItem); this.menuElement.appendChild(deleteItem); @@ -271,12 +409,11 @@ export class X6Graph { * 显示右键菜单 */ private showMenu(nodeId: string, x: number, y: number) { - if (!this.menuElement) - return; - this.menuElement.setAttribute('data-node-id', nodeId); + if (!this.menuElement) return; + this.menuElement.setAttribute("data-node-id", nodeId); this.menuElement.style.left = `${x}px`; this.menuElement.style.top = `${y}px`; - this.menuElement.style.display = 'block'; + this.menuElement.style.display = "block"; } /** @@ -284,7 +421,7 @@ export class X6Graph { */ private hideMenu() { if (this.menuElement) { - this.menuElement.style.display = 'none'; + this.menuElement.style.display = "none"; } } @@ -302,14 +439,14 @@ export class X6Graph { container, width: container.clientWidth, height: container.clientHeight, - grid: options.grid ?? { size: 10, visible: false, type: 'dot' }, + grid: options.grid ?? { size: 10, visible: false, type: "dot" }, mousewheel: options.zoom ?? { enabled: true, - modifiers: 'ctrl', + modifiers: "ctrl", minScale: 0.5, maxScale: 2, }, - panning: { enabled: true, modifier: 'shift' }, + panning: { enabled: true, modifier: "shift" }, snapline: true, // 关键:完善选择配置,确保单击和框选生效 selecting: { @@ -318,20 +455,24 @@ export class X6Graph { }); // 注册“直接曲线虚线”自定义边 - Graph.registerEdge('direct-curved-dashed-edge', { - inherit: 'edge', - // 连接器:使用 smooth(平滑曲线,无额外拐点) - connector: { name: 'smooth' }, - attrs: { - line: { - targetMarker: '', // 移除目标标记(可选) - stroke: '#A2B1C3', // 线条颜色 - strokeWidth: 2, // 线条宽度 - strokeDasharray: '5, 5', // 虚线:线段5px + 间隔5px + Graph.registerEdge( + "direct-curved-dashed-edge", + { + inherit: "edge", + // 连接器:使用 smooth(平滑曲线,无额外拐点) + connector: { name: "smooth" }, + attrs: { + line: { + targetMarker: "", // 移除目标标记(可选) + stroke: "#A2B1C3", // 线条颜色 + strokeWidth: 2, // 线条宽度 + strokeDasharray: "5, 5", // 虚线:线段5px + 间隔5px + }, }, + zIndex: 0, // 层级(避免遮挡节点) }, - zIndex: 0, // 层级(避免遮挡节点) - }, true); + true, + ); // 绑定右键菜单事件 this.bindRightClickEvent(); @@ -346,7 +487,7 @@ export class X6Graph { const graph = this.getGraph(); // 节点右键点击 - graph.on('node:contextmenu', (args) => { + graph.on("node:contextmenu", (args) => { args.e.preventDefault(); // 阻止浏览器默认右键菜单 const { cell, e } = args; const node = cell as Node; @@ -360,25 +501,40 @@ export class X6Graph { return; } - if (nodeData.level === 'level2') { - - } else if (nodeData.level === 'level3') { - - } else if (nodeData.level === 'level4') { - + if (nodeData.level === "level2") { + const item2 = document.getElementById("ant-x6-menu"); + item2.style.display = "block"; + const item = document.getElementById("ant-x6-menu-1"); + item.style.display = "none"; + const item1 = document.getElementById("ant-x6-menu-2"); + item1.style.display = "none"; + } else if (nodeData.level === "level3") { + const item = document.getElementById("ant-x6-menu-1"); + item.style.display = "block"; + const item1 = document.getElementById("ant-x6-menu-2"); + item1.style.display = "block"; + const item2 = document.getElementById("ant-x6-menu"); + item2.style.display = "none"; + } else if (nodeData.level === "level4") { + const item = document.getElementById("ant-x6-menu-1"); + item.style.display = "none"; + const item1 = document.getElementById("ant-x6-menu-2"); + item1.style.display = "none"; + const item2 = document.getElementById("ant-x6-menu"); + item2.style.display = "none"; } // 显示菜单(位置为点击坐标) this.showMenu(node.id, e.clientX, e.clientY); }); // 画布空白处右键点击(隐藏菜单) - graph.on('blank:contextmenu', (args) => { + graph.on("blank:contextmenu", (args) => { args.e.preventDefault(); this.hideMenu(); }); // 点击其他区域隐藏菜单 - document.addEventListener('click', () => { + document.addEventListener("click", () => { this.hideMenu(); }); } @@ -388,7 +544,7 @@ export class X6Graph { */ private calculateTextWidth(text: string, fontSize = 14): number { // 创建临时元素计算文字宽度 - const tempSpan = document.createElement('span'); + const tempSpan = document.createElement("span"); tempSpan.style.cssText = ` position: absolute; visibility: hidden; @@ -430,9 +586,11 @@ export class X6Graph { private addNode(config: SimpleNodeConfig): Node { const graph = this.getGraph(); // 计算根节点自适应宽度 - const rootWidth = this.calculateTextWidth(config.label, this.nodeStyle.label.fontSize) + this.nodePadding * 2; + const rootWidth = + this.calculateTextWidth(config.label, this.nodeStyle.label.fontSize) + + this.nodePadding * 2; const node = graph.addNode({ - shape: 'rect', + shape: "rect", id: config.id, x: config.x, y: config.y, @@ -451,11 +609,11 @@ export class X6Graph { // 文字的样式(颜色、位置、字号等) label: { text: config.label, - fill: '#ffffff', // 白色文字 + fill: "#ffffff", // 白色文字 fontSize: 14, - fontWeight: 'bold', - textAnchor: 'middle', // 文字水平居中 - verticalAnchor: 'middle', // 文字垂直居中 + fontWeight: "bold", + textAnchor: "middle", // 文字水平居中 + verticalAnchor: "middle", // 文字垂直居中 }, }, data: { @@ -473,7 +631,10 @@ export class X6Graph { /** * 添加子节点(按层级动态分配位置) */ - addChildNode(parentId: string, config: ChildNodeConfig): { node: Node; edge: Edge } { + addChildNode( + parentId: string, + config: ChildNodeConfig, + ): { node: Node; edge: Edge } { const graph = this.getGraph(); // 校验父节点 @@ -500,20 +661,20 @@ export class X6Graph { // 创建连线 const edge = graph.addEdge({ - shape: 'direct-curved-dashed-edge', + shape: "direct-curved-dashed-edge", source: { cell: parentId, - anchor: { name: 'right' }, + anchor: { name: "right" }, }, // 目标节点(子节点)的锚点:左侧 target: { cell: childNode.id, - anchor: { name: 'left' }, + anchor: { name: "left" }, }, style: { - stroke: '#888', + stroke: "#888", strokeWidth: 1.2, - targetMarker: { name: 'block', size: 8 }, + targetMarker: { name: "block", size: 8 }, }, }); @@ -524,8 +685,8 @@ export class X6Graph { // 重排同层级节点 this.rearrangeLevelNodes(config.level); // 新增:如果添加的是第三层级,同步重排第二层级 - if (config.level === 'level3') { - this.rearrangeLevelNodes('level2'); + if (config.level === "level3") { + this.rearrangeLevelNodes("level2"); } return { node: childNode, edge: edge as Edge }; } @@ -539,28 +700,28 @@ export class X6Graph { const centerY = (containerHeight - this.nodeStyle.height) / 2; // 画布中心点 // 获取当前层级的所有节点(按标签排序) - const levelNodes = graph.getNodes() - .filter(node => node.getData().level === level) + const levelNodes = graph + .getNodes() + .filter((node) => node.getData().level === level) .sort((a, b) => { - const labelA = a.attr('label/text') || ''; - const labelB = b.attr('label/text') || ''; + const labelA = a.attr("label/text") || ""; + const labelB = b.attr("label/text") || ""; return labelA.localeCompare(labelB, undefined, { numeric: true }); }); const totalNodes = levelNodes.length; - if (totalNodes === 0) - return; + if (totalNodes === 0) return; // ======================== 第二层级:间距根据自身第三层级子节点数量动态调整 ======================== - if (level === 'level2') { - // 1. 统计每个第二层级节点的第三层级子节点数量 + if (level === "level2") { + // 1. 统计每个第二层级节点的第三层级子节点数量 const level2ChildCount = new Map(); levelNodes.forEach((node) => { const childIds = this.getChildNodeIds(node.id); // 筛选出属于第三层级的子节点 const level3ChildCount = childIds.filter((childId) => { const childNode = graph.getCellById(childId) as Node; - return childNode?.getData().level === 'level3'; + return childNode?.getData().level === "level3"; }).length; level2ChildCount.set(node.id, level3ChildCount); }); @@ -588,58 +749,73 @@ export class X6Graph { // 3. 计算整体偏移量,让第二层级节点整体垂直居中 const lastNodeY = nodePositions.get(levelNodes[totalNodes - 1].id) || 0; - const totalHeight = lastNodeY - nodePositions.get(levelNodes[0].id) || 0 + this.nodeStyle.height; - const offset = centerY - (nodePositions.get(levelNodes[0].id) || 0) - totalHeight / 2; + const totalHeight = + lastNodeY - nodePositions.get(levelNodes[0].id) || + 0 + this.nodeStyle.height; + const offset = + centerY - (nodePositions.get(levelNodes[0].id) || 0) - totalHeight / 2; // 4. 应用最终位置(加上偏移量,确保整体居中) levelNodes.forEach((node) => { const nodeY = (nodePositions.get(node.id) || 0) + offset; - const nodeX = this.levelXMap.get('level2') || 300; + const nodeX = this.levelXMap.get("level2") || 300; node.setPosition(nodeX, nodeY); }); } // ======================== 第三层级:跟随父节点垂直分布 ======================== - else if (level === 'level3') { + else if (level === "level3") { const parentGroups = new Map(); levelNodes.forEach((node) => { - const parentEdge = graph.getEdges().find(edge => edge.getTargetCellId() === node.id); + const parentEdge = graph + .getEdges() + .find((edge) => edge.getTargetCellId() === node.id); if (parentEdge) { const parentId = parentEdge.getSourceCellId(); - parentGroups.set(parentId, [...(parentGroups.get(parentId) || []), node]); + parentGroups.set(parentId, [ + ...(parentGroups.get(parentId) || []), + node, + ]); } }); parentGroups.forEach((children, parentId) => { const parentNode = graph.getCellById(parentId) as Node; - if (!parentNode) - return; + if (!parentNode) return; const parentY = parentNode.getPosition().y; const childCount = children.length; const verticalGap = 20; // 第三层级内部固定间距 - const totalChildHeight = this.nodeStyle.height * childCount + verticalGap * (childCount - 1); + const totalChildHeight = + this.nodeStyle.height * childCount + verticalGap * (childCount - 1); const startOffset = -totalChildHeight / 2 + this.nodeStyle.height / 2; children.forEach((child, index) => { - const childY = parentY + startOffset + (this.nodeStyle.height + verticalGap) * index; - const childX = this.levelXMap.get('level3') || 550; + const childY = + parentY + + startOffset + + (this.nodeStyle.height + verticalGap) * index; + const childX = this.levelXMap.get("level3") || 550; child.setPosition(childX, childY); }); }); - } - else if (level === 'level4') { - // 1. 按父节点(level3)分组(每个level3只对应1个level4) + } else if (level === "level4") { + // 1. 按父节点(level3)分组(每个level3只对应1个level4) const parentGroups = new Map(); levelNodes.forEach((node) => { - const parentEdge = graph.getEdges().find(edge => edge.getTargetCellId() === node.id); + const parentEdge = graph + .getEdges() + .find((edge) => edge.getTargetCellId() === node.id); if (parentEdge) { const parentId = parentEdge.getSourceCellId(); // 只保留level3的父节点(确保父节点层级正确) const parentNode = graph.getCellById(parentId) as Node; - if (parentNode?.getData().level === 'level3') { - parentGroups.set(parentId, [...(parentGroups.get(parentId) || []), node]); + if (parentNode?.getData().level === "level3") { + parentGroups.set(parentId, [ + ...(parentGroups.get(parentId) || []), + node, + ]); } } }); @@ -647,12 +823,11 @@ export class X6Graph { // 2. 每个level3父节点的level4子节点:放在父节点右侧(固定间距) parentGroups.forEach((children, parentId) => { const parentNode = graph.getCellById(parentId) as Node; - if (!parentNode) - return; + if (!parentNode) return; // level4固定在level3父节点右侧,X坐标=level3X + 节点宽度 + 间距(20px) const parentPos = parentNode.getPosition(); - const level4X = this.levelXMap.get('level4') || 800; + const level4X = this.levelXMap.get("level4") || 800; // Y坐标与父节点完全对齐(上下居中) const level4Y = parentPos.y; @@ -662,8 +837,8 @@ export class X6Graph { targetChild.setPosition(level4X, level4Y); // 多余节点直接删除(避免多个level4) if (children.length > 1) { - children.slice(1).forEach(excessNode => excessNode.remove()); - this.resetLevelCount('level4'); // 重置数量统计 + children.slice(1).forEach((excessNode) => excessNode.remove()); + this.resetLevelCount("level4"); // 重置数量统计 } } }); @@ -676,8 +851,7 @@ export class X6Graph { removeNode(nodeId: string): void { const graph = this.getGraph(); const cell = graph.getCellById(nodeId); - if (!cell || !graph.isNode(cell)) - return; + if (!cell || !graph.isNode(cell)) return; const node = cell as Node; const nodeLevel = node.getData().level as NodeLevel; @@ -702,33 +876,30 @@ export class X6Graph { * @param importData - 目标灌库数据(格式如 [{userName: "...", tables: [...]}]) * @param rootConfig - 根节点配置(需提前确定根节点的 id 和 label,如 sourceOwner) */ - importData( - importData: ImportDataItem[], - rootId, - ): void { + importData(importData: ImportDataItem[], rootId): void { // 3. 遍历灌库数据,创建 level2→level3→level4 层级 importData.forEach((item) => { - // 3.1 创建 level2 节点(userName 作为 label,父节点是根节点) + // 3.1 创建 level2 节点(userName 作为 label,父节点是根节点) const { node: level2Node } = this.addChildNode(rootId, { id: uuidv4(), label: item.userName, - level: 'level2', + level: "level2", }); // 3.2 遍历当前 level2 的 tables,创建 level3 和 level4 节点 item.tables.forEach((table) => { - // 3.2.1 创建 level3 节点(tableName 作为 label,父节点是当前 level2) + // 3.2.1 创建 level3 节点(tableName 作为 label,父节点是当前 level2) const { node: level3Node } = this.addChildNode(level2Node.id, { id: uuidv4(), label: table.tableName, - level: 'level3', + level: "level3", }); // 3.2.2 创建 level4 节点(columnName 作为 label,父节点是当前 level3) this.addChildNode(level3Node.id, { id: uuidv4(), label: table.columnName, - level: 'level4', + level: "level4", }); }); }); @@ -740,55 +911,72 @@ export class X6Graph { * @param name - 传入的targetOwner字段值 * @returns 符合格式的目标表格数组 / 错误信息 */ - getTargetTableData(taskId: string, name: string): { data?: TargetTableItem[]; error?: string } { + getTargetTableData( + taskId: string, + name: string, + ): { data?: TargetTableItem[]; error?: string } { const graph = this.getGraph(); const result: TargetTableItem[] = []; // 1. 找到所有 level2 节点(sourceOwner 来源于 level2 的 label) - const level2Nodes = graph.getNodes().filter(node => node.getData().level === 'level2'); + const level2Nodes = graph + .getNodes() + .filter((node) => node.getData().level === "level2"); if (level2Nodes.length === 0) { - return { error: '未找到任何 level2 节点,无法组装数据' }; + return { error: "未找到任何 level2 节点,无法组装数据" }; } // 2. 遍历每个 level2 节点,校验子级并组装数据 for (const level2Node of level2Nodes) { - const sourceOwner = level2Node.attr('label/text') || ''; + const sourceOwner = level2Node.attr("label/text") || ""; if (!sourceOwner) { - return { error: `存在未设置 label 的 level2 节点(ID: ${level2Node.id})` }; + return { + error: `存在未设置 label 的 level2 节点(ID: ${level2Node.id})`, + }; } // 2.1 校验 level2 是否有子级(必须包含 level3) const level2ChildIds = this.getChildNodeIds(level2Node.id); const level3Nodes = level2ChildIds - .map(id => graph.getCellById(id) as Node) - .filter(node => node?.getData().level === 'level3'); + .map((id) => graph.getCellById(id) as Node) + .filter((node) => node?.getData().level === "level3"); if (level3Nodes.length === 0) { - return { error: `level2 节点「${sourceOwner}」下无 level3 子节点,请完善配置` }; + return { + error: `level2 节点「${sourceOwner}」下无 level3 子节点,请完善配置`, + }; } // 2.2 遍历每个 level3 节点,校验子级并组装数据 for (const level3Node of level3Nodes) { - const tableName = level3Node.attr('label/text') || ''; + const tableName = level3Node.attr("label/text") || ""; if (!tableName) { - return { error: `存在未设置 label 的 level3 节点(父级:${sourceOwner})` }; + return { + error: `存在未设置 label 的 level3 节点(父级:${sourceOwner})`, + }; } + const syncCount = level3Node.getData().syncCount||""; + // 2.2.1 校验 level3 是否有子级(必须包含 level4) const level3ChildIds = this.getChildNodeIds(level3Node.id); const level4Nodes = level3ChildIds - .map(id => graph.getCellById(id) as Node) - .filter(node => node?.getData().level === 'level4'); + .map((id) => graph.getCellById(id) as Node) + .filter((node) => node?.getData().level === "level4"); if (level4Nodes.length === 0) { - return { error: `level3 节点「${tableName}」下无 level4 子节点,请完善配置` }; + return { + error: `level3 节点「${tableName}」下无 level4 子节点,请完善配置`, + }; } // 2.2.2 遍历每个 level4 节点(每个level3仅取第一个有效level4,避免重复) const validLevel4Node = level4Nodes[0]; - const columnName = validLevel4Node.attr('label/text') || ''; + const columnName = validLevel4Node.attr("label/text") || ""; if (!columnName) { - return { error: `存在未设置 label 的 level4 节点(父级:${tableName})` }; + return { + error: `存在未设置 label 的 level4 节点(父级:${tableName})`, + }; } // 2.2.3 组装单条数据 @@ -798,6 +986,7 @@ export class X6Graph { targetOwner: name, // 传入的name字段作为targetOwner tableName, columnName, + syncCount }); } } @@ -821,7 +1010,10 @@ export class X6Graph { * 获取子节点ID列表 */ private getChildNodeIds(parentId: string): string[] { - return this.getGraph().getEdges().filter(edge => edge.getSourceCellId() === parentId).map(edge => edge.getTargetCellId()); + return this.getGraph() + .getEdges() + .filter((edge) => edge.getSourceCellId() === parentId) + .map((edge) => edge.getTargetCellId()); } /** @@ -836,9 +1028,9 @@ export class X6Graph { */ clear(): void { this.getGraph().clearCells(); - this.levelCountMap.set('level2', 0); - this.levelCountMap.set('level3', 0); - this.levelCountMap.set('level4', 0); + this.levelCountMap.set("level2", 0); + this.levelCountMap.set("level3", 0); + this.levelCountMap.set("level4", 0); } /** @@ -858,7 +1050,7 @@ export class X6Graph { */ private getGraph(): Graph { if (!this.graph) { - throw new Error('请先调用 init() 初始化画布'); + throw new Error("请先调用 init() 初始化画布"); } return this.graph; } diff --git a/src/pages/sjtb/configTask/dataTable/index.vue b/src/pages/sjtb/configTask/dataTable/index.vue index 4978aece..468c4f44 100644 --- a/src/pages/sjtb/configTask/dataTable/index.vue +++ b/src/pages/sjtb/configTask/dataTable/index.vue @@ -1,16 +1,23 @@ @@ -447,6 +605,14 @@ function handleCancel() { width: 100%; padding: 20px; box-sizing: border-box; + position: relative; + + ::v-deep .config-dialog { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + } .custom-form { margin-bottom: 20px; diff --git a/src/pages/xzypybj/configTask/dataTable/antvX6.ts b/src/pages/xzypybj/configTask/dataTable/antvX6.ts index fdc1548b..c9bc3e21 100644 --- a/src/pages/xzypybj/configTask/dataTable/antvX6.ts +++ b/src/pages/xzypybj/configTask/dataTable/antvX6.ts @@ -1,9 +1,9 @@ -import type { Edge, Node, NodeConfig } from '@antv/x6'; -import { Graph } from '@antv/x6'; -import { v4 as uuidv4 } from 'uuid'; +import type { Edge, Node, NodeConfig } from "@antv/x6"; +import { Graph } from "@antv/x6"; +import { v4 as uuidv4 } from "uuid"; // 层级类型定义 -export type NodeLevel = 'level2' | 'level3' | 'level4'; +export type NodeLevel = "level2" | "level3" | "level4"; // 目标灌库数据的类型定义 interface ImportDataItem { @@ -50,14 +50,14 @@ export class X6Graph { private readonly nodeStyle = { width: 100, height: 40, - fill: '#0a4a5c', // 深色背景(示例中的深青色) - stroke: '#0a4a5c', // 边框与背景同色,视觉上隐藏边框 + fill: "#0a4a5c", // 深色背景(示例中的深青色) + stroke: "#0a4a5c", // 边框与背景同色,视觉上隐藏边框 strokeWidth: 2, radius: 4, label: { - fill: '#93CDD5', // 白色文字 + fill: "#93CDD5", // 白色文字 fontSize: 14, - fontWeight: 'bold', // 可选:文字加粗 + fontWeight: "bold", // 可选:文字加粗 }, }; @@ -69,16 +69,16 @@ export class X6Graph { // 层级X坐标映射 private readonly levelXMap = new Map([ - ['level2', 300], - ['level3', 550], - ['level4', 800], + ["level2", 300], + ["level3", 550], + ["level4", 800], ]); // 层级节点数量记录 private readonly levelCountMap = new Map([ - ['level2', 0], - ['level3', 0], - ['level4', 0], + ["level2", 0], + ["level3", 0], + ["level4", 0], ]); constructor(containerId: string) { @@ -87,7 +87,7 @@ export class X6Graph { } private createMenuElement() { - this.menuElement = document.createElement('div'); + this.menuElement = document.createElement("div"); this.menuElement.style.cssText = ` position: absolute; width: 100px; @@ -102,16 +102,16 @@ export class X6Graph { `; // 1. 新增“配置”菜单项 - const configItem = document.createElement('div'); + const configItem = document.createElement("div"); configItem.style.cssText = ` padding: 6px 12px; cursor: pointer; font-size: 14px; border-bottom: 1px solid #f0f0f0; /* 分隔线,区分两个选项 */ `; - configItem.textContent = '配置'; - configItem.addEventListener('click', () => { - const nodeId = this.menuElement?.getAttribute('data-node-id'); + configItem.textContent = "配置"; + configItem.addEventListener("click", () => { + const nodeId = this.menuElement?.getAttribute("data-node-id"); if (nodeId) { const graph = this.getGraph(); const targetNode = graph.getCellById(nodeId) as Node; @@ -121,35 +121,41 @@ export class X6Graph { // 2. 高亮样式(保持原有) targetNode.attr({ body: { - stroke: 'rgb(12,129,123)', + stroke: "rgb(12,129,123)", strokeWidth: 2, - strokeDasharray: '5, 5', + strokeDasharray: "5, 5", }, }); // ======================== 新增:获取所有父级信息 ======================== // 1. 定义追溯父级的方法 - const getAllParents = (targetNodeId: string): Array<{ id: string; label: string; level?: NodeLevel }> => { - const parents: Array<{ id: string; label: string; level?: NodeLevel }> = []; + const getAllParents = ( + targetNodeId: string, + ): Array<{ id: string; label: string; level?: NodeLevel }> => { + const parents: Array<{ + id: string; + label: string; + level?: NodeLevel; + }> = []; let currentNodeId = targetNodeId; // 循环追溯:直到找不到父节点(根节点) while (true) { // 找到指向当前节点的连线(来源即父节点) - const parentEdge = graph.getEdges().find(edge => edge.getTargetCellId() === currentNodeId); - if (!parentEdge) - break; // 无父节点,终止循环 + const parentEdge = graph + .getEdges() + .find((edge) => edge.getTargetCellId() === currentNodeId); + if (!parentEdge) break; // 无父节点,终止循环 // 获取父节点ID和父节点实例 const parentNodeId = parentEdge.getSourceCellId(); const parentNode = graph.getCellById(parentNodeId) as Node; - if (!parentNode) - break; + if (!parentNode) break; // 收集父节点信息(ID、标签、层级) parents.push({ id: parentNodeId, - label: parentNode.attr('label/text') || '', // 节点显示的文字 + label: parentNode.attr("label/text") || "", // 节点显示的文字 level: parentNode.getData().level, // 父节点的层级 }); @@ -168,19 +174,19 @@ export class X6Graph { // 关键:添加当前节点信息(含label),拼接成“父级+当前节点”的完整链 const currentNodeInfo = { id: nodeId, - label: targetNode.attr('label/text') || '', // 从当前节点获取label + label: targetNode.attr("label/text") || "", // 从当前节点获取label level: nodeLevel, }; allParentsOrdered = [...allParentsOrdered, currentNodeInfo]; - console.log('目标节点(父级+自身)完整链:', allParentsOrdered); + console.log("目标节点(父级+自身)完整链:", allParentsOrdered); // ======================================================================== // 3. 事件总线传递:携带完整链信息 - EventBusTool.getEventBus().publish('antvX6Event', { - key: 'configNode', + EventBusTool.getEventBus().publish("antvX6Event", { + key: "configNode", id: nodeId, - label: targetNode.attr('label/text') || '', + label: targetNode.attr("label/text") || "", level: nodeLevel, allParents: allParentsOrdered, // 包含父级+当前节点(均有label) }); @@ -188,23 +194,23 @@ export class X6Graph { this.hideMenu(); }); // 配置项悬停效果 - configItem.addEventListener('mouseenter', () => { - configItem.style.background = '#f0f0f0'; + configItem.addEventListener("mouseenter", () => { + configItem.style.background = "#f0f0f0"; }); - configItem.addEventListener('mouseleave', () => { - configItem.style.background = 'white'; + configItem.addEventListener("mouseleave", () => { + configItem.style.background = "white"; }); // 2. 原有“删除”菜单项(保持不变,调整顺序在配置项下方) - const deleteItem = document.createElement('div'); + const deleteItem = document.createElement("div"); deleteItem.style.cssText = ` padding: 6px 12px; cursor: pointer; font-size: 14px; `; - deleteItem.textContent = '删除'; - deleteItem.addEventListener('click', () => { - const nodeId = this.menuElement?.getAttribute('data-node-id'); + deleteItem.textContent = "删除"; + deleteItem.addEventListener("click", () => { + const nodeId = this.menuElement?.getAttribute("data-node-id"); if (nodeId) { // 获取图形实例和目标节点(和配置项逻辑一致) const graph = this.getGraph(); @@ -212,8 +218,8 @@ export class X6Graph { // 从节点数据中提取level const nodeLevel = targetNode.getData().level; this.removeNode(nodeId); - EventBusTool.getEventBus().publish('antvX6Event', { - key: 'removeNode', + EventBusTool.getEventBus().publish("antvX6Event", { + key: "removeNode", id: nodeId, level: nodeLevel, // 新增:传递level信息 }); @@ -221,11 +227,11 @@ export class X6Graph { this.hideMenu(); }); // 删除项悬停效果 - deleteItem.addEventListener('mouseenter', () => { - deleteItem.style.background = '#f0f0f0'; + deleteItem.addEventListener("mouseenter", () => { + deleteItem.style.background = "#f0f0f0"; }); - deleteItem.addEventListener('mouseleave', () => { - deleteItem.style.background = 'white'; + deleteItem.addEventListener("mouseleave", () => { + deleteItem.style.background = "white"; }); // 3. 将两个菜单项添加到菜单容器(配置项在上,删除项在下) @@ -238,12 +244,11 @@ export class X6Graph { * 显示右键菜单 */ private showMenu(nodeId: string, x: number, y: number) { - if (!this.menuElement) - return; - this.menuElement.setAttribute('data-node-id', nodeId); + if (!this.menuElement) return; + this.menuElement.setAttribute("data-node-id", nodeId); this.menuElement.style.left = `${x}px`; this.menuElement.style.top = `${y}px`; - this.menuElement.style.display = 'block'; + this.menuElement.style.display = "block"; } /** @@ -251,7 +256,7 @@ export class X6Graph { */ private hideMenu() { if (this.menuElement) { - this.menuElement.style.display = 'none'; + this.menuElement.style.display = "none"; } } @@ -269,14 +274,14 @@ export class X6Graph { container, width: container.clientWidth, height: container.clientHeight, - grid: options.grid ?? { size: 10, visible: false, type: 'dot' }, + grid: options.grid ?? { size: 10, visible: false, type: "dot" }, mousewheel: options.zoom ?? { enabled: true, - modifiers: 'ctrl', + modifiers: "ctrl", minScale: 0.5, maxScale: 2, }, - panning: { enabled: true, modifier: 'shift' }, + panning: { enabled: true, modifier: "shift" }, snapline: true, // 关键:完善选择配置,确保单击和框选生效 selecting: { @@ -285,20 +290,24 @@ export class X6Graph { }); // 注册“直接曲线虚线”自定义边 - Graph.registerEdge('direct-curved-dashed-edge', { - inherit: 'edge', - // 连接器:使用 smooth(平滑曲线,无额外拐点) - connector: { name: 'smooth' }, - attrs: { - line: { - targetMarker: '', // 移除目标标记(可选) - stroke: '#A2B1C3', // 线条颜色 - strokeWidth: 2, // 线条宽度 - strokeDasharray: '5, 5', // 虚线:线段5px + 间隔5px + Graph.registerEdge( + "direct-curved-dashed-edge", + { + inherit: "edge", + // 连接器:使用 smooth(平滑曲线,无额外拐点) + connector: { name: "smooth" }, + attrs: { + line: { + targetMarker: "", // 移除目标标记(可选) + stroke: "#A2B1C3", // 线条颜色 + strokeWidth: 2, // 线条宽度 + strokeDasharray: "5, 5", // 虚线:线段5px + 间隔5px + }, }, + zIndex: 0, // 层级(避免遮挡节点) }, - zIndex: 0, // 层级(避免遮挡节点) - }, true); + true, + ); // 绑定右键菜单事件 this.bindRightClickEvent(); @@ -313,7 +322,7 @@ export class X6Graph { const graph = this.getGraph(); // 节点右键点击 - graph.on('node:contextmenu', (args) => { + graph.on("node:contextmenu", (args) => { args.e.preventDefault(); // 阻止浏览器默认右键菜单 const { cell, e } = args; const node = cell as Node; @@ -331,13 +340,13 @@ export class X6Graph { }); // 画布空白处右键点击(隐藏菜单) - graph.on('blank:contextmenu', (args) => { + graph.on("blank:contextmenu", (args) => { args.e.preventDefault(); this.hideMenu(); }); // 点击其他区域隐藏菜单 - document.addEventListener('click', () => { + document.addEventListener("click", () => { this.hideMenu(); }); } @@ -368,7 +377,7 @@ export class X6Graph { private addNode(config: SimpleNodeConfig): Node { const graph = this.getGraph(); const node = graph.addNode({ - shape: 'rect', + shape: "rect", id: config.id, x: config.x, y: config.y, @@ -387,11 +396,11 @@ export class X6Graph { // 文字的样式(颜色、位置、字号等) label: { text: config.label, - fill: '#ffffff', // 白色文字 + fill: "#ffffff", // 白色文字 fontSize: 14, - fontWeight: 'bold', - textAnchor: 'middle', // 文字水平居中 - verticalAnchor: 'middle', // 文字垂直居中 + fontWeight: "bold", + textAnchor: "middle", // 文字水平居中 + verticalAnchor: "middle", // 文字垂直居中 }, }, data: { @@ -409,7 +418,10 @@ export class X6Graph { /** * 添加子节点(按层级动态分配位置) */ - addChildNode(parentId: string, config: ChildNodeConfig): { node: Node; edge: Edge } { + addChildNode( + parentId: string, + config: ChildNodeConfig, + ): { node: Node; edge: Edge } { const graph = this.getGraph(); // 校验父节点 @@ -436,20 +448,20 @@ export class X6Graph { // 创建连线 const edge = graph.addEdge({ - shape: 'direct-curved-dashed-edge', + shape: "direct-curved-dashed-edge", source: { cell: parentId, - anchor: { name: 'right' }, + anchor: { name: "right" }, }, // 目标节点(子节点)的锚点:左侧 target: { cell: childNode.id, - anchor: { name: 'left' }, + anchor: { name: "left" }, }, style: { - stroke: '#888', + stroke: "#888", strokeWidth: 1.2, - targetMarker: { name: 'block', size: 8 }, + targetMarker: { name: "block", size: 8 }, }, }); @@ -460,8 +472,8 @@ export class X6Graph { // 重排同层级节点 this.rearrangeLevelNodes(config.level); // 新增:如果添加的是第三层级,同步重排第二层级 - if (config.level === 'level3') { - this.rearrangeLevelNodes('level2'); + if (config.level === "level3") { + this.rearrangeLevelNodes("level2"); } return { node: childNode, edge: edge as Edge }; } @@ -475,28 +487,28 @@ export class X6Graph { const centerY = (containerHeight - this.nodeStyle.height) / 2; // 画布中心点 // 获取当前层级的所有节点(按标签排序) - const levelNodes = graph.getNodes() - .filter(node => node.getData().level === level) + const levelNodes = graph + .getNodes() + .filter((node) => node.getData().level === level) .sort((a, b) => { - const labelA = a.attr('label/text') || ''; - const labelB = b.attr('label/text') || ''; + const labelA = a.attr("label/text") || ""; + const labelB = b.attr("label/text") || ""; return labelA.localeCompare(labelB, undefined, { numeric: true }); }); const totalNodes = levelNodes.length; - if (totalNodes === 0) - return; + if (totalNodes === 0) return; // ======================== 第二层级:间距根据自身第三层级子节点数量动态调整 ======================== - if (level === 'level2') { - // 1. 统计每个第二层级节点的第三层级子节点数量 + if (level === "level2") { + // 1. 统计每个第二层级节点的第三层级子节点数量 const level2ChildCount = new Map(); levelNodes.forEach((node) => { const childIds = this.getChildNodeIds(node.id); // 筛选出属于第三层级的子节点 const level3ChildCount = childIds.filter((childId) => { const childNode = graph.getCellById(childId) as Node; - return childNode?.getData().level === 'level3'; + return childNode?.getData().level === "level3"; }).length; level2ChildCount.set(node.id, level3ChildCount); }); @@ -524,58 +536,73 @@ export class X6Graph { // 3. 计算整体偏移量,让第二层级节点整体垂直居中 const lastNodeY = nodePositions.get(levelNodes[totalNodes - 1].id) || 0; - const totalHeight = lastNodeY - nodePositions.get(levelNodes[0].id) || 0 + this.nodeStyle.height; - const offset = centerY - (nodePositions.get(levelNodes[0].id) || 0) - totalHeight / 2; + const totalHeight = + lastNodeY - nodePositions.get(levelNodes[0].id) || + 0 + this.nodeStyle.height; + const offset = + centerY - (nodePositions.get(levelNodes[0].id) || 0) - totalHeight / 2; // 4. 应用最终位置(加上偏移量,确保整体居中) levelNodes.forEach((node) => { const nodeY = (nodePositions.get(node.id) || 0) + offset; - const nodeX = this.levelXMap.get('level2') || 300; + const nodeX = this.levelXMap.get("level2") || 300; node.setPosition(nodeX, nodeY); }); } // ======================== 第三层级:跟随父节点垂直分布 ======================== - else if (level === 'level3') { + else if (level === "level3") { const parentGroups = new Map(); levelNodes.forEach((node) => { - const parentEdge = graph.getEdges().find(edge => edge.getTargetCellId() === node.id); + const parentEdge = graph + .getEdges() + .find((edge) => edge.getTargetCellId() === node.id); if (parentEdge) { const parentId = parentEdge.getSourceCellId(); - parentGroups.set(parentId, [...(parentGroups.get(parentId) || []), node]); + parentGroups.set(parentId, [ + ...(parentGroups.get(parentId) || []), + node, + ]); } }); parentGroups.forEach((children, parentId) => { const parentNode = graph.getCellById(parentId) as Node; - if (!parentNode) - return; + if (!parentNode) return; const parentY = parentNode.getPosition().y; const childCount = children.length; const verticalGap = 20; // 第三层级内部固定间距 - const totalChildHeight = this.nodeStyle.height * childCount + verticalGap * (childCount - 1); + const totalChildHeight = + this.nodeStyle.height * childCount + verticalGap * (childCount - 1); const startOffset = -totalChildHeight / 2 + this.nodeStyle.height / 2; children.forEach((child, index) => { - const childY = parentY + startOffset + (this.nodeStyle.height + verticalGap) * index; - const childX = this.levelXMap.get('level3') || 550; + const childY = + parentY + + startOffset + + (this.nodeStyle.height + verticalGap) * index; + const childX = this.levelXMap.get("level3") || 550; child.setPosition(childX, childY); }); }); - } - else if (level === 'level4') { - // 1. 按父节点(level3)分组(每个level3只对应1个level4) + } else if (level === "level4") { + // 1. 按父节点(level3)分组(每个level3只对应1个level4) const parentGroups = new Map(); levelNodes.forEach((node) => { - const parentEdge = graph.getEdges().find(edge => edge.getTargetCellId() === node.id); + const parentEdge = graph + .getEdges() + .find((edge) => edge.getTargetCellId() === node.id); if (parentEdge) { const parentId = parentEdge.getSourceCellId(); // 只保留level3的父节点(确保父节点层级正确) const parentNode = graph.getCellById(parentId) as Node; - if (parentNode?.getData().level === 'level3') { - parentGroups.set(parentId, [...(parentGroups.get(parentId) || []), node]); + if (parentNode?.getData().level === "level3") { + parentGroups.set(parentId, [ + ...(parentGroups.get(parentId) || []), + node, + ]); } } }); @@ -583,12 +610,11 @@ export class X6Graph { // 2. 每个level3父节点的level4子节点:放在父节点右侧(固定间距) parentGroups.forEach((children, parentId) => { const parentNode = graph.getCellById(parentId) as Node; - if (!parentNode) - return; + if (!parentNode) return; // level4固定在level3父节点右侧,X坐标=level3X + 节点宽度 + 间距(20px) const parentPos = parentNode.getPosition(); - const level4X = this.levelXMap.get('level4') || 800; + const level4X = this.levelXMap.get("level4") || 800; // Y坐标与父节点完全对齐(上下居中) const level4Y = parentPos.y; @@ -598,8 +624,8 @@ export class X6Graph { targetChild.setPosition(level4X, level4Y); // 多余节点直接删除(避免多个level4) if (children.length > 1) { - children.slice(1).forEach(excessNode => excessNode.remove()); - this.resetLevelCount('level4'); // 重置数量统计 + children.slice(1).forEach((excessNode) => excessNode.remove()); + this.resetLevelCount("level4"); // 重置数量统计 } } }); @@ -612,8 +638,7 @@ export class X6Graph { removeNode(nodeId: string): void { const graph = this.getGraph(); const cell = graph.getCellById(nodeId); - if (!cell || !graph.isNode(cell)) - return; + if (!cell || !graph.isNode(cell)) return; const node = cell as Node; const nodeLevel = node.getData().level as NodeLevel; @@ -638,33 +663,30 @@ export class X6Graph { * @param importData - 目标灌库数据(格式如 [{userName: "...", tables: [...]}]) * @param rootConfig - 根节点配置(需提前确定根节点的 id 和 label,如 sourceOwner) */ - importData( - importData: ImportDataItem[], - rootId, - ): void { + importData(importData: ImportDataItem[], rootId): void { // 3. 遍历灌库数据,创建 level2→level3→level4 层级 importData.forEach((item) => { - // 3.1 创建 level2 节点(userName 作为 label,父节点是根节点) + // 3.1 创建 level2 节点(userName 作为 label,父节点是根节点) const { node: level2Node } = this.addChildNode(rootId, { id: uuidv4(), label: item.userName, - level: 'level2', + level: "level2", }); // 3.2 遍历当前 level2 的 tables,创建 level3 和 level4 节点 item.tables.forEach((table) => { - // 3.2.1 创建 level3 节点(tableName 作为 label,父节点是当前 level2) + // 3.2.1 创建 level3 节点(tableName 作为 label,父节点是当前 level2) const { node: level3Node } = this.addChildNode(level2Node.id, { id: uuidv4(), label: table.tableName, - level: 'level3', + level: "level3", }); // 3.2.2 创建 level4 节点(columnName 作为 label,父节点是当前 level3) this.addChildNode(level3Node.id, { id: uuidv4(), label: table.columnName, - level: 'level4', + level: "level4", }); }); }); @@ -676,55 +698,70 @@ export class X6Graph { * @param name - 传入的targetOwner字段值 * @returns 符合格式的目标表格数组 / 错误信息 */ - getTargetTableData(taskId: string, name: string): { data?: TargetTableItem[]; error?: string } { + getTargetTableData( + taskId: string, + name: string, + ): { data?: TargetTableItem[]; error?: string } { const graph = this.getGraph(); const result: TargetTableItem[] = []; // 1. 找到所有 level2 节点(sourceOwner 来源于 level2 的 label) - const level2Nodes = graph.getNodes().filter(node => node.getData().level === 'level2'); + const level2Nodes = graph + .getNodes() + .filter((node) => node.getData().level === "level2"); if (level2Nodes.length === 0) { - return { error: '未找到任何 level2 节点,无法组装数据' }; + return { error: "未找到任何 level2 节点,无法组装数据" }; } // 2. 遍历每个 level2 节点,校验子级并组装数据 for (const level2Node of level2Nodes) { - const sourceOwner = level2Node.attr('label/text') || ''; + const sourceOwner = level2Node.attr("label/text") || ""; if (!sourceOwner) { - return { error: `存在未设置 label 的 level2 节点(ID: ${level2Node.id})` }; + return { + error: `存在未设置 label 的 level2 节点(ID: ${level2Node.id})`, + }; } // 2.1 校验 level2 是否有子级(必须包含 level3) const level2ChildIds = this.getChildNodeIds(level2Node.id); const level3Nodes = level2ChildIds - .map(id => graph.getCellById(id) as Node) - .filter(node => node?.getData().level === 'level3'); + .map((id) => graph.getCellById(id) as Node) + .filter((node) => node?.getData().level === "level3"); if (level3Nodes.length === 0) { - return { error: `level2 节点「${sourceOwner}」下无 level3 子节点,请完善配置` }; + return { + error: `level2 节点「${sourceOwner}」下无 level3 子节点,请完善配置`, + }; } // 2.2 遍历每个 level3 节点,校验子级并组装数据 for (const level3Node of level3Nodes) { - const tableName = level3Node.attr('label/text') || ''; + const tableName = level3Node.attr("label/text") || ""; if (!tableName) { - return { error: `存在未设置 label 的 level3 节点(父级:${sourceOwner})` }; + return { + error: `存在未设置 label 的 level3 节点(父级:${sourceOwner})`, + }; } // 2.2.1 校验 level3 是否有子级(必须包含 level4) const level3ChildIds = this.getChildNodeIds(level3Node.id); const level4Nodes = level3ChildIds - .map(id => graph.getCellById(id) as Node) - .filter(node => node?.getData().level === 'level4'); + .map((id) => graph.getCellById(id) as Node) + .filter((node) => node?.getData().level === "level4"); if (level4Nodes.length === 0) { - return { error: `level3 节点「${tableName}」下无 level4 子节点,请完善配置` }; + return { + error: `level3 节点「${tableName}」下无 level4 子节点,请完善配置`, + }; } // 2.2.2 遍历每个 level4 节点(每个level3仅取第一个有效level4,避免重复) const validLevel4Node = level4Nodes[0]; - const columnName = validLevel4Node.attr('label/text') || ''; + const columnName = validLevel4Node.attr("label/text") || ""; if (!columnName) { - return { error: `存在未设置 label 的 level4 节点(父级:${tableName})` }; + return { + error: `存在未设置 label 的 level4 节点(父级:${tableName})`, + }; } // 2.2.3 组装单条数据 @@ -757,7 +794,10 @@ export class X6Graph { * 获取子节点ID列表 */ private getChildNodeIds(parentId: string): string[] { - return this.getGraph().getEdges().filter(edge => edge.getSourceCellId() === parentId).map(edge => edge.getTargetCellId()); + return this.getGraph() + .getEdges() + .filter((edge) => edge.getSourceCellId() === parentId) + .map((edge) => edge.getTargetCellId()); } /** @@ -772,9 +812,9 @@ export class X6Graph { */ clear(): void { this.getGraph().clearCells(); - this.levelCountMap.set('level2', 0); - this.levelCountMap.set('level3', 0); - this.levelCountMap.set('level4', 0); + this.levelCountMap.set("level2", 0); + this.levelCountMap.set("level3", 0); + this.levelCountMap.set("level4", 0); } /** @@ -794,7 +834,7 @@ export class X6Graph { */ private getGraph(): Graph { if (!this.graph) { - throw new Error('请先调用 init() 初始化画布'); + throw new Error("请先调用 init() 初始化画布"); } return this.graph; }