@yuku123/z-wf-frontend-component 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/dist/z-wf-frontend-component.css +1 -0
- package/dist/z-wf_frontend_component.es.js +30846 -0
- package/dist/z-wf_frontend_component.umd.js +114 -0
- package/package.json +62 -0
- package/src/components/FlowDesigner/index.module.css +137 -0
- package/src/components/FlowDesigner/index.tsx +133 -0
- package/src/components/FlowDesigner/nodes/index.ts +157 -0
- package/src/index.ts +2 -0
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yuku123/z-wf-frontend-component",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "z-wf FlowDesigner 组件包 - LogicFlow 驱动的可视化流程设计器",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"react",
|
|
8
|
+
"logicflow",
|
|
9
|
+
"flow-designer",
|
|
10
|
+
"workflow",
|
|
11
|
+
"z-opc"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "zifang",
|
|
16
|
+
"email": "1340947819@qq.com"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/yuku123/z-opc/tree/main/z-wf/_frontend_component#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/yuku123/z-opc.git",
|
|
22
|
+
"directory": "z-wf/_frontend_component"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/yuku123/z-opc/issues"
|
|
26
|
+
},
|
|
27
|
+
"main": "./dist/z-wf_frontend_component.umd.js",
|
|
28
|
+
"module": "./dist/z-wf_frontend_component.es.js",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/z-wf_frontend_component.es.js",
|
|
32
|
+
"require": "./dist/z-wf_frontend_component.umd.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"dev": "vite --port 3098",
|
|
46
|
+
"build": "vite build",
|
|
47
|
+
"preview": "vite preview"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": "^19.2.0",
|
|
51
|
+
"react-dom": "^19.2.0"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@logicflow/core": "^2.2.3",
|
|
55
|
+
"@logicflow/extension": "^2.2.3"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
59
|
+
"typescript": "^5.3.3",
|
|
60
|
+
"vite": "^6.2.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
.designerContainer {
|
|
2
|
+
width: 100%;
|
|
3
|
+
height: 100%;
|
|
4
|
+
position: relative;
|
|
5
|
+
background: #fafafa;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.canvas {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* 拖拽面板样式 */
|
|
14
|
+
.dndItem {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
align-items: center;
|
|
18
|
+
padding: 12px;
|
|
19
|
+
cursor: grab;
|
|
20
|
+
transition: all 0.2s;
|
|
21
|
+
border-radius: 6px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.dndItem:hover {
|
|
25
|
+
background: #f0f7ff;
|
|
26
|
+
transform: scale(1.05);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.dndItem img {
|
|
30
|
+
width: 40px;
|
|
31
|
+
height: 40px;
|
|
32
|
+
margin-bottom: 8px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.dndItem span {
|
|
36
|
+
font-size: 12px;
|
|
37
|
+
color: #595959;
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* LogicFlow 样式覆盖 */
|
|
42
|
+
:global(.lf-dnd-panel) {
|
|
43
|
+
width: 80px !important;
|
|
44
|
+
background: #fff;
|
|
45
|
+
border-right: 1px solid #e8e8e8;
|
|
46
|
+
padding: 16px 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:global(.lf-control) {
|
|
50
|
+
background: #fff;
|
|
51
|
+
border-radius: 4px;
|
|
52
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
:global(.lf-mini-map) {
|
|
56
|
+
background: #fff;
|
|
57
|
+
border: 1px solid #e8e8e8;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* 节点样式 */
|
|
63
|
+
:global(.lf-node) {
|
|
64
|
+
cursor: pointer;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:global(.lf-node:hover) {
|
|
68
|
+
filter: brightness(0.95);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:global(.lf-node.selected) {
|
|
72
|
+
filter: drop-shadow(0 0 4px rgba(22, 119, 255, 0.5));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* 边样式 */
|
|
76
|
+
:global(.lf-edge) {
|
|
77
|
+
stroke: #8c8c8c;
|
|
78
|
+
stroke-width: 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
:global(.lf-edge:hover) {
|
|
82
|
+
stroke: #1677ff;
|
|
83
|
+
stroke-width: 3;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* 文本样式 */
|
|
87
|
+
:global(.lf-text) {
|
|
88
|
+
font-size: 14px;
|
|
89
|
+
fill: #262626;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* 属性面板 */
|
|
93
|
+
.propertyPanel {
|
|
94
|
+
position: absolute;
|
|
95
|
+
right: 0;
|
|
96
|
+
top: 0;
|
|
97
|
+
bottom: 0;
|
|
98
|
+
width: 300px;
|
|
99
|
+
background: #fff;
|
|
100
|
+
border-left: 1px solid #e8e8e8;
|
|
101
|
+
padding: 16px;
|
|
102
|
+
overflow-y: auto;
|
|
103
|
+
z-index: 10;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.propertyPanelTitle {
|
|
107
|
+
font-size: 16px;
|
|
108
|
+
font-weight: 500;
|
|
109
|
+
color: #262626;
|
|
110
|
+
margin-bottom: 16px;
|
|
111
|
+
padding-bottom: 12px;
|
|
112
|
+
border-bottom: 1px solid #f0f0f0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* 工具栏 */
|
|
116
|
+
.toolbar {
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: 16px;
|
|
119
|
+
left: 96px;
|
|
120
|
+
right: 316px;
|
|
121
|
+
height: 48px;
|
|
122
|
+
background: #fff;
|
|
123
|
+
border-radius: 4px;
|
|
124
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
padding: 0 16px;
|
|
128
|
+
gap: 8px;
|
|
129
|
+
z-index: 10;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.toolbarDivider {
|
|
133
|
+
width: 1px;
|
|
134
|
+
height: 24px;
|
|
135
|
+
background: #e8e8e8;
|
|
136
|
+
margin: 0 8px;
|
|
137
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, {useCallback, useEffect, useRef} from 'react';
|
|
2
|
+
import styles from './index.module.css';
|
|
3
|
+
import {registerApprovalNodes} from './nodes';
|
|
4
|
+
import LogicFlow from '@logicflow/core';
|
|
5
|
+
import {Control, DndPanel, Menu, MiniMap, SelectionSelect} from '@logicflow/extension';
|
|
6
|
+
|
|
7
|
+
const nodeTypes = [
|
|
8
|
+
{
|
|
9
|
+
type: 'start',
|
|
10
|
+
label: '开始节点',
|
|
11
|
+
icon: 'https://cdn.jsdelivr.net/gh/LogicFlow/static@latest/docs/static/start.png'
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
type: 'approval',
|
|
15
|
+
label: '审批节点',
|
|
16
|
+
icon: 'https://cdn.jsdelivr.net/gh/LogicFlow/static@latest/docs/static/approve.png'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: 'condition',
|
|
20
|
+
label: '条件分支',
|
|
21
|
+
icon: 'https://cdn.jsdelivr.net/gh/LogicFlow/static@latest/docs/static/condition.png'
|
|
22
|
+
},
|
|
23
|
+
{type: 'copy', label: '抄送节点', icon: 'https://cdn.jsdelivr.net/gh/LogicFlow/static@latest/docs/static/copy.png'},
|
|
24
|
+
{type: 'end', label: '结束节点', icon: 'https://cdn.jsdelivr.net/gh/LogicFlow/static@latest/docs/static/end.png'},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
interface FlowDesignerProps {
|
|
28
|
+
processDefinitionId?: string;
|
|
29
|
+
initialData?: any;
|
|
30
|
+
readOnly?: boolean;
|
|
31
|
+
onSave?: (data: any) => void;
|
|
32
|
+
onDeploy?: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FlowDesigner: React.FC<FlowDesignerProps> = ({
|
|
36
|
+
processDefinitionId,
|
|
37
|
+
initialData,
|
|
38
|
+
readOnly = false,
|
|
39
|
+
onSave,
|
|
40
|
+
onDeploy,
|
|
41
|
+
}) => {
|
|
42
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
43
|
+
const lfRef = useRef<any>(null);
|
|
44
|
+
|
|
45
|
+
const initLogicFlow = useCallback(() => {
|
|
46
|
+
if (!containerRef.current) return;
|
|
47
|
+
|
|
48
|
+
LogicFlow.use(Control);
|
|
49
|
+
LogicFlow.use(MiniMap);
|
|
50
|
+
LogicFlow.use(Menu);
|
|
51
|
+
LogicFlow.use(DndPanel);
|
|
52
|
+
LogicFlow.use(SelectionSelect);
|
|
53
|
+
|
|
54
|
+
const lf = new LogicFlow({
|
|
55
|
+
container: containerRef.current,
|
|
56
|
+
grid: {size: 20, visible: true, type: 'dot', config: {color: '#e5e5e5'}},
|
|
57
|
+
keyboard: {enabled: true},
|
|
58
|
+
snapline: true,
|
|
59
|
+
outline: true,
|
|
60
|
+
textEdit: !readOnly,
|
|
61
|
+
isSilentMode: readOnly,
|
|
62
|
+
style: {rect: {rx: 4, ry: 4}},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
registerApprovalNodes(lf);
|
|
66
|
+
|
|
67
|
+
lf.extension.dndPanel.setPatternItems(
|
|
68
|
+
nodeTypes.map(node => ({type: node.type, label: node.label, icon: node.icon, className: styles.dndItem}))
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
lf.render(initialData || {
|
|
72
|
+
nodes: [{id: 'start-1', type: 'start', x: 100, y: 300, text: '开始'}],
|
|
73
|
+
edges: []
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
lfRef.current = lf;
|
|
77
|
+
}, [initialData, readOnly]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
// 确保CDN资源加载完成
|
|
81
|
+
if (typeof LogicFlow !== 'undefined') {
|
|
82
|
+
initLogicFlow();
|
|
83
|
+
} else {
|
|
84
|
+
const timer = setInterval(() => {
|
|
85
|
+
if (typeof LogicFlow !== 'undefined') {
|
|
86
|
+
clearInterval(timer);
|
|
87
|
+
initLogicFlow();
|
|
88
|
+
}
|
|
89
|
+
}, 300);
|
|
90
|
+
return () => clearInterval(timer);
|
|
91
|
+
}
|
|
92
|
+
return () => {
|
|
93
|
+
lfRef.current?.destroy();
|
|
94
|
+
lfRef.current = null;
|
|
95
|
+
};
|
|
96
|
+
}, [initLogicFlow]);
|
|
97
|
+
|
|
98
|
+
const getGraphData = useCallback(() => lfRef.current?.getGraphData() || null, []);
|
|
99
|
+
|
|
100
|
+
React.useImperativeHandle(
|
|
101
|
+
(React as any).useRef<any>().current,
|
|
102
|
+
() => ({getGraphData}),
|
|
103
|
+
[getGraphData]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className={styles.designerContainer}>
|
|
108
|
+
<div ref={containerRef} className={styles.canvas} style={{height: '70vh'}}/>
|
|
109
|
+
<div style={{marginTop: 16, display: 'flex', gap: 12}}>
|
|
110
|
+
<button onClick={() => onSave?.(getGraphData())} style={{
|
|
111
|
+
padding: '8px 16px',
|
|
112
|
+
backgroundColor: '#1677ff',
|
|
113
|
+
color: '#fff',
|
|
114
|
+
border: 'none',
|
|
115
|
+
borderRadius: 4,
|
|
116
|
+
cursor: 'pointer'
|
|
117
|
+
}}>保存流程
|
|
118
|
+
</button>
|
|
119
|
+
<button onClick={() => onDeploy?.()} style={{
|
|
120
|
+
padding: '8px 16px',
|
|
121
|
+
backgroundColor: '#52c41a',
|
|
122
|
+
color: '#fff',
|
|
123
|
+
border: 'none',
|
|
124
|
+
borderRadius: 4,
|
|
125
|
+
cursor: 'pointer'
|
|
126
|
+
}}>部署流程
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default FlowDesigner;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {CircleNode, h, PolygonNode, RectNode} from '@logicflow/core';
|
|
2
|
+
|
|
3
|
+
// 开始节点
|
|
4
|
+
class StartNode extends RectNode {
|
|
5
|
+
static extendKey = 'start';
|
|
6
|
+
|
|
7
|
+
getNodeStyle() {
|
|
8
|
+
const style = super.getNodeStyle();
|
|
9
|
+
style.fill = '#52c41a';
|
|
10
|
+
style.stroke = '#52c41a';
|
|
11
|
+
style.strokeWidth = 2;
|
|
12
|
+
return style;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getTextStyle() {
|
|
16
|
+
const style = super.getTextStyle();
|
|
17
|
+
style.color = '#fff';
|
|
18
|
+
style.fontSize = 14;
|
|
19
|
+
return style;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getShape() {
|
|
23
|
+
const {x, y, width, height} = this.getAttributes();
|
|
24
|
+
const r = height / 2;
|
|
25
|
+
return h('rect', {
|
|
26
|
+
x: x - width / 2,
|
|
27
|
+
y: y - height / 2,
|
|
28
|
+
width,
|
|
29
|
+
height,
|
|
30
|
+
rx: r,
|
|
31
|
+
ry: r,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 审批节点
|
|
37
|
+
class ApprovalNode extends RectNode {
|
|
38
|
+
static extendKey = 'approval';
|
|
39
|
+
|
|
40
|
+
getNodeStyle() {
|
|
41
|
+
const style = super.getNodeStyle();
|
|
42
|
+
style.fill = '#fff';
|
|
43
|
+
style.stroke = '#1677ff';
|
|
44
|
+
style.strokeWidth = 2;
|
|
45
|
+
return style;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getTextStyle() {
|
|
49
|
+
const style = super.getTextStyle();
|
|
50
|
+
style.color = '#262626';
|
|
51
|
+
style.fontSize = 14;
|
|
52
|
+
return style;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 条件节点(菱形)
|
|
57
|
+
class ConditionNode extends PolygonNode {
|
|
58
|
+
static extendKey = 'condition';
|
|
59
|
+
|
|
60
|
+
getNodeStyle() {
|
|
61
|
+
const style = super.getNodeStyle();
|
|
62
|
+
style.fill = '#fff7e6';
|
|
63
|
+
style.stroke = '#fa8c16';
|
|
64
|
+
style.strokeWidth = 2;
|
|
65
|
+
return style;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getTextStyle() {
|
|
69
|
+
const style = super.getTextStyle();
|
|
70
|
+
style.color = '#262626';
|
|
71
|
+
style.fontSize = 13;
|
|
72
|
+
return style;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getAttributes() {
|
|
76
|
+
const attributes = super.getAttributes();
|
|
77
|
+
const width = Math.max(80, attributes.width || 80);
|
|
78
|
+
const height = Math.max(80, attributes.height || 80);
|
|
79
|
+
const points = [
|
|
80
|
+
[attributes.x, attributes.y - height / 2],
|
|
81
|
+
[attributes.x + width / 2, attributes.y],
|
|
82
|
+
[attributes.x, attributes.y + height / 2],
|
|
83
|
+
[attributes.x - width / 2, attributes.y],
|
|
84
|
+
];
|
|
85
|
+
return {...attributes, points, width, height};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 抄送节点
|
|
90
|
+
class CopyNode extends RectNode {
|
|
91
|
+
static extendKey = 'copy';
|
|
92
|
+
|
|
93
|
+
getNodeStyle() {
|
|
94
|
+
const style = super.getNodeStyle();
|
|
95
|
+
style.fill = '#f6ffed';
|
|
96
|
+
style.stroke = '#52c41a';
|
|
97
|
+
style.strokeWidth = 2;
|
|
98
|
+
style.strokeDasharray = '5,5';
|
|
99
|
+
return style;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getTextStyle() {
|
|
103
|
+
const style = super.getTextStyle();
|
|
104
|
+
style.color = '#262626';
|
|
105
|
+
style.fontSize = 14;
|
|
106
|
+
return style;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 结束节点
|
|
111
|
+
class EndNode extends CircleNode {
|
|
112
|
+
static extendKey = 'end';
|
|
113
|
+
|
|
114
|
+
getNodeStyle() {
|
|
115
|
+
const style = super.getNodeStyle();
|
|
116
|
+
style.fill = '#ff4d4f';
|
|
117
|
+
style.stroke = '#ff4d4f';
|
|
118
|
+
style.strokeWidth = 2;
|
|
119
|
+
return style;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getTextStyle() {
|
|
123
|
+
const style = super.getTextStyle();
|
|
124
|
+
style.color = '#fff';
|
|
125
|
+
style.fontSize = 14;
|
|
126
|
+
return style;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 注册所有节点
|
|
131
|
+
export function registerApprovalNodes(lf: any): void {
|
|
132
|
+
lf.register({
|
|
133
|
+
type: 'start',
|
|
134
|
+
view: StartNode,
|
|
135
|
+
model: RectNode.model,
|
|
136
|
+
});
|
|
137
|
+
lf.register({
|
|
138
|
+
type: 'approval',
|
|
139
|
+
view: ApprovalNode,
|
|
140
|
+
model: RectNode.model,
|
|
141
|
+
});
|
|
142
|
+
lf.register({
|
|
143
|
+
type: 'condition',
|
|
144
|
+
view: ConditionNode,
|
|
145
|
+
model: PolygonNode.model,
|
|
146
|
+
});
|
|
147
|
+
lf.register({
|
|
148
|
+
type: 'copy',
|
|
149
|
+
view: CopyNode,
|
|
150
|
+
model: RectNode.model,
|
|
151
|
+
});
|
|
152
|
+
lf.register({
|
|
153
|
+
type: 'end',
|
|
154
|
+
view: EndNode,
|
|
155
|
+
model: CircleNode.model,
|
|
156
|
+
});
|
|
157
|
+
}
|
package/src/index.ts
ADDED