├── .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 |
43 |
44 | 45 |
46 |
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 |