├── .env
├── .gitignore
├── README.md
├── craco.config.js
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.tsx
├── assets
│ └── images
│ │ └── spl.png
├── index.css
├── index.tsx
├── react-app-env.d.ts
├── typing.d.ts
└── workflow
│ ├── components
│ ├── addNode.tsx
│ ├── index.module.less
│ └── nodeBox.tsx
│ ├── index.module.less
│ ├── index.tsx
│ └── mock.js
├── tsconfig.json
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | GENERATE_SOURCEMAP = false
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /dist
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Hook低仿钉钉审批流、企业微信审批流
2 |
3 | - 此项目由React脚手架创建,React18、TypeScript、Ant Design、Hook、Less、Craco编写的简化版审批流(钉钉审批流、企业微信审批流)。
4 | - 有的OA系统会用到,此项目旨在交流学习,如有需要可以自行拓展。
5 | - 如果喜欢的话,欢迎star😍
6 |
7 | ### 在线地址
8 | [点击查看在线演示](https://itangdong.github.io/)
9 |
10 | ### 效果图:
11 |
12 |
13 |
14 | ·
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const CracoLessPlugin = require('craco-less');
3 |
4 | module.exports = {
5 | webpack: {
6 | configure:(webpackConfig, { env, paths }) => {
7 | // 修改build的生成文件名称
8 | paths.appBuild = 'dist';
9 | webpackConfig.output ={
10 | ...webpackConfig.output,
11 | path: path.resolve(__dirname,'dist'),
12 | publicPath: '/',
13 | };
14 | return webpackConfig;
15 | },
16 | },
17 | plugins: [
18 | {
19 | plugin: CracoLessPlugin,
20 | options: {
21 | lessLoaderOptions: {
22 | // 应用全局样式与开启css module
23 | lessOptions: {
24 | javascriptEnabled: true,
25 | },
26 | },
27 | },
28 | },
29 | ],
30 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workflow-react-ts",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@ant-design/icons": "^4.7.0",
7 | "@types/node": "^16.11.26",
8 | "@types/react": "^17.0.43",
9 | "@types/react-dom": "^17.0.14",
10 | "antd": "^4.19.3",
11 | "react": "^18.0.0",
12 | "react-dom": "^18.0.0",
13 | "react-scripts": "5.0.0",
14 | "typescript": "^4.6.3"
15 | },
16 | "scripts": {
17 | "start": "craco start",
18 | "build": "craco build",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": [
23 | "react-app",
24 | "react-app/jest"
25 | ]
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | },
39 | "devDependencies": {
40 | "@craco/craco": "^6.4.3",
41 | "craco-less": "^2.0.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itangdong/workflow-react-ts/9019ebca9db292faf9e699d04dcda0fa7c71b7fb/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | 审批流(React)
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itangdong/workflow-react-ts/9019ebca9db292faf9e699d04dcda0fa7c71b7fb/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itangdong/workflow-react-ts/9019ebca9db292faf9e699d04dcda0fa7c71b7fb/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Demo from './workflow/index';
4 |
5 | import 'antd/dist/antd.less';
6 |
7 | function App() {
8 | return (
9 |
10 | );
11 | }
12 |
13 | export default App;
14 |
--------------------------------------------------------------------------------
/src/assets/images/spl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itangdong/workflow-react-ts/9019ebca9db292faf9e699d04dcda0fa7c71b7fb/src/assets/images/spl.png
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 |
6 | const root = ReactDOM.createRoot((document.getElementById('root')) as Element);
7 | root.render(
8 |
9 |
10 | ,
11 | );
12 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/typing.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.less'{
2 | const content: { [key: string]: string };
3 | export default content;
4 | }
5 | declare module '*.ts';
6 | declare module 'dva-core';
7 |
--------------------------------------------------------------------------------
/src/workflow/components/addNode.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 审批流组件 - 添加节点组件
3 | */
4 | import React from 'react';
5 | import { PlusCircleTwoTone } from '@ant-design/icons';
6 |
7 | import Style from './index.module.less';
8 |
9 | const AddNode = ({ parentNodeData, dataSource, onSetDataSource }: any) => {
10 | // 寻找nodeId
11 | const loopFn = (arr: any, id: any) => {
12 | arr.forEach((item: any) => {
13 | // 找到了这个节点就开始添加新的子节点
14 | if (item.nodeId === id) {
15 | const oldNode = item.childNode;
16 | item.childNode = {
17 | nodeId: +new Date(),
18 | nodeName: '审核人',
19 | type: 1,
20 | childNode: oldNode,
21 | };
22 | } else if (item.childNode) {
23 | // 当前节点不是目标节点,但是有子节点,则继续遍历子节点
24 | loopFn([item.childNode], id);
25 | }
26 | // 当前节点不是目标节点,但是有条件节点,则继续遍历条件节点
27 | if (item.nodeId !== id && item.conditionNodes && item.conditionNodes.length > 0) {
28 | loopFn(item.conditionNodes, id);
29 | }
30 | });
31 | return arr;
32 | };
33 |
34 | // 点击添加节点
35 | const onAddNode = () => {
36 | const { nodeId } = parentNodeData;
37 | const result = loopFn([dataSource], nodeId)[0];
38 | onSetDataSource(JSON.parse(JSON.stringify(result)));
39 | };
40 |
41 | return (
42 |
47 | );
48 | };
49 |
50 | export default AddNode;
51 |
--------------------------------------------------------------------------------
/src/workflow/components/index.module.less:
--------------------------------------------------------------------------------
1 | .node-wrap{
2 | flex-direction: column;
3 | justify-content: flex-start;
4 | align-items: center;
5 | flex-wrap: wrap;
6 | padding: 0 50px;
7 | position: relative;
8 | display: flex;
9 | width: 100%;
10 | .node-card{
11 | display: flex;
12 | flex-direction: column;
13 | position: relative;
14 | width: 220px;
15 | min-height: 72px;
16 | flex-shrink: 0;
17 | background: #fff;
18 | border-radius: 4px;
19 | cursor: pointer;
20 | .node-title{
21 | height: 24px;
22 | line-height: 24px;
23 | display: flex;
24 | justify-content: space-between;
25 | border-radius: 4px 4px 0 0;
26 | padding: 0 10px;
27 | .node-close-icon{
28 | color: #595959;
29 | line-height: 24px;
30 | &:hover{
31 | color: #333;
32 | }
33 | }
34 | }
35 | &::after{
36 | pointer-events: none;
37 | content: "";
38 | position: absolute;
39 | top: 0;
40 | bottom: 0;
41 | left: 0;
42 | right: 0;
43 | z-index: 2;
44 | border-radius: 4px;
45 | border: 1px solid transparent;
46 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1);
47 | }
48 | &::before{
49 | content: "";
50 | position: absolute;
51 | top: -12px;
52 | left: 50%;
53 | transform: translateX(-50%);
54 | width: 0;
55 | height: 4px;
56 | border-style: solid;
57 | border-width: 8px 6px 4px;
58 | border-color: #cacaca transparent transparent;
59 | background: #f5f5f7;
60 | }
61 | }
62 | .initiator{
63 | background-color: #ff7875;
64 | }
65 | .auditor{
66 | background-color: #ffa940;
67 | }
68 | .condition{
69 | background-color: #73d13d;
70 | }
71 | .copy{
72 | background-color: #87e8de;
73 | }
74 | }
75 |
76 | .route-node-wrap{
77 | display: flex;
78 | flex-direction: column;
79 | flex-wrap: wrap;
80 | align-items: center;
81 | min-height: 270px;
82 | width: 100%;
83 | flex-shrink: 0;
84 | .branch-wrap{
85 | display: flex;
86 | overflow: visible;
87 | min-height: 180px;
88 | height: auto;
89 | border-bottom: 2px solid #ccc;
90 | border-top: 2px solid #ccc;
91 | position: relative;
92 | margin-top: 15px;
93 | .add-branch-btn{
94 | border: none;
95 | outline: none;
96 | user-select: none;
97 | justify-content: center;
98 | font-size: 12px;
99 | padding: 0 10px;
100 | height: 30px;
101 | line-height: 30px;
102 | border-radius: 15px;
103 | color: #3296fa;
104 | background: #fff;
105 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
106 | position: absolute;
107 | top: -16px;
108 | left: 50%;
109 | transform: translateX(-50%);
110 | transform-origin: center center;
111 | cursor: pointer;
112 | z-index: 1;
113 | display: flex;
114 | align-items: center;
115 | transition: scale .3s cubic-bezier(.645, .045, .355, 1);
116 | &:hover{
117 | transform: translateX(-50%) scale(1.1);
118 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1);
119 | }
120 | }
121 | .col-box{
122 | display: flex;
123 | flex-direction: column;
124 | align-items: center;
125 | position: relative;
126 | background-color: #f0f2f5;
127 | &::before{
128 | content: "";
129 | position: absolute;
130 | top: 0;
131 | left: 0;
132 | right: 0;
133 | bottom: 0;
134 | z-index: 0;
135 | margin: auto;
136 | width: 2px;
137 | height: 100%;
138 | background-color: #cacaca;
139 | }
140 | .condition-node{
141 | min-height: 220px;
142 | display: inline-flex;
143 | .condition-node-card{
144 | display: inline-flex;
145 | flex-direction: column;
146 | padding-top: 30px;
147 | padding-right: 50px;
148 | padding-left: 50px;
149 | justify-content: center;
150 | align-items: center;
151 | flex-grow: 1;
152 | position: relative;
153 | &::before{
154 | content: "";
155 | position: absolute;
156 | top: 0;
157 | left: 0;
158 | right: 0;
159 | bottom: 0;
160 | margin: auto;
161 | width: 2px;
162 | height: 100%;
163 | background-color: #cacaca;
164 | }
165 | }
166 | }
167 | .top-right-cover-line, .top-left-cover-line{
168 | position: absolute;
169 | height: 8px;
170 | width: 50%;
171 | background-color: #f0f2f5;
172 | top: -4px;
173 | }
174 | .top-left-cover-line{
175 | left: -1px;
176 | }
177 | .top-right-cover-line{
178 | right: -1px;
179 | }
180 | .bottom-left-cover-line, .bottom-right-cover-line{
181 | position: absolute;
182 | height: 8px;
183 | width: 50%;
184 | background-color: #f0f2f5;
185 | bottom: -4px;
186 | }
187 | .bottom-left-cover-line{
188 | left: -1px;
189 | }
190 | .bottom-right-cover-line{
191 | right: -1px;
192 | }
193 | }
194 | }
195 | }
196 |
197 | .add-node-btn-box{
198 | width: 240px;
199 | display: flex;
200 | flex-shrink: 0;
201 | position: relative;
202 | &::before{
203 | content: "";
204 | position: absolute;
205 | top: 0;
206 | left: 0;
207 | right: 0;
208 | bottom: 0;
209 | margin: auto;
210 | width: 2px;
211 | height: 100%;
212 | background-color: #cacaca;
213 | }
214 | .add-node-btn{
215 | user-select: none;
216 | width: 240px;
217 | height: 80px;
218 | padding: 20px 0 32px;
219 | display: flex;
220 | justify-content: center;
221 | flex-shrink: 0;
222 | flex-grow: 1;
223 | .create-btn{
224 | position: absolute;
225 | font-size: 26px;
226 | cursor: pointer;
227 | transition: all .3s cubic-bezier(.645, .045, .355, 1);
228 | &:hover{
229 | transform: scale(1.3);
230 | }
231 | }
232 | }
233 | }
--------------------------------------------------------------------------------
/src/workflow/components/nodeBox.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 审批流组件 - 节点组件
3 | */
4 | import React, { useRef } from 'react';
5 | import { CloseCircleOutlined } from '@ant-design/icons';
6 |
7 | import AddNode from './addNode';
8 |
9 | import Style from './index.module.less';
10 |
11 | const NodeBox = ({ currentData, dataSource, onSetDataSource }: any) => {
12 | const normalNodeId = useRef(0);
13 | // 找到普通节点并把它删除掉(非条件节点)
14 | const removeNormalNodeFn = (arr: any, id: any) => {
15 | arr.forEach((item: any) => {
16 | // 找到了这个节点就开始添加新的子节点
17 | // item的子节点nodeId匹配上了,那么就是要删除这个子节点,item的child指向子节点的子节点
18 | if (item.childNode && item.childNode.nodeId === id) {
19 | item.childNode = item.childNode.childNode;
20 | } else if (item.childNode) {
21 | // 当前节点不是目标节点,但是有子节点,则继续遍历子节点
22 | removeNormalNodeFn([item.childNode], id);
23 | }
24 | // 当前节点不是目标节点,但是有条件节点,则继续遍历条件节点
25 | if ((item.childNode && item.childNode.nodeId) !== id && item.conditionNodes
26 | && item.conditionNodes.length > 0) {
27 | removeNormalNodeFn(item.conditionNodes, id);
28 | }
29 | });
30 | return arr;
31 | };
32 |
33 | // 找到条件节点把它删除
34 | const removeConditionNodeFn = (arr: any, id: any) => {
35 | arr.forEach((item: any) => {
36 | let flag = true;
37 | if (item.conditionNodes && item.conditionNodes.length > 0 && flag) {
38 | // 在条件列表中找到目标条件节点的索引
39 | const findIndex = item.conditionNodes.findIndex(
40 | (conditionItem: any) => conditionItem.nodeId === id,
41 | );
42 | // 如果找到了这个节点,则不需要再往下遍历了
43 | if (findIndex !== -1) {
44 | flag = false;
45 | // 如果条件节点只有2个,那么删除掉一个,则要删除的不光是条件节点,而是整个路由节点
46 | if (item.conditionNodes.length === 2) {
47 | normalNodeId.current = item.nodeId;
48 | } else {
49 | // 条件有多个,直接删除这一个
50 | item.conditionNodes.splice(findIndex, 1);
51 | }
52 | }
53 | }
54 | if (item.childNode && flag) {
55 | removeConditionNodeFn([item.childNode], id);
56 | }
57 | // 需要删除的条件节点,可能在条件节点下
58 | if (item.conditionNodes?.length && flag) {
59 | removeConditionNodeFn(item.conditionNodes, id);
60 | }
61 | });
62 | return arr;
63 | };
64 |
65 | // 点击添加条件按钮
66 | const onAddCondition = () => {
67 | const { nodeId } = currentData;
68 | const addConditionFn = (arr: any, id: number) => {
69 | arr.forEach((item: any) => {
70 | // 找到对应路由节点
71 | console.log(item.nodeId)
72 | if (item.nodeId === id) {
73 | return item.conditionNodes.push({
74 | nodeName: '条件N',
75 | type: 3,
76 | nodeId: +new Date(),
77 | conditionList: [],
78 | nodeUserList: [],
79 | conditionNodes: [],
80 | childNode: null,
81 | });
82 | }
83 | if (item.childNode) {
84 | addConditionFn([item.childNode], id);
85 | }
86 | // 条件节点下可能直接还是条件节点
87 | if (item.conditionNodes?.length) {
88 | addConditionFn(item.conditionNodes, id);
89 | }
90 | });
91 | return arr;
92 | };
93 | const [result] = addConditionFn([dataSource], nodeId);
94 | // 刷新state
95 | onSetDataSource(JSON.parse(JSON.stringify(result)));
96 | };
97 |
98 | // 渲染一般节点(非路由节点)
99 | const renderNormalNode = (normalNodeData: any) => {
100 | // 删除节点
101 | const onDeleteCard = () => {
102 | const { nodeId, type } = normalNodeData;
103 | // 最终需要渲染的结果数据
104 | let result = {};
105 | // 如果是删除的是条件节点,则需要从它的父节点的conditionList中删除它
106 | // 并且判断如果只有2个条件,删除了一个,那么父节点的conditionList直接置空
107 | if (type === 3) {
108 | // 删除条件节点并得到最终删除后的数据
109 | [result] = removeConditionNodeFn([dataSource], nodeId);
110 | // normalNodeId.current 如果有值,则说明需要删除的是路由节点(没有值则说明是删除的是路由节点下的某一个条件节点)
111 | if (normalNodeId.current) {
112 | [result] = removeNormalNodeFn([dataSource], normalNodeId.current);
113 | normalNodeId.current = 0;
114 | }
115 | } else {
116 | // 删除的是普通节点并得到最终删除后的数据
117 | [result] = removeNormalNodeFn([dataSource], nodeId);
118 | }
119 | // 刷新state
120 | onSetDataSource(JSON.parse(JSON.stringify(result)));
121 | };
122 | // 基础样式
123 | let computedClass = Style['node-title'];
124 | // 发起人
125 | if (normalNodeData.type === 0) {
126 | computedClass = `${computedClass} ${Style['initiator']}`;
127 | }
128 | // 审批人
129 | if (normalNodeData.type === 1) {
130 | computedClass = `${computedClass} ${Style['auditor']}`;
131 | }
132 | // 抄送人
133 | if (normalNodeData.type === 2) {
134 | computedClass = `${computedClass} ${Style['copy']}`;
135 | }
136 | // 条件
137 | if (normalNodeData.type === 3) {
138 | computedClass = `${computedClass} ${Style['condition']}`;
139 | }
140 |
141 | return (
142 |
143 |
144 |
145 | {normalNodeData.nodeName}
146 | {
147 | normalNodeData.type !== 0
148 | ?
149 | : null
150 | }
151 |
152 |
153 | {/* 添加子节点 */}
154 |
159 |
160 | );
161 | };
162 |
163 | // 渲染遮盖线条
164 | const renderLineDom = (index: number) => {
165 | // 如果是渲染的第一个节点,则遮盖住左上与左下两条边线
166 | if (index === 0) {
167 | return (
168 | <>
169 |
170 |
171 | >
172 | );
173 | }
174 | // 如果渲染的是最后一个节点,则遮盖住右上与右下两条边线
175 | if (index === currentData.conditionNodes.length - 1) {
176 | return (
177 | <>
178 |
179 |
180 | >
181 | );
182 | }
183 | return null;
184 | };
185 |
186 | // 渲染路由节点
187 | const renderRouteNode = () => (
188 |
189 | {/* 条件分支节点 */}
190 |
191 |
添加条件
192 | {/* 渲染多列条件节点 */}
193 | {
194 | currentData.conditionNodes.map((item: any, index: number) => (
195 | // 路由节点整个包裹dom元素
196 |
197 | {/* 条件节点 */}
198 |
199 | {/* 每一个条件 */}
200 |
201 | {/* 条件盒子里面的节点 */}
202 | {
203 | renderNormalNode(item)
204 | }
205 |
206 |
207 | {/* 条件节后面可以是任意节点,所以自调用本组件 */}
208 | {
209 | item.childNode
210 | ? (
211 |
216 | )
217 | : null
218 | }
219 | {/* 渲染遮盖线条,需要遮盖住四个角的边线 */}
220 | {renderLineDom(index)}
221 |
222 | ))
223 | }
224 |
225 | {/* 添加子节点 */}
226 |
231 |
232 | );
233 | return (
234 | <>
235 | {/* 渲染一般节点或者路由节点 */}
236 | {currentData.type === 4 ? renderRouteNode() : renderNormalNode(currentData)}
237 | {/* 如果有子节点,继续递归调用本组件 */}
238 | {
239 | currentData.childNode
240 | ? (
241 |
246 | )
247 | : null
248 | }
249 | >
250 | );
251 | };
252 |
253 | export default NodeBox;
254 |
--------------------------------------------------------------------------------
/src/workflow/index.module.less:
--------------------------------------------------------------------------------
1 | .page-wrap{
2 | overflow: auto;
3 | background-color: #f0f2f5;
4 | width: 100vw;
5 | height: 100vh;
6 | .header-operate{
7 | position: fixed;
8 | left: 0;
9 | top: 0;
10 | z-index: 2;
11 | width: 100%;
12 | padding: 8px;
13 | }
14 | .workflow-wrap {
15 | display: inline-flex;
16 | flex-direction: column;
17 | align-items: center;
18 | min-width: 100%;
19 | padding-top: 30px;
20 | .start-flag{
21 | margin-bottom: 20px;
22 | }
23 | .last-node-box-wrap{
24 | .last-node-box-circle{
25 | width: 10px;
26 | height: 10px;
27 | margin: auto;
28 | border-radius: 50%;
29 | background: #dbdcdc;
30 | }
31 | .last-node-box-text{
32 | margin-top: 5px;
33 | text-align: center;
34 | }
35 | }
36 | }
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/src/workflow/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * 审批流展示页面-入口
3 | */
4 | import React, { useState } from 'react';
5 | import {
6 | Button,
7 | Space,
8 | } from 'antd';
9 |
10 | import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
11 | import dataObj from './mock.js';
12 |
13 |
14 | import NodeBox from './components/nodeBox';
15 | import Style from './index.module.less';
16 |
17 | const WorkFlowIndex = () => {
18 | // 渲染成表单的源数据
19 | const [dataSource, setDataSource] = useState(dataObj);
20 | // 缩放比例
21 | const [scale, setScale] = useState(100);
22 |
23 | // 变小
24 | const onChangeSmall = () => {
25 | setScale(scale - 10);
26 | };
27 |
28 | // 变大
29 | const onChangeBig = () => {
30 | setScale(scale + 10);
31 | };
32 |
33 | const topFixedElement = (
34 |
35 | } onClick={onChangeSmall} />
36 | } onClick={onChangeBig} />
37 | 当前缩放比例:{scale} %
38 |
39 | );
40 |
41 | return (
42 |
43 |
{topFixedElement}
44 |
52 |
53 | );
54 | };
55 |
56 | export default WorkFlowIndex;
57 |
--------------------------------------------------------------------------------
/src/workflow/mock.js:
--------------------------------------------------------------------------------
1 | export default {
2 | nodeName: '发起人',
3 | type: 0,
4 | nodeId: 1,
5 | conditionList: [],
6 | nodeUserList: [],
7 | childNode: {
8 | nodeName: '审核人',
9 | type: 1,
10 | nodeId: 2,
11 | childNode: {
12 | nodeName: '路由',
13 | type: 4,
14 | nodeId: 3,
15 | conditionList: [],
16 | nodeUserList: [],
17 | childNode: {
18 | nodeName: '抄送人',
19 | type: 2,
20 | nodeId: 4,
21 | childNode: null,
22 | nodeUserList: [],
23 | },
24 | conditionNodes: [{
25 | nodeName: '条件1',
26 | type: 3,
27 | nodeId: 6,
28 | conditionList: [{
29 | type: 1,
30 | }],
31 | nodeUserList: [{
32 | targetId: 85,
33 | type: 1,
34 | name: '娃娃乐',
35 | }],
36 | childNode: {
37 | nodeName: '审核人',
38 | type: 1,
39 | nodeId: 5,
40 | conditionList: [],
41 | nodeUserList: [],
42 | childNode: null,
43 | conditionNodes: [],
44 | },
45 | conditionNodes: [],
46 | }, {
47 | nodeName: '条件2',
48 | type: 3,
49 | nodeId: 7,
50 | conditionList: [],
51 | nodeUserList: [],
52 | childNode: {
53 | nodeName: '路由',
54 | type: 4,
55 | nodeId: 30,
56 | conditionList: [],
57 | nodeUserList: [],
58 | conditionNodes: [{
59 | nodeName: '条件3',
60 | type: 3,
61 | nodeId: 60,
62 | conditionList: [{
63 | type: 1,
64 | }],
65 | nodeUserList: [{
66 | targetId: 85,
67 | type: 1,
68 | name: '娃娃乐',
69 | }],
70 | childNode: {
71 | nodeName: '审核人',
72 | type: 1,
73 | nodeId: 50,
74 | conditionList: [],
75 | nodeUserList: [],
76 | childNode: null,
77 | conditionNodes: [],
78 | },
79 | conditionNodes: [],
80 | }, {
81 | nodeName: '条件2',
82 | type: 3,
83 | nodeId: 70,
84 | conditionList: [],
85 | nodeUserList: [],
86 | childNode: null,
87 | conditionNodes: [],
88 | }],
89 | childNode: {
90 | nodeName: '抄送人',
91 | type: 2,
92 | nodeId: 40,
93 | childNode: null,
94 | nodeUserList: [],
95 | },
96 | },
97 | }],
98 | },
99 | nodeUserList: [],
100 | },
101 | conditionNodes: [],
102 | };
103 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------