二话不说,先上效果图

前面已经写过基于X6的依赖关系图,对x6有了一定的了解。
官网ER图解析
还是老样子,先理解官网的er图实现
json
//er节点的数据格式:
{
"id": "1",
"shape": "er-rect", //自定义节点
"label": "学生",
"width": 150,
"height": 24,
"position": { //位置
"x": 24,
"y": 150
},
"ports": [ //连接桩 我理解的就是一行一行的字段需要一个连接点
{
"id": "1-1",
"group": "list",
"attrs": {
"portNameLabel": {
"text": "ID"
},
"portTypeLabel": {
"text": "STRING"
}
}
},
{
"id": "1-2",
"group": "list",
"attrs": {
"portNameLabel": {
"text": "Name"
},
"portTypeLabel": {
"text": "STRING"
}
}
}
]
}
//er图的边数据结构
{
"id": "4",
"shape": "edge",
"source": { //起点
"cell": "1", //起点所在的节点ID
"port": "1-1" // 所在的节点的某一行的ID
},
"target": {//终点
"cell": "2",/终点所在的节点ID
"port": "2-3" // 所在的节点的某一行的ID
},
"attrs": {
"line": { //边的样式配置
"stroke": "#A2B1C3",
"strokeWidth": 2
}
},
"zIndex": 0
},
官网ER图解析之后,每个节点有样式以及交互效果,使用 x6默认的描述 svg 标签的markup、attrs 等参数配置实现较为复杂,所以我们还是采取自定义 react 节点 实现。我们实现完一个节点之后,节点与节点的字段关系如何连接表示就需要再琢磨琢磨。
初始化画布
这个就直接上代码
yaml
export const initGraph = (container: HTMLDivElement) => {
return new Graph({
container: container!,
width: container?.clientWidth || 0,
height: container?.clientHeight || 0,
panning: true,
autoResize: true,
background: {
color: '#FFFFFF'
},
interacting: {
nodeMovable: false
},
mousewheel: {
enabled: true,
modifiers: ['ctrl', 'meta'],
minScale: 0.5,
maxScale: 2
}, // 允许鼠标滚轮缩放画布
connecting: {
router: {
// name: 'er',
name: ER_ROUTER,
args: {
offset: 25,
direction: 'H'
}
},
allowEdge: false,
allowPort: false,
allowBlank: false,
allowLoop: false,
allowMulti: false,
allowNode: false
}
});
};
自定义节点
ini
/**
* 自定义节点
*
*/
const registerERNode = () => {
register({
shape: 'custom-er-node',
component: node => {
const data = node.node.getData();
return <ERNode node={data} />;
}
});
return () => Graph.unregisterNode('custom-er-node');
};
ERNode组件实现
ERNode 就是实现整个node节点

typescript
import { Tooltip } from "antd";
import { ColumnProp } from "./interface";
import { LINE_HEIGHT } from "./constant";
import "./style.css";
const ERNode = (props: { node?: any }) => {
const { node } = props;
const styles = {
height: `${LINE_HEIGHT}px`,
lineHeight: `${LINE_HEIGHT}px`,
};
return (
<div className={`er-node`}>
<Tooltip title={node.tableName}>
<div
className={`er-node-title ${
node.tableERDependencyType === "current_table"
? "er-node-center"
: ""
}`}
>
{node.tableName}
</div>
</Tooltip>
<div className="er-column-list">
{node.columnList
? node.columnList.map((_column: ColumnProp) => (
<Tooltip
key={_column.columnName}
title={`${_column.columnName}, ${_column.columnType},${_column.comment}`}
>
<div className="er-column" style={{ ...styles }}>
<div className="er-info er-column-name">
{_column.columnName}
</div>
<div className="er-info er-type"> {_column.columnType}</div>
<div className="er-info er-type"> {_column.comment}</div>
</div>
</Tooltip>
))
: null}
</div>
</div>
);
};
export default ERNode;
注意⚠️:表名和字段列的高度都是设置LINE_HEIGHT, <math xmlns="http://www.w3.org/1998/Math/MathML"> 这个会影响连接桩的位置计算 \color{red}{这个会影响连接桩的位置计算} </math>这个会影响连接桩的位置计算。
连接桩位置计算
了解下官网对连接桩的连接桩的定义, 我理解的连接桩就是节点与节点每一行之间的连接点。所以需要在每一行设置连接点。
1、我们要在每个节点的每一行设置一个隐形的连接桩。 <math xmlns="http://www.w3.org/1998/Math/MathML"> 注意连接桩的 w i d t h 、 h e i g h t 需要跟自定义节点每一行设置的宽高一致 \color{red}{注意连接桩的width、height需要跟自定义节点每一行设置的宽高一致} </math>注意连接桩的width、height需要跟自定义节点每一行设置的宽高一致 ,这样才能完全"覆盖"。
2、有多少行设置多少个连接点。还要给每个连接桩设置 ID,以便后续的连线。 <math xmlns="http://www.w3.org/1998/Math/MathML"> 注意连接桩的位置使用的是属性 y , 这个 y 是绝对定位,而 R e f Y 是相对定位 \color{red}{注意连接桩的位置使用的是属性y,这个y是绝对定位,而RefY是相对定位} </math>注意连接桩的位置使用的是属性y,这个y是绝对定位,而RefY是相对定位。(被这个属性坑过,导致连接桩的位置一直不对)
3、如图,绿色的部分就是每一行的连接桩所在位置,但是此时连接桩的层级高于节点层级,因此需要设置z-index为0.

节点连接桩的设置代码如下:

arduino
import { NODE_WIDTH, LINE_HEIGHT, ER_ROUTER } from "./constant";
export const generatePorts = (id: string, attrs: Record<string, any>) => {
return {
id,
markup: [
{
tagName: "rect",
selector: "portBody",
},
],
args: {
position: "top",
},
attrs: {
portBody: {
width: NODE_WIDTH,//需要跟自定义节点每一行设置的一致
height: LINE_HEIGHT,//需要跟自定义节点每一行设置的一致
strokeWidth: 1,
stroke: "#EFF4FF",
fill: "#82a682",
magnet: false,
transform: "matrix(1,0,0,1,0,0)",
...attrs,
},
},
zIndex: 0, //隐藏在节点层级之后
};
};
生成节点
有了连接桩,也有自定义节点,就需要生成节点数据啦。被依赖节点和依赖节点竖直排放,就涉及到节点的位置计算。
注意点:
1、节点的总高度 === ( 字段行数+1 ) * LINE_HEIGHT
2、position的x:中心表在中间,中心表依赖的在左边。依赖中心表的在右边。(这个简单)
3、position的y:节点的起始位置 ==上一个节点的y + (上一个节点的字段行数 + 2) * LINE_HEIGHT。 每个节点的间距为一个LINE_HEIGHT,如图

typescript
/**
* rect的配置
*/
const getErRect = useCallback(
(tableInfoList: TableInfoProp[], args?: StoreKeyValue) => {
return tableInfoList?.reduce(
(prev: StoreKeyValue[], item: TableInfoProp, idx: number) => {
const rectConfig = {
id: item.tableLocation,
data: item,
shape: "custom-er-node",
width: NODE_WIDTH,
height: LINE_HEIGHT * (item.columnList.length + 1),
position: {
x:
item.tableERDependencyType === DEPENDENCY_ON ? LEFT_X : RIGHT_Y,
y:
idx === 0
? 0
: prev[idx - 1].position.y +
(tableInfoList[idx - 1].columnList.length + 2) *
LINE_HEIGHT,
},
ports: item.columnList.map((column: ColumnProp, index: number) => {
return generatePorts(`${column.columnLocation}`, {
id: `${column.columnLocation}`,
height: LINE_HEIGHT,
magnet: false,
y: LINE_HEIGHT * (index + 1), //y 是坐标点
// refY: LINE_HEIGHT * (index + 1), //refY是相对自身的偏移量
});
}),
...args,
};
prev.push(rectConfig);
return prev;
},
[],
);
},
[],
);
边的配置
边的配置就简单啦,就是根据需求设置文本(1:1、n:1、1:n)、连线边的头尾ID、头尾自定义箭头等。细节就不展示代码啦。
less
/**
* 边的配置
*/
const getErEdge = useCallback(() => {
if (graph)
return (
columnRelations.map((item: ColumnRelationProp) => {
const edgeConfig = {
id: `${item.currentColumnLocation}##${item.relatedColumnLocation}`,
// labels: getEdgesLabel(item.relation as EDGE_LABLE_KEYS),
labels: getEdgesLabel(item),//连线文本
...getEdgesTargetAndSource(item),//连线边的头尾ID
attrs: {
...getEdgesLine(item), //头尾自定义箭头
},
...EDGE_CONFIG,
};
return edgeConfig;
}) || []
);
}, [graph, tableInfoList, columnRelations]);
ER的生成
节点、边的配置、边和边的连线关系都有啦之后,就可以生成ER图啦
scss
/**
* 生成ER图 ER图手动布局
*/
useEffect(() => {
if (graph && tableInfoList.length) {
const { nodes = [], edges = [] } = getERNodes();
const cells: Cell[] = [];
nodes.forEach((item: any) => {
cells.push(graph.createNode(item));
});
edges.forEach((item: any) => {
cells.push(graph.createEdge(item));
});
setTimeout(() => {
graph.resetCells(cells);
graph.zoomToFit({ padding: 10, maxScale: 1 });
}, 20);
}
}, [tableInfoList, graph]);
连线交叉问题
以上生成的效果图如下:

连线什么都是无序的, 连线会交叉。那如何优化成有序,连续更加清晰点呢。 解决办法:根据中心表的字段排序依赖表和被依赖表,这样获取到的依赖表和被依赖是从上到下根据中心表的字段的依赖关系按顺序排列,这样连线就不会交叉啦。
ini
/**
*
* @returns 获取中心表、有序依赖表和有序被依赖表
* 依赖表和被依赖表:根据中心表的字段排序表,否则连续会交叉
*/
const getSortTables = () => {
//中心表
const _centerTable = tableInfoList.find(
(item) => item.tableERDependencyType === CURRENT_TABLE,
);
setCenterTableLocation(_centerTable?.tableLocation || "");
//依赖表和被依赖表
const tables = _centerTable
? _centerTable?.columnList.reduce(
(
prev: {
beRelateTable: TableInfoProp[];
relateTable: TableInfoProp[];
},
item: ColumnProp,
) => {
// 中心表依赖的字段:currentLocation是中心表
const _relateColumns = columnRelations.filter(
(_c) => _c.currentColumnLocation === item.columnLocation,
);
//依赖中心表的字段:relatedColumnLocation中心表
const _beRelateColumns = columnRelations.filter(
(_c) => _c.relatedColumnLocation === item.columnLocation,
);
//中心表依赖的字段的所在表
_relateColumns.forEach((_c: ColumnRelationProp) => {
for (let index = 0; index < tableInfoList.length; index++) {
const _table = tableInfoList[index];
if (
_table.columnList.find(
(_cc) => _cc.columnLocation === _c.relatedColumnLocation,
)
) {
prev.relateTable = uniqWith(
[...prev.relateTable, ...[_table]],
isEqual,
);
break;
}
}
});
//依赖中心表的字段的所在表
_beRelateColumns.forEach((_c: ColumnRelationProp) => {
for (let index = 0; index < tableInfoList.length; index++) {
const _table = tableInfoList[index];
if (
_table.columnList.find(
(_cc) => _cc.columnLocation === _c.currentColumnLocation,
)
) {
prev.beRelateTable = uniqWith(
[...prev.beRelateTable, ...[_table]],
isEqual,
);
break;
}
}
});
return prev;
},
{
beRelateTable: [] as TableInfoProp[], //依赖我的表
relateTable: [] as TableInfoProp[], //我依赖的表
},
)
: { beRelateTable: [], relateTable: [] };
return {
centerTable: _centerTable ? [_centerTable] : [],
...tables,
};
};
附完整代码:codepen