数据同步逻辑修改

This commit is contained in:
冯少康 2026-08-07 16:39:41 +08:00
parent 5c50d114b4
commit cedc6ca7a1
3 changed files with 835 additions and 437 deletions

View File

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

View File

@ -1,16 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { ElMessage, ElSelect } from 'element-plus'; import { ElMessage, ElSelect } from "element-plus";
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from "uuid";
import { useI18n } from 'vue-i18n'; import { useI18n } from "vue-i18n";
import { createTargetTables, getFileId, getMindMap, getTable, getUser, targetUser } from '@/utils/axios/sync/configTask/index'; import {
createTargetTables,
getFileId,
getMindMap,
getTable,
getUser,
targetUser,
} from "@/utils/axios/sync/configTask/index";
import { X6Graph } from './antvX6'; import { X6Graph } from "./antvX6";
// data props DataProps // data props DataProps
const props = defineProps<{ const props = defineProps<{
data: DataProps; // data data: DataProps; // data
}>(); }>();
const emit = defineEmits(['cancel']); const emit = defineEmits(["cancel"]);
// //
interface DataProps { interface DataProps {
taskId: string; taskId: string;
@ -35,17 +42,17 @@ const level3 = ref<InstanceType<typeof ElSelect> | null>(null);
const level4 = ref<InstanceType<typeof ElSelect> | null>(null); const level4 = ref<InstanceType<typeof ElSelect> | null>(null);
// //
const manualModeInput = ref(''); const manualModeInput = ref("");
// //
const formData = reactive({ const formData = reactive({
taskId: '', taskId: "",
sourceId: '', // mySql sourceId: "", // mySql
sourceName: '', sourceName: "",
mode: [], mode: [],
dataValue: [], dataValue: [],
field: '', field: "",
targetUserName: '', targetUserName: "",
}); });
// Id // Id
@ -56,7 +63,14 @@ const prevModeValues = ref<string[]>([]);
// () // ()
const prevDataValues = ref<string[]>([]); const prevDataValues = ref<string[]>([]);
// () // ()
const prevFieldValues = ref<string>(''); const prevFieldValues = ref<string>("");
//
const pztbslFlag = ref(false);
const pztbslValue = ref("");
// id
const nowPzTbslId = ref(null);
// //
const configDataValues = ref<string[]>([]); const configDataValues = ref<string[]>([]);
@ -64,7 +78,7 @@ const configDataValues = ref<string[]>([]);
// //
function initGraph() { function initGraph() {
// ID graph-container // ID graph-container
graph.value = new X6Graph('graphContainer').init({ grid: false, zoom: true }); graph.value = new X6Graph("graphContainer").init({ grid: false, zoom: true });
// 1. // 1.
const root = graph.value.addRootNode({ const root = graph.value.addRootNode({
@ -77,16 +91,15 @@ function initGraph() {
// //
function handleModeChange(newValues: string[]) { function handleModeChange(newValues: string[]) {
if (!graph.value) if (!graph.value) return;
return;
// prevModeValues // prevModeValues
const oldValues = prevModeValues.value; const oldValues = prevModeValues.value;
// 1. // 1.
const added = newValues.filter(v => !oldValues.includes(v)); const added = newValues.filter((v) => !oldValues.includes(v));
// 2. // 2.
const removed = oldValues.filter(v => !newValues.includes(v)); const removed = oldValues.filter((v) => !newValues.includes(v));
// //
added.forEach((value) => { added.forEach((value) => {
@ -95,7 +108,7 @@ function handleModeChange(newValues: string[]) {
graph.value?.addChildNode(formData.sourceId, { graph.value?.addChildNode(formData.sourceId, {
id: value, id: value,
label: item.label, label: item.label,
level: 'level2', level: "level2",
}); });
} }
}); });
@ -111,21 +124,20 @@ function handleModeChange(newValues: string[]) {
// 仿 handleModeChange // 仿 handleModeChange
function handleDataListChange(newValues: string[]) { function handleDataListChange(newValues: string[]) {
if (!graph.value) if (!graph.value) return;
return;
// prevDataValues // prevDataValues
const oldValues = prevDataValues.value; const oldValues = prevDataValues.value;
// 1. // 1.
const added = newValues.filter(v => !oldValues.includes(v)); const added = newValues.filter((v) => !oldValues.includes(v));
// 2. // 2.
const removed = oldValues.filter(v => !newValues.includes(v)); const removed = oldValues.filter((v) => !newValues.includes(v));
// level3 level2 // level3 level2
added.forEach((value) => { added.forEach((value) => {
// label // label
const item = dataList.value.find(item => item.value === value); const item = dataList.value.find((item) => item.value === value);
if (item) { if (item) {
// ID level2 level2 ID // ID level2 level2 ID
// ID // ID
@ -134,7 +146,7 @@ function handleDataListChange(newValues: string[]) {
graph.value?.addChildNode(parentNodeId, { graph.value?.addChildNode(parentNodeId, {
id: `${value}`, // ID id: `${value}`, // ID
label: item.label, label: item.label,
level: 'level3', // level3 level: "level3", // level3
}); });
} }
}); });
@ -152,11 +164,11 @@ function handleDataListChange(newValues: string[]) {
} }
// //
function handleFieldListChange(newValue: string) { // string[] string function handleFieldListChange(newValue: string) {
console.log('当前选中值:', newValue); // string[] string
console.log("当前选中值:", newValue);
if (!graph.value) if (!graph.value) return;
return;
// 1. prevFieldValues // 1. prevFieldValues
const oldValue = prevFieldValues.value; const oldValue = prevFieldValues.value;
@ -175,7 +187,7 @@ function handleFieldListChange(newValue: string) { // 注意:参数从 string[
// //
if (newValue) { if (newValue) {
// label // label
const item = fieldList.value.find(item => item.value === newValue); const item = fieldList.value.find((item) => item.value === newValue);
if (item) { if (item) {
// ID // ID
const parentNodeId = configData.value[2].id; const parentNodeId = configData.value[2].id;
@ -183,7 +195,7 @@ function handleFieldListChange(newValue: string) { // 注意:参数从 string[
graph.value.addChildNode(parentNodeId, { graph.value.addChildNode(parentNodeId, {
id: newValue, id: newValue,
label: item.label, label: item.label,
level: 'level4', // level4 level: "level4", // level4
}); });
} }
} }
@ -202,67 +214,130 @@ onMounted(() => {
// (/) // (/)
nextTick(() => { nextTick(() => {
getTargetUserName(formData.taskId); getTargetUserName(formData.taskId);
getMindMap({ getMindMap(
taskId: formData.taskId, {
}, (res) => { taskId: formData.taskId,
initGraph(); },
getModelList((value: any) => { (res) => {
console.log(value); initGraph();
if (res.result.userInfos.length) { getModelList((value: any) => {
console.log(res); console.log(value);
graph.value?.importData(res.result.userInfos, formData.sourceId); if (res.result.userInfos.length) {
res.result.userInfos.forEach((item: any) => { graph.value?.importData(res.result.userInfos, formData.sourceId);
modeList.value.forEach((mode: any) => {
if (mode.label === item.userName) { res.result.userInfos.forEach((item: any) => {
formData.mode.push(mode.value); const graphSl = graph.value?.getGraph();
//
const allNodes = graphSl.getNodes();
console.log(allNodes, "allNodes");
// label name item.name
const matchNodesData = allNodes
.map((node) => {
return {
label: node.label,
id: node.id,
};
})
.filter((item1) => {
return item1.label == item.userName;
});
const exists = modeList.value.find(
(item1: any) => item1.label === item.userName
);
if (!exists) {
const newItem = {
value: matchNodesData[0].id,
label: item.userName,
};
modeList.value.push(newItem);
formData.mode.push(newItem.value);
} else {
formData.mode.push(exists.value);
} }
}); });
}); }
} });
});
// mindMap.value = res.result; // mindMap.value = res.result;
}); }
);
}); });
// antvX6Event // antvX6Event
EventBusTool.getEventBus().subscribe('antvX6Event', (res: any) => { EventBusTool.getEventBus().subscribe("antvX6Event", (res: any) => {
// icon // icon
if (res.key === 'removeNode') { if (res.key === "removeNode") {
console.log(res.id); console.log(res.id);
if (res.level === 'level2') { if (res.level === "level2") {
// 3. // 3.
formData.mode = formData.mode.filter(v => v !== res.id); formData.mode = formData.mode.filter((v) => v !== res.id);
prevModeValues.value = prevModeValues.value.filter(v => v !== res.id); prevModeValues.value = prevModeValues.value.filter((v) => v !== res.id);
formData.dataValue = []; formData.dataValue = [];
formData.field = ''; formData.field = "";
} } else if (res.level === "level3") {
else if (res.level === 'level3') {
// 3. // 3.
formData.dataValue = formData.dataValue.filter(v => v !== res.id); formData.dataValue = formData.dataValue.filter((v) => v !== res.id);
prevDataValues.value = prevDataValues.value.filter(v => v !== res.id); prevDataValues.value = prevDataValues.value.filter((v) => v !== res.id);
formData.field = ''; formData.field = "";
} } else if (res.level === "level4") {
else if (res.level === 'level4') { formData.field = "";
formData.field = ''; prevFieldValues.value = "";
prevFieldValues.value = '';
} }
} }
if (res.key === 'configNode') { if (res.key === "configNode") {
configData.value = res.allParents; configData.value = res.allParents;
configDataValues.value = [...formData.dataValue, formData.field]; configDataValues.value = [...formData.dataValue, formData.field];
console.log(
configDataValues.value,
configData.value,
"2configData.value"
);
// //
if (res.level === 'level2') { if (res.level === "level2") {
getDataListById(res.label); getDataListById(res.label);
} }
if (res.level === 'level3') { if (res.level === "level3") {
getFieldById(res.label); if (res.isTbsl) {
const graphSl = graph.value?.getGraph();
const jdItem = graphSl.getCellById(res.id);
nowPzTbslId.value = res.id;
const oldSyncCount = jdItem.getData().syncCount;
pztbslFlag.value = true;
if (oldSyncCount) {
pztbslValue.value = oldSyncCount;
} else {
pztbslValue.value = "";
}
} else {
getFieldById(res.label);
}
} }
} }
}); });
}); });
function paTbslMethod() {
if (!pztbslValue.value) {
ElMessage({
type: "error",
message: `请配置同步数量`,
});
return;
}
const graphSl = graph.value?.getGraph();
const jdItem = graphSl.getCellById(nowPzTbslId.value);
jdItem.setData({ ...jdItem.getData(), syncCount:pztbslValue.value })
pztbslFlag.value = false;
pztbslValue.value = "";
nowPzTbslId.value = null;
}
// taskId // taskId
function getTargetUserName(taskId: any) { function getTargetUserName(taskId: any) {
targetUser({ taskId }, (res) => { targetUser({ taskId }, (res) => {
@ -299,6 +374,8 @@ function getDataListById(name: any) {
}; };
}); });
console.log(formData, "2141");
level3.value?.toggleMenu(); level3.value?.toggleMenu();
formData.dataValue = []; formData.dataValue = [];
}); });
@ -307,22 +384,29 @@ function getDataListById(name: any) {
function getFieldById(name: any) { function getFieldById(name: any) {
console.log(configData.value[1].label, name); console.log(configData.value[1].label, name);
getFileId({ sourceId: formData.sourceId, username: configData.value[1].label, tableName: name }, (res) => { getFileId(
fieldList.value = res.result.map((item: any) => { {
return { sourceId: formData.sourceId,
value: uuidv4(), username: configData.value[1].label,
label: item, tableName: name,
}; },
}); (res) => {
level4.value?.toggleMenu(); fieldList.value = res.result.map((item: any) => {
formData.field = ''; return {
}); value: uuidv4(),
label: item,
};
});
level4.value?.toggleMenu();
formData.field = "";
}
);
} }
// //
function handleAddMode() { function handleAddMode() {
const inputVal = manualModeInput.value.trim(); const inputVal = manualModeInput.value.trim();
if (!inputVal) { if (!inputVal) {
ElMessage({ type: 'warning', message: '请输入模式名称' }); ElMessage({ type: "warning", message: "请输入模式名称" });
return; return;
} }
// label // label
@ -332,97 +416,152 @@ function handleAddMode() {
if (!formData.mode.includes(exists.value)) { if (!formData.mode.includes(exists.value)) {
formData.mode.push(exists.value); formData.mode.push(exists.value);
handleModeChange([...formData.mode]); handleModeChange([...formData.mode]);
} else {
ElMessage({ type: "warning", message: "该模式已添加" });
} }
else { } else {
ElMessage({ type: 'warning', message: '该模式已添加' });
}
}
else {
// //
const newItem = { value: uuidv4(), label: inputVal }; const newItem = { value: uuidv4(), label: inputVal };
modeList.value.push(newItem); modeList.value.push(newItem);
formData.mode.push(newItem.value); formData.mode.push(newItem.value);
handleModeChange([...formData.mode]); handleModeChange([...formData.mode]);
} }
manualModeInput.value = ''; manualModeInput.value = "";
} }
// //
function handleSave() { function handleSave() {
if (!formData.targetUserName) { if (!formData.targetUserName) {
ElMessage({ ElMessage({
type: 'error', type: "error",
message: `请选择目标用户名`, message: `请选择目标用户名`,
}); });
return; return;
} }
const data = graph.value?.getTargetTableData(formData.taskId, formData.targetUserName); const data = graph.value?.getTargetTableData(
formData.taskId,
formData.targetUserName
);
console.log(data); console.log(data);
if (data) { if (data) {
createTargetTables(data, (res) => { createTargetTables(
console.log(res); data,
if (res === true) { (res) => {
ElMessage({ console.log(res);
type: 'success', if (res === true) {
message: `配置数据表成功`, ElMessage({
}); type: "success",
emit('cancel'); message: `配置数据表成功`,
} });
}, 30 * 60 * 1000); emit("cancel");
} }
else { },
30 * 60 * 1000
);
} else {
ElMessage({ ElMessage({
type: 'error', type: "error",
message: `配置有误,请检查`, message: `配置有误,请检查`,
}); });
} }
} }
// //
function handleCancel() { function handleCancel() {
emit('cancel'); emit("cancel");
} }
</script> </script>
<template> <template>
<div class="dataTable"> <div class="dataTable">
<ElForm :inline="true" :model="formData" class="custom-form" style="margin-bottom: 20px;"> <ElForm
:inline="true"
:model="formData"
class="custom-form"
style="margin-bottom: 20px"
>
<ElFormItem :label="t('database')"> <ElFormItem :label="t('database')">
<ElInput v-model="formData.sourceName" disabled style="width: 180px" /> <ElInput v-model="formData.sourceName" disabled style="width: 180px" />
</ElFormItem> </ElFormItem>
<ElFormItem :label="t('mode')"> <!-- <ElFormItem :label="t('mode')">
<ElSelect v-model="formData.mode" style="width: 220px" multiple :max-collapse-tags="0" collapse-tags <ElSelect v-model="formData.mode" style="width: 220px" multiple :max-collapse-tags="0" collapse-tags
collapse-tags-tooltip filterable :placeholder="t('pleaseSelect')" class="custom-select" collapse-tags-tooltip filterable :placeholder="t('pleaseSelect')" class="custom-select"
allow-create default-first-option :reserve-keyword="false" allow-create default-first-option :reserve-keyword="false"
@change="handleModeChange"> @change="handleModeChange">
<ElOption v-for="(item, index) in modeList" :key="index" :label="item.label" :value="item.value" /> <ElOption v-for="(item, index) in modeList" :key="index" :label="item.label" :value="item.value" />
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem> -->
<!--<ElFormItem label="手动输入模式" v-show="!formData.mode || !formData.mode.length"> <!-- <ElFormItem label="手动输入模式" v-show="!formData.mode || !formData.mode.length">
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<ElInput v-model="manualModeInput" style="width: 160px" placeholder="输入模式名" @keyup.enter="handleAddMode" /> <ElInput v-model="manualModeInput" style="width: 160px" placeholder="输入模式名" @keyup.enter="handleAddMode" />
<ElButton type="primary" style="margin-left: 4px; height: 34px; border-radius: 4px;" @click="handleAddMode"> <ElButton type="primary" style="margin-left: 4px; height: 34px; border-radius: 4px;" @click="handleAddMode">
添加 添加
</ElButton> </ElButton>
</div> </div>
</ElFormItem>--> </ElFormItem> -->
<ElFormItem :label="t('mode')">
<ElInput
v-model="manualModeInput"
style="width: 160px"
placeholder="输入模式名"
@keyup.enter="handleAddMode"
/>
</ElFormItem>
<ElFormItem :label="t('dataTable')"> <ElFormItem :label="t('dataTable')">
<ElSelect ref="level3" v-model="formData.dataValue" style="width: 220px" multiple :max-collapse-tags="0" <ElSelect
collapse-tags collapse-tags-tooltip filterable :placeholder="t('pleaseSelect')" class="custom-select" ref="level3"
@change="handleDataListChange"> v-model="formData.dataValue"
<ElOption v-for="(item, index) in dataList" :key="index" :label="item.label" :value="item.value" /> style="width: 220px"
multiple
:max-collapse-tags="0"
collapse-tags
collapse-tags-tooltip
filterable
:placeholder="t('pleaseSelect')"
class="custom-select"
@change="handleDataListChange"
>
<ElOption
v-for="(item, index) in dataList"
:key="index"
:label="item.label"
:value="item.value"
/>
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem :label="t('field')"> <ElFormItem :label="t('field')">
<ElSelect ref="level4" v-model="formData.field" style="width: 220px" filterable :placeholder="t('pleaseSelect')" <ElSelect
class="custom-select" @change="handleFieldListChange"> ref="level4"
<ElOption v-for="(item, index) in fieldList" :key="index" :label="item.label" :value="item.value" v-model="formData.field"
:disabled="formData.dataValue.length === 0" /> style="width: 220px"
filterable
:placeholder="t('pleaseSelect')"
class="custom-select"
@change="handleFieldListChange"
>
<ElOption
v-for="(item, index) in fieldList"
:key="index"
:label="item.label"
:value="item.value"
:disabled="formData.dataValue.length === 0"
/>
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
<ElFormItem :label="t('targetUserName')"> <ElFormItem :label="t('targetUserName')">
<ElSelect v-model="formData.targetUserName" style="width: 220px" filterable :placeholder="t('pleaseSelect')" <ElSelect
class="custom-select"> v-model="formData.targetUserName"
<ElOption v-for="(item, index) in targetUserNameList" :key="index" :label="item.label" :value="item.value" /> style="width: 220px"
filterable
:placeholder="t('pleaseSelect')"
class="custom-select"
>
<ElOption
v-for="(item, index) in targetUserNameList"
:key="index"
:label="item.label"
:value="item.value"
/>
</ElSelect> </ElSelect>
</ElFormItem> </ElFormItem>
</ElForm> </ElForm>
@ -432,13 +571,32 @@ function handleCancel() {
</div> </div>
<div class="btns"> <div class="btns">
<div class="save" @click="handleSave"> <div class="save" @click="handleSave">保存</div>
保存 <div class="close" @click="handleCancel">取消</div>
</div>
<div class="close" @click="handleCancel">
取消
</div>
</div> </div>
<Dialog
v-model="pztbslFlag"
title="配置同步数量"
width="20vw"
class="config-dialog"
>
<el-input
v-model="pztbslValue"
style="width: 240px"
type="number"
placeholder="请配置同步数量"
/>
<template #footer>
<ElButton
type="text"
style="color: #fff; cursor: no-drop"
@click="paTbslMethod"
>
配置
</ElButton>
</template>
</Dialog>
</div> </div>
</template> </template>
@ -447,6 +605,14 @@ function handleCancel() {
width: 100%; width: 100%;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
position: relative;
::v-deep .config-dialog {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.custom-form { .custom-form {
margin-bottom: 20px; margin-bottom: 20px;

View File

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