x6实现泳道图流程
# Graph
# 初始化
# 滚动处理
this.baseGraph = new BaseGraph({
container: this.graphContainer,
grid: true,
// mousewheel: {//先屏蔽,目前会影响到流程图移动;
// enabled: true,
// // zoomAtMousePosition: false,
// // factor: 1.1,
// modifiers: ['ctrl', 'meta'],
// minScale: 0.5,
// maxScale: 2,
// },
scroller: {
enabled: true,
pageVisible: true, // 是否分页
pageBreak: true, // 是否显示分页符
pannable: true,
// autoResize: false,
minVisibleWidth: 0,
minVisibleHeight: 0,
},
...this.getContainerSize(),
autoResize: true,
});
this.graph = this.baseGraph.graph;
constructor(graphOptions: GraphOptions) {
const defaultCfg = this.getDefaultCfg();
let offset;
if (this.graph) {
offset = this.graph.getScrollbarPosition();
this.graph.dispose();
}
const graph = new Graph({
...defaultCfg,
...graphOptions,
translating: {
restrict(cellView: CellView) {
if (cellView) {
const cell = cellView.cell as Node;
const parentId = cell.prop('parent');
if (parentId) {
const parentNode = graph.getCellById(parentId) as Node;
if (parentNode) {
return parentNode.getBBox().moveAndExpand({
x: 0,
y: 30,
width: 0,
height: -30,
});
}
}
return cell.getBBox();
}
return null;
},
},
});
if (offset) {
graph.setScrollbarPosition(offset.left, offset.top);
}
this.graph = graph;
this.init();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# 约束在泳道内
translating: {
restrict(cellView: CellView) {
if (cellView) {
const cell = cellView.cell as Node;
const parentId = cell.prop('parent');
if (parentId) {
const parentNode = graph.getCellById(parentId) as Node;
if (parentNode) {
return parentNode.getBBox().moveAndExpand({
x: 0,
y: 30,
width: 0,
height: -30,
});
}
}
return cell.getBBox();
}
return null;
},
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 连线约束
connecting: {
snap: true, //当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附,可以通过配置 radius 属性自定义触发吸附的距离。当 snap 设置为 false 时不会触发自动吸附。默认值为 false。
allowBlank: false,
allowMulti: false,
allowLoop: false,
highlight: true,
anchor: 'center', //当连接到节点时,通过 anchor 来指定被连接的节点的锚点,默认值为 center。
connector: 'rounded',
connectionPoint: 'boundary',
router: {
name: 'er',
args: {
direction: 'V',
},
},
createEdge() {
return new Shape.Edge({
...edgeCfg,
});
},
validateConnection({ sourceView, targetView, sourceMagnet, targetMagnet }) {
if (sourceView === targetView) {
return false;
}
if (!sourceMagnet) {
return false;
}
if (!targetMagnet) {
return false;
}
return true;
},
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 初始化节点和边
详见下面中的节点及边的处理;
# 桩点处理
初始化泳道图节点时,不设置ports;其他节点要设置; 用于连接两节点连线用;
this.graph.on('node:mouseenter', ({ node, view }) => {
const ports = this.graphContainer.querySelectorAll('.x6-port-body') as NodeListOf<SVGAElement>;
if (!node.parent) return;
this.showPorts(ports, true);
});
this.graph.on('node:mouseleave', ({ node }) => {
const ports = this.graphContainer.querySelectorAll('.x6-port-body') as NodeListOf<SVGAElement>;
this.showPorts(ports, false);
});
showPorts(ports: NodeListOf<SVGAElement>, show: boolean) {
for (let i = 0, len = ports.length; i < len; i = i + 1) {
ports[i].style.visibility = show ? 'visible' : 'hidden';
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Node/Edge数据组合
# 外部数据同步回调处理
// 设置填充数据, 通过edge判断类型;
setGraphData = cells => {
cells.forEach(item => {
const { id, shape, data, position } = item;
if (checkEdge(shape)) {
const { source, target } = item;
const info = {
id,
shape: 'myedge',
source,
target,
data: { ...data, isInit: true },
};
this.upateGraphData(false, 'add', info);
} else {
const info = {
id,
shape,
position: { ...position },
parent: data.parent,
data: { ...data, isInit: true },
};
this.upateGraphData(true, 'add', info);
}
});
};
upateGraphData = (isNode, type = 'add', data) => {
const { id } = data;
if (isNode) {
const index = this.nodeList.findIndex(item => item.id === id);
if (type === 'add') {
if (index === -1) {
this.nodeList.splice(this.nodeList.length, 0, data);
} else {
this.nodeList.splice(index, 1, data);
}
} else if (type === 'del') {
this.nodeList.splice(index, 1);
}
} else {
const index = this.edgeList.findIndex(item => item.id === id);
if (type === 'add') {
if (index === -1) {
this.edgeList.splice(this.edgeList.length, 0, data);
} else {
this.edgeList.splice(index, 1, data);
}
} else if (type === 'del') {
this.edgeList.splice(index, 1);
}
}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Node结点数据组合
有两种数据来源,目前通过isInit
标志判断,true表示为原始数据,false为手动生成的新数据;
- 原有数据回显示处理;
- 右边算子拖动生成的数据;这种数据用手动加入到自己维护的数组中;
原有数据回显示处理
注意这里的坐标重算机制,自动默认算在中间位置;
public addNode<T extends NodeConfig>(nodeData: T) {
const { id, position: { x = 0, y = 0 } = {}, shape, parent, name, isInit, data, ...rest } = nodeData;
let myProps = {};
if (checkSL(shape)) {
//目前泳道时原生类型;其实这个可以单独处理这个泳道的
myProps = {
position: { x, y },
label: name,
parent,
};
} else {
//算子时自定义的
let xp = 0,
xm = x;
// 通过老数据回显的情况下【拖动生成的数据】,parent是在data中
let parentN = parent || (data && data.parent) || '';
if (parentN) {
// parentNode = this.graph.findViewByCell(parent)
const parentNode = this.findNodeById(parentN);
if (parentNode) {
xp = parentNode.getData().x;
xm = xp + this.swimLaneW / 2 - cellTitleW / 2;
}
}
myProps = {
x: xm, //默认在中间
y,
ports, //要有桩点
parent: parentN,
};
}
const newNode: Node = this.graph.addNode({
id,
shape,
...rest,
...myProps,
data: {
//保存下坐标信息,用于查找孩子及父亲时用到数据;
...data,
shape,
id,
x,
y,
name: name || data.name,
isInit,
},
});
this.nodes.push(newNode);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
右边算子拖动生成的数据
链接时,做必要的数据验证及标识处理;
- isInit 设置为false;
- 更新坐标位置;
validateNode(droppingNode, options) {
let { x, y } = droppingNode.getPosition();
let xp = 0,
xm = x;
if (droppingNode.getParentId()) {
const parentNode = baseGraph.cellController.findNodeById(droppingNode.getParentId());
if (parentNode) {
xp = parentNode.getData().x;
xm = xp + swimLaneW / 2 - cellTitleW / 2;
}
}
if (xm > 0) {
const data = droppingNode.getData();
droppingNode.setData({ ...data, x: xm, y, id: droppingNode.id, isInit: false });
droppingNode.setPosition(xm, y);
}
return true;
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
事件监听处理
this.graph.on('node:added', ({ node, index, options }) => {
if (!node.getData.isInit) {
//拖动添加的节点要同步更新到自己维护的内部数组中;
this.baseGraph.cellController.nodes.push(node);
}
if (this.props.nodeCb) this.props.nodeCb('add', this.assemblNodeData(node));
});
this.graph.on('node:removed', ({ node, index, options }) => {
// 删除时,这里面的节点可拿到xy信息;已经没有意义了;
if (this.props.nodeCb) this.props.nodeCb('del', node);
});
private assemblNodeData(node) {
const { id, shape, data, name } = node;
return {
id,
shape,
position: node.getPosition(),
parent: node.getParentId(),
data: {
//保存下坐标信息,用于查找孩子及父亲时用到数据;
...data,
shape,
id,
name: name || data.name,
properties: data.properties || [],
parent: node.getParentId(),
},
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Edge边数据组合
有两种数据来源,目前通过isInit
标志判断,true表示为原始数据,false为手动生成的新数据;
- 原有数据回显示处理;
- 手动算子拖动生成的数据;这种数据用手动加入到自己维护的数组中;边数据监听回调,同步外部数据;定要注意
edge:connected
的监听处理新节点数据isNew;
原有数据回显示处理
public addEdge<T extends EdgeConfig>(edgeData: T) {
const { id, source, target, data, isInit, ...rest } = edgeData;
// const { id, source, target } = data;
if (!source || !target) {
throw new Error('edge must has source and target!');
}
const newEdge: Edge = this.graph.addEdge({
id: id ? id : `${source}-${target}`,
data: { ...data, isInit },
source,
target,
shape: 'myedge',
isInit,
...rest,
});
this.edges.push(newEdge);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
手动算子拖动生成的数据
this.graph.on('edge:added', ({ edge, index, options }) => {
if (this.props.edgeCb) this.props.edgeCb('add', this.assemblEdgeData(edge));
});
this.graph.on('edge:connected', ({ isNew, edge }) => {
if (isNew && this.props.edgeCb) {
this.props.edgeCb('add', this.assemblEdgeData(edge, true));
}
});
this.graph.on('edge:removed', ({ edge, index, options }) => {
if (this.props.edgeCb) this.props.edgeCb('del', edge);
});
private assemblEdgeData(edge, isNew = false) {
const { id, data } = edge;
return {
id,
shape: 'myedge',
source: edge.getSource(),
target: edge.getTarget(),
data: {
...data,
isInit: isNew ? false : true,
},
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 数据更新回显
- 数据data处理;
- 页面更新;
- 标识是否是新旧节点;
// 设置填充数据, 通过edge判断类型;
setGraphData = cells => {
// console.log('-----setGraphData----11111111--', cells);
cells.forEach(item => {
const { id, shape, data, position } = item;
if (checkEdge(shape)) {
const { source, target } = item;
const info = {
id,
shape: 'myedge',
source,
target,
data: { ...data, isInit: true },
};
this.upateGraphData(false, 'add', info);
} else {
const info = {
id,
shape,
position: { ...position },
parent: data.parent,
data: { ...data, isInit: true },
};
this.upateGraphData(true, 'add', info);
}
});
};
calRenderData = (nodeList, edgeList) => {
const {
development: { dataModel },
} = this.props;
const nodes = nodeList.map(entity => {
return {
...entity,
data: {
...entity.data,
isVerify: dataModel[entity.id] ? true : false, // 是否验证体检过
},
};
});
const edges = edgeList.map(relation => {
return {
...relation,
data: relation.data,
};
});
return { nodes, edges };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 泳道图
# 动态计算泳道的宽高
initData = () => {
const { width } = this.getContainerSize();
let swimLaneW1 = (width - 40) / 4;
swimLaneW1 = swimLaneW1 < cellTitleW ? cellTitleW + 25 : swimLaneW1;
let swimLaneH = (document.body.offsetHeight - 105) * 4;
this.swimLaneW = swimLaneW1;
this.swimLaneData = {
nodes: [
{
id: nodeTypes.sl1,
width: swimLaneW1,
height: swimLaneH,
name: '数据源',
position: {
x: startSwimLane,
y: 10,
},
shape: nodeTypes.sl1,
zIndex: -100,
},
{
id: nodeTypes.sl2,
width: swimLaneW1,
height: swimLaneH,
name: '解析逻辑',
position: {
x: startSwimLane + swimLaneW1,
y: 10,
},
shape: nodeTypes.sl2,
zIndex: -100,
},
{
id: nodeTypes.sl3,
width: swimLaneW1,
height: swimLaneH,
name: '处理规则',
position: {
x: startSwimLane + swimLaneW1 * 2,
y: 10,
},
shape: nodeTypes.sl3,
zIndex: -100,
},
{
id: nodeTypes.sl4,
width: swimLaneW1,
height: swimLaneH,
name: '目标源',
position: {
x: startSwimLane + swimLaneW1 * 3,
y: 10,
},
shape: nodeTypes.sl4,
zIndex: -100,
},
],
edges: [],
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# 4泳道图背景
registerNodeLane(this.swimLaneW); //这个要比node在前面处理,后面节点关系
this.baseGraph.updateGraph(this.swimLaneData, this.swimLaneW);
export function registerNodeLane(swimLaneW) {
let swimLanes = [
{
type: nodeTypes.sl1,
borderColor: '#39CDCC',
iconBgColor: '#E6FFFB',
},
{
type: nodeTypes.sl2,
borderColor: '#029DFF',
iconBgColor: '#E6F7FF',
},
{
type: nodeTypes.sl3,
borderColor: '#406BFF',
iconBgColor: '#D6E4FE',
},
{
type: nodeTypes.sl4,
borderColor: '#37474F',
iconBgColor: '#ECEEF2',
},
];
swimLanes.forEach(item => {
Graph.registerNode(
item.type,
{
inherit: 'rect',
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'rect',
selector: 'name-rect',
},
{
tagName: 'text',
selector: 'name-text',
},
],
attrs: {
body: {
fill: 'transparent',
stroke: item.borderColor,
strokeWidth: 1,
},
'name-rect': {
width: swimLaneW,
height: 30,
fill: item.iconBgColor,
stroke: item.borderColor,
strokeWidth: 1,
// x: -1,
},
'name-text': {
ref: 'name-rect',
refY: 0.5,
refX: 0.5,
textAnchor: 'middle',
fontWeight: 'bold',
fill: '#000',
fontSize: 12,
},
},
},
true
);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 可拖拽组件
# 算子列表及折叠功能
groups: [
{
// layoutOptions: {
// columns: 1,
// marginX: 44,
// rowHeight: 50,
// },
name: 'type1',
title: '数据源',
graphHeight: 150,
},
{
name: 'type2',
title: '解析逻辑',
graphHeight: 60,
},
{
name: 'type3',
title: '处理规则',
graphHeight: 330,
},
{
name: 'type4',
title: '目标源',
graphHeight: 60,
},
],
const stencilContainer = document.querySelector('#stencil');
stencilContainer.appendChild(stencil.container);
const stencilNodes = nodeEntity.map(item => {
return graph.createNode({
shape: item.shape,
data: { ...item },
ports,
parent: item.parent,
});
});
stencil.load([...stencilNodes.slice(0, 3)], 'type1');
stencil.load([...stencilNodes.slice(3, 4)], 'type2');
stencil.load([...stencilNodes.slice(4, 11)], 'type3');
stencil.load([...stencilNodes.slice(11, 12)], 'type4');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 验证坐标自定义
验证坐标自定义&&连线边框数据修正
validateNode(droppingNode, options) {
let { x, y } = droppingNode.getPosition();
let xp = 0,
xm = x;
if (droppingNode.getParentId()) {
const parentNode = baseGraph.cellController.findNodeById(droppingNode.getParentId());
if (parentNode) {
xp = parentNode.getData().x;
xm = xp + swimLaneW / 2 - cellTitleW / 2;
}
}
if (xm > 0) {
const data = droppingNode.getData();
droppingNode.setData({
...data,
x: xm,
y,
id: droppingNode.id,
isInit: false, // 是否是保存后的初始化数据
isNode: true, // 是节点,而不是plan
});
droppingNode.setPosition(xm, y);
}
return true;
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 抽屉折叠功能
# 左右折叠功能
动态计算画布及容器的大小;resize处理;
render() {
const { expanedR } = this.state;
return (
<div className={styles.graph}>
<div className={styles.content} ref={this.refContainer} />
<div className={classnames(styles.right, expanedR ? styles.expaned : styles.retract)}>
<div id="stencil" className={classnames(styles.stencil, expanedR ? styles.expaned : styles.retract)} />
<div
className={classnames(styles.expandBtn, 'pointer')}
onClick={() => {
this.setState({ expanedR: !expanedR }, () => this.resizeFn());
}}
>
{expanedR ? <RightOutlined /> : <LeftOutlined />}
</div>
</div>
<Dropdown overlay={this.renderContextMenu()} trigger={['click']} onVisibleChange={this.handleContextMenuVisibleChange}>
<span ref={this.refContextMenuAnchor} className={styles.menu} />
</Dropdown>
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
getContainerSize = () => {
const { expanedL = true, expanedR = true } = this.state;
let width = document.body.offsetWidth;
if (expanedL) width = width - sliderLW;
if (expanedR) width = width - sliderRW;
if (!expanedR) width = width - 30;
return {
width: width,
height: document.body.offsetHeight - 100,
};
};
// TODO: @debounce(300)
resizeFn = (expanedL = this.state.expanedL) => {
// const debounceInput = () => {
// 通过外部更新里面的数据
this.setState({ expanedL: expanedL }, () => {
const { width, height } = this.getContainerSize();
this.graph.resize(width, height);
this.initData();
this.removeNodesAndEdges();
this.baseGraph.updateGraph(this.chartData, this.swimLaneW);
});
// };
// return _.debounce(debounceInput, 300);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 最终效果
上次更新: 2023/11/17, 05:08:18