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"; // 目标灌库数据的类型定义 interface ImportDataItem { userName: string; // 对应 level2 节点的 label tables: Array<{ tableName: string; // 对应 level3 节点的 label columnName: string; // 对应 level4 节点的 label }>; } // 子节点配置接口 interface ChildNodeConfig { id: string; label: string; level: NodeLevel; x?: number; businessId?: string; } // 根节点配置接口 interface RootNodeConfig { id: string; label: string; businessId?: string; x?: number; } // 普通节点配置(内部使用) interface SimpleNodeConfig { id: string; x: number; y: number; label: string; level?: NodeLevel; businessId?: string; } export class X6Graph { private graph: Graph | null = null; private containerId: string; private menuElement: HTMLDivElement | null = null; // 右键菜单 private readonly nodePadding = 20; // 节点样式(原生rect节点,确保可见) private readonly nodeStyle = { width: 100, height: 40, fill: "#0a4a5c", // 深色背景(示例中的深青色) stroke: "#0a4a5c", // 边框与背景同色,视觉上隐藏边框 strokeWidth: 2, radius: 4, label: { fill: "#93CDD5", // 白色文字 fontSize: 14, fontWeight: "bold", // 可选:文字加粗 }, }; // 固定尺寸 private readonly nodeSize = { width: 200, height: 40, }; // 层级X坐标映射 private readonly levelXMap = new Map([ ["level2", 300], ["level3", 550], ["level4", 1000], ]); // 层级节点数量记录 private readonly levelCountMap = new Map([ ["level2", 0], ["level3", 0], ["level4", 0], ]); constructor(containerId: string) { this.containerId = containerId; this.createMenuElement(); // 初始化右键菜单 } private createMenuElement() { 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; background: white; border: 1px solid #ccc; border-radius: 4px; padding: 5px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 1000; display: none; font-family: sans-serif; `; // 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; /* 分隔线,区分两个选项 */ `; 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; 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) }); } this.hideMenu(); }); // 配置菜单悬停效果 pzItem.addEventListener("mouseenter", () => { pzItem.style.background = "#f0f0f0"; }); 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. 配置同步数量 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"); if (nodeId) { const graph = this.getGraph(); const targetNode = graph.getCellById(nodeId) as Node; const nodeLevel = targetNode.getData().level; // 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("mouseleave", () => { syncNumItem.style.background = "white"; }); // 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"); if (nodeId) { // 获取图形实例和目标节点(和配置项逻辑一致) const graph = this.getGraph(); const targetNode = graph.getCellById(nodeId) as Node; // 从节点数据中提取level const nodeLevel = targetNode.getData().level; this.removeNode(nodeId); EventBusTool.getEventBus().publish("antvX6Event", { key: "removeNode", id: nodeId, level: nodeLevel, // 新增:传递level信息 }); } this.hideMenu(); }); // 删除项悬停效果 deleteItem.addEventListener("mouseenter", () => { deleteItem.style.background = "#f0f0f0"; }); deleteItem.addEventListener("mouseleave", () => { deleteItem.style.background = "white"; }); // 3. 将两个菜单项添加到菜单容器(配置项在上,删除项在下) this.menuElement.appendChild(pzItem); this.menuElement.appendChild(configItem); this.menuElement.appendChild(syncNumItem); this.menuElement.appendChild(deleteItem); document.body.appendChild(this.menuElement); } /** * 显示右键菜单 */ private showMenu(nodeId: string, x: number, y: number) { 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"; } /** * 隐藏右键菜单 */ private hideMenu() { if (this.menuElement) { this.menuElement.style.display = "none"; } } /** * 初始化画布(使用原生rect节点) */ init(options: Partial<{ grid: boolean; zoom: boolean }> = {}): this { const container = document.getElementById(this.containerId); if (!container) { throw new Error(`画布容器 "${this.containerId}" 不存在`); } // 初始化画布 this.graph = new Graph({ container, width: container.clientWidth, height: container.clientHeight, grid: options.grid ?? { size: 10, visible: false, type: "dot" }, mousewheel: options.zoom ?? { enabled: true, modifiers: "ctrl", minScale: 0.5, maxScale: 2, }, panning: { enabled: true, modifier: "shift" }, snapline: true, // 关键:完善选择配置,确保单击和框选生效 selecting: { enabled: true, // 开启选择交互 }, }); // 注册“直接曲线虚线”自定义边 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, // 层级(避免遮挡节点) }, true, ); // 绑定右键菜单事件 this.bindRightClickEvent(); return this; } /** * 绑定右键点击事件 */ private bindRightClickEvent() { const graph = this.getGraph(); // 节点右键点击 graph.on("node:contextmenu", (args) => { args.e.preventDefault(); // 阻止浏览器默认右键菜单 const { cell, e } = args; const node = cell as Node; const nodeData = node.getData(); // 获取节点数据 console.log(nodeData); // 关键判断:如果节点无 level(根节点),直接返回不显示菜单 if (!nodeData.level) { return; } 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) => { args.e.preventDefault(); this.hideMenu(); }); // 点击其他区域隐藏菜单 document.addEventListener("click", () => { this.hideMenu(); }); } /** * 计算文字宽度(用于自适应节点宽度) */ private calculateTextWidth(text: string, fontSize = 14): number { // 创建临时元素计算文字宽度 const tempSpan = document.createElement("span"); tempSpan.style.cssText = ` position: absolute; visibility: hidden; white-space: nowrap; font-size: ${fontSize}px; font-weight: bold; font-family: sans-serif; `; tempSpan.textContent = text; document.body.appendChild(tempSpan); const width = tempSpan.offsetWidth; document.body.removeChild(tempSpan); return width; } /** * 添加根节点(原生rect节点) */ addRootNode(config: RootNodeConfig): Node { const graph = this.getGraph(); const containerHeight = graph.container.clientHeight; const centerY = (containerHeight - this.nodeStyle.height) / 2; const rootX = config.x ?? 50; console.log(config.label); return this.addNode({ id: config.id, x: rootX, y: centerY, label: config.label, businessId: config.businessId, }); } /** * 添加普通节点 */ private addNode(config: SimpleNodeConfig): Node { const graph = this.getGraph(); // 计算根节点自适应宽度 const rootWidth = this.calculateTextWidth(config.label, this.nodeStyle.label.fontSize) + this.nodePadding * 2; const node = graph.addNode({ shape: "rect", id: config.id, x: config.x, y: config.y, width: rootWidth, height: this.nodeSize.height, label: config.label, attrs: { // 矩形主体的样式(背景、边框、圆角等) body: { fill: this.nodeStyle.fill, // 深青色背景 stroke: this.nodeStyle.stroke, // 边框与背景同色(视觉隐藏) strokeWidth: 1, rx: 20, // 圆角 ry: 20, }, // 文字的样式(颜色、位置、字号等) label: { text: config.label, fill: "#ffffff", // 白色文字 fontSize: 14, fontWeight: "bold", textAnchor: "middle", // 文字水平居中 verticalAnchor: "middle", // 文字垂直居中 }, }, data: { businessId: config.businessId, level: config.level, }, }); if (!graph.isNode(node)) { throw new Error(`创建节点 "${config.id}" 失败`); } return node as Node; } /** * 添加子节点(按层级动态分配位置) */ addChildNode( parentId: string, config: ChildNodeConfig, skipRearrange = false, ): { node: Node; edge: Edge } { const graph = this.getGraph(); // 校验父节点 const parentCell = graph.getCellById(parentId); if (!parentCell || !graph.isNode(parentCell)) { throw new Error(`父节点 "${parentId}" 不存在或无效`); } // 确定X位置 const levelX = this.levelXMap.get(config.level) ?? 300; const nodeX = config.x ?? levelX; // 更新层级节点数量 const currentCount = this.levelCountMap.get(config.level) || 0; const totalNodesAfterAdd = currentCount + 1; this.levelCountMap.set(config.level, totalNodesAfterAdd); // 创建子节点(临时位置,后续重排) const childNode = this.addNode({ ...config, x: nodeX, y: 0, // 临时Y值 }); // 创建连线 const edge = graph.addEdge({ shape: "direct-curved-dashed-edge", source: { cell: parentId, anchor: { name: "right" }, }, // 目标节点(子节点)的锚点:左侧 target: { cell: childNode.id, anchor: { name: "left" }, }, style: { stroke: "#888", strokeWidth: 1.2, targetMarker: { name: "block", size: 8 }, }, }); if (!graph.isEdge(edge)) { throw new Error(`创建连线(${parentId}→${childNode.id})失败`); } // // 重排同层级节点 // this.rearrangeLevelNodes(config.level); // // 新增:如果添加的是第三层级,同步重排第二层级 // if (config.level === "level3") { // this.rearrangeLevelNodes("level2"); // } if (!skipRearrange) { this.rearrangeLevelNodes(config.level); // 新增:如果添加的是第三层级,同步重排第二层级 if (config.level === "level3") { this.rearrangeLevelNodes("level2"); } // 如果新增level4,要重排level3(父层级),父节点位置可能变化 if (config.level === "level4") { this.rearrangeLevelNodes("level3"); } } return { node: childNode, edge: edge as Edge }; } /** * 重排指定层级的节点(垂直分布,第三层级跟随父节点,第二层级间距动态适配子节点数) */ private rearrangeLevelNodes(level: NodeLevel): void { const graph = this.getGraph(); const containerHeight = graph.container.clientHeight; const centerY = (containerHeight - this.nodeStyle.height) / 2; 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") || ""; return labelA.localeCompare(labelB, undefined, { numeric: true }); }); const totalNodes = levelNodes.length; if (totalNodes === 0) return; if (level === "level2") { 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"; }).length; level2ChildCount.set(node.id, level3ChildCount); }); const baseGap = 100; const factor = 30; let currentY = centerY; const nodePositions = new Map(); const firstNode = levelNodes[0]; nodePositions.set(firstNode.id, currentY); for (let i = 1; i < totalNodes; i++) { const prevNode = levelNodes[i - 1]; const prevChildCount = level2ChildCount.get(prevNode.id) || 0; const dynamicGap = baseGap + prevChildCount * factor; currentY += this.nodeStyle.height + dynamicGap; nodePositions.set(levelNodes[i].id, currentY); } // ✅修复括号优先级 const firstY = nodePositions.get(levelNodes[0].id) ?? 0; const lastNodeY = nodePositions.get(levelNodes[totalNodes - 1].id) ?? 0; const totalHeight = lastNodeY - firstY + this.nodeStyle.height; const offset = centerY - firstY - totalHeight / 2; levelNodes.forEach((node) => { const nodeY = (nodePositions.get(node.id) || 0) + offset; const nodeX = this.levelXMap.get("level2") || 300; node.setPosition(nodeX, nodeY); }); } else if (level === "level3") { const parentGroups = new Map(); levelNodes.forEach((node) => { const parentEdge = graph .getEdges() .find((edge) => edge.getTargetCellId() === node.id); if (parentEdge) { const parentId = parentEdge.getSourceCellId(); parentGroups.set(parentId, [ ...(parentGroups.get(parentId) || []), node, ]); } }); parentGroups.forEach((children, parentId) => { const parentNode = graph.getCellById(parentId) as Node; 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 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; child.setPosition(childX, childY); }); }); } else if (level === "level4") { const levelNodes = this.graph.getCells().filter((cell) => { return cell.isNode() && cell.getData()?.level === level; }) as Node[]; if (!levelNodes.length) return; const allEdges = this.graph.getEdges(); //按照父节点Y排序,保证顺序和左侧level3从上到下保持一致 levelNodes.sort((a, b) => { let ay = 0, by = 0; for (const e of allEdges) { if (e.getTargetCellId() === a.id) { const p = this.graph.getCellById(e.getSourceCellId()) as Node; ay = p.getPosition().y; } if (e.getTargetCellId() === b.id) { const p = this.graph.getCellById(e.getSourceCellId()) as Node; by = p.getPosition().y; } } return ay - by; }); const nodeH = this.nodeStyle.height; const verticalGap = 20; const totalHeight = levelNodes.length * nodeH + (levelNodes.length - 1) * verticalGap; //参考level3整体的中心点 const level3Nodes = this.graph .getCells() .filter((c) => c.isNode() && c.getData()?.level === "level3") as Node[]; let refCenterY = 300; if (level3Nodes.length > 0) { const topY = Math.min(...level3Nodes.map((n) => n.getPosition().y)); const botY = Math.max(...level3Nodes.map((n) => n.getPosition().y)) + nodeH; refCenterY = (topY + botY) / 2; } const startY = refCenterY - totalHeight / 2; const level4X = this.levelXMap.get("level4") || 800; let currentY = startY; levelNodes.forEach((node) => { node.setPosition(level4X, currentY); currentY += nodeH + verticalGap; }); } } /** * 删除节点(递归删除子节点并重新排列) */ removeNode(nodeId: string): void { const graph = this.getGraph(); const cell = graph.getCellById(nodeId); if (!cell || !graph.isNode(cell)) return; // if (targetNode.getData().tbslFlag) { // // 1、拿到被删节点的直接父节点 // const inEdges = graph.getIncomingEdges(graph.getCellById(nodeId)); // inEdges[0].setData({ ...inEdges[0].getData(), syncCount: null }); // inEdges[0].setAttrs({ // label: { // text: 22222, // }, // }); // } if (cell.data.tbslFlag) { graph.getEdges().forEach((edge) => { const sourceNode = edge.getSourceNode(); const targetNode = edge.getTargetNode(); if (targetNode.id == cell.id) { sourceNode.setData({ syncCount: null }); } }); const inEdges = graph.getIncomingEdges(cell); inEdges[0].setData({ ...inEdges[0].getData(), syncCount: null }); inEdges[0].setAttrs({ label: { text: 22222, }, }); } const node = cell as Node; const nodeLevel = node.getData().level as NodeLevel; // 递归删除子节点 this.getChildNodeIds(nodeId).forEach((childId) => { this.removeNode(childId); }); // 删除当前节点 node.remove(); // 重置数量并重新排列 if (nodeLevel) { this.resetLevelCount(nodeLevel); this.rearrangeLevelNodes(nodeLevel); } } /** * 灌库方法:将目标数据按层级创建到画布(根节点→level2→level3→level4) * @param importData - 目标灌库数据(格式如 [{userName: "...", tables: [...]}]) * @param rootConfig - 根节点配置(需提前确定根节点的 id 和 label,如 sourceOwner) */ importData(importData: ImportDataItem[], rootId): void { // 3. 遍历灌库数据,创建 level2→level3→level4 层级 importData.forEach((item) => { // 3.1 创建 level2 节点(userName 作为 label,父节点是根节点) const { node: level2Node } = this.addChildNode( rootId, { id: uuidv4(), label: item.userName, level: "level2", }, true, ); // 3.2 遍历当前 level2 的 tables,创建 level3 和 level4 节点 item.tables.forEach((table) => { // 3.2.1 创建 level3 节点(tableName 作为 label,父节点是当前 level2) const { node: level3Node } = this.addChildNode( level2Node.id, { id: uuidv4(), label: table.tableName, level: "level3", }, true, ); // 3.2.2 创建 level4 节点(columnName 作为 label,父节点是当前 level3) this.addChildNode( level3Node.id, { id: uuidv4(), label: table.columnName, level: "level4", }, true, ); if (table.syncCount) { level3Node.data.syncCount = table.syncCount; console.log(level3Node, "3333"); this.addChildNode(level3Node.id, { id: uuidv4(), label: table.syncCount.toString(), level: "level4", }); } }, true); }); // // 全部节点循环创建完成后,统一执行全套重排 this.rearrangeLevelNodes("level2"); this.rearrangeLevelNodes("level3"); this.rearrangeLevelNodes("level4"); this.graph.zoomToFit({ padding: 40, // 四周留白,节点不会贴边框 maxZoom: 1, // 最大缩放比例,不要放太大;数据多时会缩小 minZoom: 0.3, }); } /** * 获取目标表格数据(按 level2→level3→level4 层级组装,含子级完整性校验) * @param taskId - 传入的任务ID * @param name - 传入的targetOwner字段值 * @returns 符合格式的目标表格数组 / 错误信息 */ 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"); if (level2Nodes.length === 0) { return { error: "未找到任何 level2 节点,无法组装数据" }; } // 2. 遍历每个 level2 节点,校验子级并组装数据 for (const level2Node of level2Nodes) { const sourceOwner = level2Node.attr("label/text") || ""; if (!sourceOwner) { 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"); if (level3Nodes.length === 0) { return { error: `level2 节点「${sourceOwner}」下无 level3 子节点,请完善配置`, }; } // 2.2 遍历每个 level3 节点,校验子级并组装数据 for (const level3Node of level3Nodes) { const tableName = level3Node.attr("label/text") || ""; if (!tableName) { 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"); if (level4Nodes.length === 0) { return { error: `level3 节点「${tableName}」下无 level4 子节点,请完善配置`, }; } // 2.2.2 遍历每个 level4 节点(每个level3仅取第一个有效level4,避免重复) const validLevel4Node = level4Nodes[0]; const columnName = validLevel4Node.attr("label/text") || ""; if (!columnName) { return { error: `存在未设置 label 的 level4 节点(父级:${tableName})`, }; } // 2.2.3 组装单条数据 result.push({ taskId, sourceOwner, targetOwner: name, // 传入的name字段作为targetOwner tableName, columnName, syncCount, }); } } // 3. 数据组装完成,返回结果 return result.length > 0 ? result : null; } /** * 重置层级节点数量 */ private resetLevelCount(level: NodeLevel): void { const graph = this.getGraph(); const actualCount = graph.getNodes().filter((node) => { return node.getData().level === level; }).length; this.levelCountMap.set(level, actualCount); } /** * 获取子节点ID列表 */ private getChildNodeIds(parentId: string): string[] { return this.getGraph() .getEdges() .filter((edge) => edge.getSourceCellId() === parentId) .map((edge) => edge.getTargetCellId()); } /** * 适配视图 */ zoomToFitView(): void { this.getGraph().zoomToFit({ padding: 50 }); } /** * 清空画布 */ clear(): void { this.getGraph().clearCells(); this.levelCountMap.set("level2", 0); this.levelCountMap.set("level3", 0); this.levelCountMap.set("level4", 0); } /** * 销毁画布 */ destroy(): void { this.graph?.destroy(); this.graph = null; // 移除右键菜单 if (this.menuElement && this.menuElement.parentElement) { this.menuElement.parentElement.removeChild(this.menuElement); } } /** * 获取画布实例 */ private getGraph(): Graph { if (!this.graph) { throw new Error("请先调用 init() 初始化画布"); } return this.graph; } }