├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── LICENSE ├── examples └── approval-process-designer-react │ ├── .gitignore │ ├── README.md │ ├── eslint.config.js │ ├── index.html │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── App.css │ ├── App.tsx │ ├── activities │ │ ├── ApprovalActivity.tsx │ │ ├── CcActivity.tsx │ │ ├── ConditionActivity.tsx │ │ ├── Icons.tsx │ │ ├── RouteActivity.tsx │ │ ├── StartActivity.tsx │ │ └── index.tsx │ ├── assets │ │ └── react.svg │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── images ├── img.png └── shuque_wx.jpg ├── lerna.json ├── package.json ├── packages └── approval-process-designer-react │ ├── .fatherrc.ts │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ ├── Icons.tsx │ ├── activity │ │ ├── Activity.tsx │ │ ├── AddActivityBox.tsx │ │ ├── ApprovalActivity.tsx │ │ ├── BranchBox.tsx │ │ ├── CcActivity.tsx │ │ ├── CondiitionActivity.tsx │ │ ├── EndActivity.tsx │ │ ├── RouteActivity.tsx │ │ ├── RouteBranches.tsx │ │ ├── StartActivity.tsx │ │ └── index.tsx │ ├── components │ │ ├── Popover │ │ │ ├── Popover.tsx │ │ │ └── index.tsx │ │ ├── Row │ │ │ ├── Col.tsx │ │ │ ├── Row.tsx │ │ │ └── index.tsx │ │ ├── Tooltip │ │ │ ├── Tooltip.tsx │ │ │ └── index.tsx │ │ └── index.tsx │ ├── container │ │ ├── ApprovalProcessDesigner.tsx │ │ └── index.tsx │ ├── context.tsx │ ├── hooks │ │ ├── index.tsx │ │ ├── useActivities.tsx │ │ ├── useProcess.tsx │ │ ├── useProcessEngine.tsx │ │ └── useProcessNode.tsx │ ├── index.ts │ ├── model │ │ ├── ApprovalProcessEngine.ts │ │ ├── ProcessNode.ts │ │ └── index.ts │ ├── panel │ │ ├── StudioPanel.tsx │ │ └── index.tsx │ ├── store.tsx │ ├── types.tsx │ ├── util.ts │ └── widget │ │ ├── ActivityWidget.tsx │ │ ├── AddActivityItemWidget.tsx │ │ ├── IconWidget.tsx │ │ ├── ProcessWidget.tsx │ │ └── index.tsx │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── readme.md /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop # default branch 7 | pull_request: 8 | branches: 9 | - develop 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - run: yarn install 17 | - run: yarn run build 18 | - name: Deploy 19 | uses: peaceiris/actions-gh-pages@v3 20 | with: 21 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 22 | # github_token: ${{ secrets.GITHUB_TOKEN }} 23 | # 文档目录,如果是 react 模板需要修改为 docs-dist 24 | publish_dir: ./examples/approval-process-designer-react/dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea 3 | .npmrc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) fengxiaotx@163.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/approval-process-designer-react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "approval-process-designer-react", 3 | "private": true, 4 | "version": "0.0.1-beta.10", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "antd": "^5.11.5", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1" 16 | }, 17 | "devDependencies": { 18 | "@eslint/js": "^9.15.0", 19 | "@types/react": "^18.3.12", 20 | "@types/react-dom": "^18.3.1", 21 | "@vitejs/plugin-react-swc": "^3.5.0", 22 | "eslint": "^9.15.0", 23 | "eslint-plugin-react-hooks": "^5.0.0", 24 | "eslint-plugin-react-refresh": "^0.4.14", 25 | "globals": "^15.12.0", 26 | "typescript": "~5.6.2", 27 | "typescript-eslint": "^8.15.0", 28 | "vite": "^6.0.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import './App.css'; 3 | import { 4 | ApprovalProcessDesigner, GlobalStore, 5 | IProcessNode, 6 | ProcessWidget, StudioPanel 7 | } from "@trionesdev/approval-process-designer-react"; 8 | import {ApprovalActivity, ConditionActivity, RouteActivity, StartActivity, CcActivity} from "./activities"; 9 | import {Watermark} from "antd"; 10 | import * as Icons from "./activities/Icons" 11 | 12 | function App() { 13 | const [data, setData] = useState({ 14 | type: 'START', 15 | componentName: 'StartActivity', 16 | title: '发起人', 17 | nextNode: { 18 | type: 'APPROVAL', 19 | componentName: 'ApprovalActivity', 20 | title: '审批', 21 | nextNode: { 22 | type: 'ROUTE', 23 | componentName: 'RouteActivity', 24 | title: '路由', 25 | nextNode: { 26 | type: 'CC', 27 | componentName: 'CcActivity', 28 | title: '抄送人', 29 | }, 30 | conditionNodes: [ 31 | { 32 | type: 'CONDITION', 33 | componentName: 'ConditionActivity', 34 | title: '条件1', 35 | nextNode: { 36 | type: 'APPROVAL', 37 | componentName: 'ApprovalActivity', 38 | title: '审批人', 39 | } 40 | }, 41 | { 42 | type: 'CONDITION', 43 | componentName: 'ConditionActivity', 44 | defaultCondition: true, 45 | } 46 | ] 47 | } 48 | } 49 | }) 50 | const handleOnChange = (value: any) => { 51 | console.log("[processNode]", value) 52 | setData(value) 53 | } 54 | 55 | GlobalStore.registerIcons(Icons); 56 | return ( 57 |
58 | 60 |
66 | Triones Approval Process Designer 67 |
68 | Github 69 |
70 |
71 |
72 | 73 | 74 | 81 | 82 | 83 |
84 |
85 |
86 | ); 87 | } 88 | 89 | export default App; 90 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/activities/ApprovalActivity.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityFC, 3 | ApprovalActivity as TdApprovalActivity, 4 | DesignerCore, IActivity 5 | } from "@trionesdev/approval-process-designer-react"; 6 | import createResource = DesignerCore.createResource; 7 | import {Button, Drawer, Form, Input} from "antd"; 8 | import {useState} from "react"; 9 | 10 | export const ApprovalActivity: ActivityFC = ({...props}) => { 11 | const [form] = Form.useForm() 12 | 13 | const [open, setOpen] = useState(false) 14 | 15 | 16 | const handleClick = () => { 17 | setOpen(true) 18 | } 19 | 20 | const handleSave = () => { 21 | form.validateFields().then((values: any) => { 22 | props.processNode.props = values 23 | }) 24 | } 25 | 26 | return <> 27 | 28 | { 29 | setOpen(false) 30 | }} 31 | footer={
32 | 33 |
} 34 | > 35 |
36 | 37 | 38 | 39 |
40 |
41 | 42 | } 43 | 44 | ApprovalActivity.Resource = createResource({ 45 | icon: 'ApprovalActivityIcon', 46 | type: 'APPROVAL', 47 | componentName: 'ApprovalActivity', 48 | title: '审批人', 49 | addable: true 50 | }) 51 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/activities/CcActivity.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityFC, 3 | CcActivity as TdCcActivity, 4 | DesignerCore, 5 | IActivity 6 | } from "@trionesdev/approval-process-designer-react"; 7 | import createResource = DesignerCore.createResource; 8 | 9 | export const CcActivity: ActivityFC = TdCcActivity 10 | 11 | CcActivity.Resource = createResource({ 12 | type: 'CC', 13 | icon: 'CcActivityIcon', 14 | componentName: 'CcActivity', 15 | title: '抄送人', 16 | addable: true 17 | }) -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/activities/ConditionActivity.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityFC, 3 | ConditionActivity as TdConditionActivity, 4 | DesignerCore, IActivity 5 | } from "@trionesdev/approval-process-designer-react"; 6 | import createResource = DesignerCore.createResource; 7 | import {useState} from "react"; 8 | import {Button, Drawer} from "antd"; 9 | 10 | 11 | export const ConditionActivity: ActivityFC = ({...props}) => { 12 | const [open, setOpen] = useState(false) 13 | 14 | 15 | const handleClick = () => { 16 | setOpen(true) 17 | } 18 | 19 | const handleSave = () => { 20 | props.processNode.props = {"days": "3"} 21 | } 22 | 23 | return <> 24 | 25 | { 26 | setOpen(false) 27 | }} 28 | footer={<> } 29 | > 30 | condition drawer 31 | 32 | 33 | } 34 | ConditionActivity.Resource = createResource({ 35 | type: 'CONDITION', 36 | componentName: 'ConditionActivity' 37 | }) -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/activities/Icons.tsx: -------------------------------------------------------------------------------- 1 | export const ApprovalActivityIcon = ( 2 | 4 | 审批人 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | ) 15 | export const RouteActivityIcon = ( 16 | 18 | 条件分支 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | ) 29 | 30 | export const CcActivityIcon = ( 31 | 33 | 抄送人 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | ) -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/activities/RouteActivity.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityFC, 3 | DesignerCore, 4 | IActivity, 5 | RouteActivity as TdRouteActivity 6 | } from "@trionesdev/approval-process-designer-react"; 7 | import createResource = DesignerCore.createResource; 8 | 9 | export const RouteActivity: ActivityFC = TdRouteActivity 10 | 11 | RouteActivity.Resource = createResource({ 12 | icon: 'RouteActivityIcon', 13 | type: 'ROUTE', 14 | componentName: 'RouteActivity', 15 | title: '条件分支', 16 | addable: true 17 | }) -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/activities/StartActivity.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActivityFC, 3 | DesignerCore, 4 | IActivity, 5 | StartActivity as TdStartActivity 6 | } from "@trionesdev/approval-process-designer-react"; 7 | import createResource = DesignerCore.createResource; 8 | 9 | export const StartActivity: ActivityFC = TdStartActivity 10 | StartActivity.Resource = createResource({ 11 | type: 'START', 12 | componentName: 'StartActivity', 13 | title: '发起人' 14 | }) -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/activities/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./StartActivity" 2 | export * from "./ApprovalActivity" 3 | export * from "./RouteActivity" 4 | export * from "./ConditionActivity" 5 | export * from "./CcActivity" -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/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 | 15 | *{ 16 | box-sizing: border-box; 17 | } -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "strictNullChecks": false, 9 | "skipLibCheck": true, 10 | "noImplicitAny": false, 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": false, 23 | "noUnusedParameters": false, 24 | "noFallthroughCasesInSwitch": true, 25 | "noUncheckedSideEffectImports": true, 26 | "paths": { 27 | "@trionesdev/approval-process-designer-react": [ 28 | "../../packages/approval-process-designer-react/src" 29 | ] 30 | } 31 | }, 32 | "include": ["src", "../../packages/approval-process-designer-react/src"] 33 | } 34 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/approval-process-designer-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | import path from 'path'; 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | base: process.env.NODE_ENV === 'production' ?'/triones-approval-process-designer/': '/', 8 | plugins: [react()], 9 | resolve: { 10 | alias: { 11 | '@trionesdev/approval-process-designer-react': path.resolve( 12 | __dirname, 13 | '../../packages/approval-process-designer-react/src', 14 | ), 15 | }, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /images/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trionesdev/triones-approval-process-designer/ec9a62a36e39abbb7210f3e441a2cb69c34f6357/images/img.png -------------------------------------------------------------------------------- /images/shuque_wx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trionesdev/triones-approval-process-designer/ec9a62a36e39abbb7210f3e441a2cb69c34f6357/images/shuque_wx.jpg -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "0.0.1-beta.10", 4 | "npmClient": "pnpm" 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*", 6 | "examples/*" 7 | ], 8 | "scripts": { 9 | "clean": "lerna clean", 10 | "check:types": "tsc --project tsconfig.json --noEmit", 11 | "build": "lerna run build", 12 | "preversion": "pnpm run build", 13 | "version:beta": "lerna version prerelease --preid beta", 14 | "release:github": "ts-node scripts/release release", 15 | "release:force": "lerna publish from-package --yes --registry=https://registry.npmjs.org/", 16 | "prelease:force": "lerna publish from-package --yes --dist-tag next", 17 | "release": "lerna publish --registry=https://registry.npmjs.org/", 18 | "publishOnly:force": "lerna publish from-package --yes --ignore-scripts --no-git-tag-version --registry=https://registry.npmjs.org/", 19 | "publishOnly": "lerna publish --ignore-scripts --no-git-tag-version --force-publish --registry=https://registry.npmjs.org/", 20 | "format": "prettier --write \"**/*.{ts,tsx,js,json,css,less}\"", 21 | "triones:publishOnly": "lerna publish --ignore-scripts --no-git-tag-version --force-publish --registry=https://moensun-npm.pkg.coding.net/npm/moensun/" 22 | }, 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "lerna": "^8.0.0", 26 | "cross-env": "^7.0.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/approval-process-designer-react/.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | esm: {}, 5 | }); 6 | -------------------------------------------------------------------------------- /packages/approval-process-designer-react/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /packages/approval-process-designer-react/README.md: -------------------------------------------------------------------------------- 1 | # @trionesdev/approval-process-designer 2 | 3 | [![NPM version](https://img.shields.io/npm/v/@trionesdev/approval-process-designer.svg?style=flat)](https://npmjs.org/package/@trionesdev/approval-process-designer) 4 | [![NPM downloads](http://img.shields.io/npm/dm/@trionesdev/approval-process-designer.svg?style=flat)](https://npmjs.org/package/@trionesdev/approval-process-designer) 5 | 6 | ## Install 7 | 8 | ```bash 9 | $ yarn install 10 | ``` 11 | 12 | ```bash 13 | $ npm run dev 14 | $ npm run build 15 | ``` 16 | 17 | ## Options 18 | 19 | TODO 20 | 21 | ## LICENSE 22 | 23 | MIT 24 | -------------------------------------------------------------------------------- /packages/approval-process-designer-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trionesdev/approval-process-designer-react", 3 | "version": "0.0.1-beta.10", 4 | "description": "", 5 | "module": "dist/esm/index.js", 6 | "types": "dist/esm/index.d.ts", 7 | "scripts": { 8 | "dev": "father dev", 9 | "build": "father build", 10 | "build:deps": "father prebundle", 11 | "prepublishOnly": "father doctor && npm run build" 12 | }, 13 | "keywords": [], 14 | "authors": [ 15 | "fengxiaotx@163.com" 16 | ], 17 | "license": "MIT", 18 | "files": [ 19 | "dist", 20 | "compiled" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "devDependencies": { 26 | "@types/chance": "^1.1.6", 27 | "@types/lodash": "^4.17.13", 28 | "@types/react": "^18.2.39", 29 | "@types/react-dom": "^18.2.17", 30 | "father": "^4.3.7" 31 | }, 32 | "dependencies": { 33 | "@emotion/react": "^11.11.1", 34 | "@emotion/styled": "^11.11.0", 35 | "@formily/react": "^2.3.0", 36 | "@formily/reactive": "^2.3.0", 37 | "ahooks": "^3.7.8", 38 | "chance": "^1.1.12", 39 | "classnames": "^2.3.2", 40 | "dayjs": "^1.11.10", 41 | "lodash": "^4.17.21", 42 | "rc-tooltip": "^6.1.2", 43 | "react": "^18.2.0", 44 | "react-dom": "^18.2.0" 45 | }, 46 | "gitHead": "ca6185c6f577fcea141e313b4df922b6ab3cb3da" 47 | } 48 | -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/Icons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const RightIcon = ( 4 | 6 | 9 | 10 | ) 11 | 12 | export const PlusIcon = ( 13 | 15 | 16 | 17 | ) 18 | 19 | export const QuestionIcon = ( 20 | 22 | 25 | 26 | ) 27 | 28 | export const CloseIcon = ( 29 | 31 | 34 | 35 | ) -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/Activity.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React, {createRef, FC, useEffect, useState} from "react"; 3 | import classNames from "classnames"; 4 | import {ProcessNode} from "../model"; 5 | import {CloseIcon, RightIcon} from "../Icons"; 6 | import {AddActivityBox} from "./AddActivityBox"; 7 | import {IconWidget} from "../widget/IconWidget"; 8 | import {observer} from "@formily/react"; 9 | 10 | const ActivityStyled = styled('div')({ 11 | boxSizing: 'border-box', 12 | display: 'inline-flex', 13 | flexDirection: 'column', 14 | justifyContent: 'flex-start', 15 | alignItems: 'center', 16 | flexWrap: 'wrap', 17 | width: '100%', 18 | padding: '0 50px', 19 | position: 'relative', 20 | '.activity-box': { 21 | display: 'inline-flex', 22 | flexDirection: 'column', 23 | position: 'relative', 24 | width: '220px', 25 | minHeight: '72px', 26 | flexShrink: 0, 27 | background: '#FFFFFF', 28 | borderRadius: '4px', 29 | cursor: 'pointer', 30 | '&:hover': { 31 | '.editable-title': { 32 | borderBottom: 'dashed 1px #FFFFFF', 33 | }, 34 | '.close': { 35 | display: 'inline-flex!important' 36 | } 37 | }, 38 | '&::after': { 39 | pointerEvents: 'none', 40 | content: '" "', 41 | position: 'absolute', 42 | top: 0, 43 | bottom: 0, 44 | left: 0, 45 | right: 0, 46 | zIndex: 2, 47 | borderRadius: '4px', 48 | border: '1px solid transparent', 49 | transition: 'all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1)', 50 | boxShadow: ' 0 2px 5px 0 rgba(0, 0, 0, 0.1)' 51 | }, 52 | '.header': { 53 | position: 'relative', 54 | display: 'flex', 55 | alignItems: 'center', 56 | paddingLeft: '16px', 57 | paddingRight: '16px', 58 | width: '100%', 59 | height: '24px', 60 | lineHeight: '24px', 61 | fontSize: '12px', 62 | color: '#FFFFFF', 63 | textAlign: 'left', 64 | background: '#576A95', 65 | borderRadius: '4px 4px 0px 0px', 66 | justifyContent: 'space-between', 67 | '.close': { 68 | display: 'none' 69 | }, 70 | 'input': { 71 | outline: 'none' 72 | } 73 | }, 74 | '.editable-title': { 75 | lineHeight: '15px', 76 | overflow: 'hidden', 77 | whiteSpace: 'nowrap', 78 | textOverflow: 'ellipsis' 79 | }, 80 | '.body': { 81 | position: 'relative', 82 | fontSize: '14px', 83 | padding: '16px', 84 | // paddingRight: '30px', 85 | display: 'flex', 86 | justifyContent: 'space-between', 87 | '.text': { 88 | overflow: 'hidden', 89 | textOverflow: 'ellipsis', 90 | display: '-webkit-box' 91 | }, 92 | 'span': { 93 | display: 'inline-flex', 94 | alignItems: 'center', 95 | 'svg': { 96 | width: '1rem', 97 | height: '1rem' 98 | } 99 | } 100 | } 101 | } 102 | }) 103 | 104 | export type ActivityProps = { 105 | children?: React.ReactNode 106 | processNode?: ProcessNode 107 | titleStyle?: React.CSSProperties 108 | titleEditable?: boolean 109 | onChange?: (v: string) => void 110 | closeable?: boolean 111 | onClick?: (processNode: ProcessNode) => void 112 | } 113 | export const Activity: FC = observer(({ 114 | children, 115 | processNode, 116 | titleStyle, 117 | titleEditable, 118 | onChange, 119 | closeable, 120 | onClick, 121 | }) => { 122 | const inputRef = createRef() 123 | const [editing, setEditing] = useState(false) 124 | 125 | const handleClick = () => { 126 | onClick?.(processNode) 127 | } 128 | 129 | const handleSave = (value: any) => { 130 | processNode.title = value 131 | setEditing(false) 132 | } 133 | 134 | const handleInputBlur = (e: any) => { 135 | if (onChange) { 136 | onChange(e.target.value) 137 | } 138 | handleSave(e.target.value) 139 | } 140 | 141 | const handleKeyDown = (e: any) => { 142 | if (e.keyCode == 13) { 143 | handleSave(e.target.value) 144 | } 145 | } 146 | 147 | const handleRemove = (e: any) => { 148 | e.stopPropagation(); 149 | e.preventDefault(); 150 | processNode?.remove() 151 | } 152 | 153 | useEffect(() => { 154 | if (inputRef.current) { 155 | inputRef.current.focus() 156 | } 157 | }, [inputRef]) 158 | 159 | 160 | return 161 |
162 |
163 |
164 |
165 | {editing ? : 167 | { 169 | e.stopPropagation(); 170 | setEditing(true) 171 | }}>{processNode?.title}} 172 | 173 |
174 | {closeable && 175 | } 176 |
177 |
178 |
{processNode.description || `请设置${processNode.title}`}
179 | {React.cloneElement(RightIcon)} 180 |
181 |
182 |
183 | 184 |
185 | }) -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/AddActivityBox.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React, {FC, useRef} from "react"; 3 | import {PlusIcon} from "../Icons"; 4 | import {Popover, Col, Row} from "../components"; 5 | import {observer} from "@formily/react"; 6 | import {useProcessEngine} from "../hooks"; 7 | import {AddActivityItemWidget} from "../widget/AddActivityItemWidget"; 8 | import {ProcessNode} from "../model"; 9 | import {useClickAway} from 'ahooks'; 10 | 11 | const AddActivityBoxStyled = styled('div')({ 12 | width: '240px', 13 | display: 'inline-flex', 14 | flexShrink: 0, 15 | position: 'relative', 16 | '&::before': { 17 | content: '" "', 18 | position: 'absolute', 19 | top: 0, 20 | left: 0, 21 | right: 0, 22 | bottom: 0, 23 | // zIndex: -1, 24 | margin: 'auto', 25 | width: '2px', 26 | height: '100%', 27 | backgroundColor: `#CACACA` 28 | }, 29 | '.add-activity-btn': { 30 | userSelect: 'none', 31 | width: '240px', 32 | padding: '20px 0px 32px', 33 | display: 'flex', 34 | justifyContent: 'center', 35 | flexShrink: 0, 36 | flexGrow: 1, 37 | zIndex: 1, 38 | 'button': { 39 | border: 'none', 40 | borderRadius: '50px', 41 | height: '32px', 42 | width: '32px', 43 | backgroundColor: '#1677ff', 44 | cursor: 'pointer', 45 | display: 'inline-flex', 46 | justifyContent: 'center', 47 | alignItems: 'center', 48 | 'span': { 49 | display: 'inline-flex', 50 | justifyContent: 'center', 51 | alignItems: 'center', 52 | 'svg': { 53 | color: '#fff', 54 | fontSize: '16px' 55 | } 56 | } 57 | } 58 | } 59 | }) 60 | 61 | type AddActivityBoxProps = { 62 | processNode: ProcessNode; 63 | } 64 | 65 | export const AddActivityBox: FC = observer(({ 66 | processNode 67 | }) => { 68 | const btnRef = useRef() 69 | const [open, setOpen] = React.useState(false) 70 | const popoverRef = useRef(null); 71 | 72 | const engine = useProcessEngine() 73 | const {addableActivityResources} = engine 74 | 75 | 76 | useClickAway((e: any) => { 77 | if (btnRef.current.contains(e.target)) { 78 | 79 | } else { 80 | if (open) { 81 | setOpen(false) 82 | } 83 | } 84 | 85 | }, popoverRef); 86 | 87 | return 88 |
89 | 91 | {addableActivityResources?.map((resource) => { 92 | return 93 | { 94 | setOpen(false) 95 | }}/> 96 | 97 | }) || []} 98 | }> 99 | 102 | 103 |
104 |
105 | }) -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/ApprovalActivity.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react"; 2 | import {ProcessNode} from "../model"; 3 | import {Activity} from "./Activity"; 4 | import {IActivity} from "../types"; 5 | 6 | type ApprovalActivityProps = IActivity 7 | export const ApprovalActivity: FC = ({ 8 | processNode, 9 | nextActivity, 10 | onClick 11 | }) => { 12 | return <> 13 | 14 | {nextActivity} 15 | 16 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/BranchBox.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React, {FC} from "react"; 3 | import classNames from "classnames"; 4 | 5 | const coverLineColor = '#F0F2F5' 6 | const BranchBoxStyled = styled('div')({ 7 | backgroundColor: coverLineColor, 8 | display: 'inline-flex', 9 | flexDirection: 'column', 10 | alignItems: 'center', 11 | position: 'relative', 12 | '&::before': { 13 | content: '" "', 14 | position: 'absolute', 15 | top: 0, 16 | left: 0, 17 | right: 0, 18 | bottom: 0, 19 | zIndex: 0, 20 | margin: 'auto', 21 | width: '2px', 22 | height: '100%', 23 | backgroundColor: `#CACACA` 24 | }, 25 | '.top-left-cover-line': { 26 | position: 'absolute', 27 | height: '3px', 28 | width: '50%', 29 | backgroundColor: coverLineColor, 30 | top: '-2px', 31 | left: '-1px', 32 | }, 33 | '.bottom-left-cover-line': { 34 | position: 'absolute', 35 | height: '3px', 36 | width: '50%', 37 | backgroundColor: coverLineColor, 38 | bottom: '-2px', 39 | left: '-1px', 40 | }, 41 | '.top-right-cover-line': { 42 | position: 'absolute', 43 | height: '3px', 44 | width: '50%', 45 | backgroundColor: coverLineColor, 46 | top: '-2px', 47 | right: '-1px', 48 | }, 49 | '.bottom-right-cover-line': { 50 | position: 'absolute', 51 | height: '3px', 52 | width: '50%', 53 | backgroundColor: coverLineColor, 54 | bottom: '-2px', 55 | right: '-1px', 56 | }, 57 | }) 58 | 59 | type BranchBoxProps = { 60 | children?: React.ReactNode | React.ReactNode[] | any, 61 | firstCol?: boolean, 62 | lastCol?: boolean 63 | } 64 | 65 | export const BranchBox: FC = ({ 66 | children, 67 | firstCol, 68 | lastCol, 69 | }) => { 70 | return 71 | {firstCol && <> 72 |
73 |
74 | } 75 | {lastCol && <> 76 |
77 |
78 | } 79 | {children} 80 | 81 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/CcActivity.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react" 2 | import {Activity} from "./Activity"; 3 | import {IActivity} from "../types"; 4 | 5 | type CcActivityProps = IActivity 6 | 7 | export const CcActivity: FC = ({ 8 | processNode, 9 | nextActivity, 10 | onClick 11 | }) => { 12 | return <> 13 | 15 | {nextActivity} 16 | 17 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/CondiitionActivity.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React, {createRef, FC, useEffect, useState} from "react"; 3 | import classNames from "classnames"; 4 | import {AddActivityBox} from "./AddActivityBox"; 5 | import {BranchBox} from "./BranchBox"; 6 | import {CloseIcon, QuestionIcon} from "../Icons"; 7 | import {Tooltip} from "../components"; 8 | import {IconWidget} from "../widget/IconWidget"; 9 | import {IActivity} from "../types"; 10 | 11 | const ConditionActivityStyled = styled('div')({ 12 | boxSizing: 'border-box', 13 | minHeight: '220px', 14 | display: 'inline-flex', 15 | flexDirection: 'column', 16 | '.condition-activity-wrapper': { 17 | marginTop: '30px', 18 | paddingRight: '50px', 19 | paddingLeft: '50px', 20 | display: 'inline-flex', 21 | alignItems: 'center', 22 | flexDirection: 'column', 23 | flexGrow: 1, 24 | position: 'relative', 25 | '&::before': { 26 | content: '" "', 27 | position: 'absolute', 28 | top: 0, 29 | left: 0, 30 | right: 0, 31 | bottom: 0, 32 | margin: 'auto', 33 | width: '2px', 34 | height: '100%', 35 | backgroundColor: `#CACACA` 36 | }, 37 | '.condition-activity-box': { 38 | boxSizing: 'border-box', 39 | position: 'relative', 40 | width: '220px', 41 | minHeight: '72px', 42 | background: '#FFFFFF', 43 | borderRadius: '4px', 44 | padding: '14px 19px', 45 | cursor: 'pointer', 46 | '&::after': { 47 | pointerEvents: 'none', 48 | content: '" "', 49 | position: 'absolute', 50 | top: 0, 51 | bottom: 0, 52 | left: 0, 53 | right: 0, 54 | zIndex: 2, 55 | borderRadius: '4px', 56 | border: '1px solid transparent', 57 | transition: 'all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1)', 58 | boxShadow: ' 0 2px 5px 0 rgba(0, 0, 0, 0.1)' 59 | }, 60 | '&:hover': { 61 | '.editable-title': { 62 | borderBottom: 'dashed 1px #FFFFFF', 63 | borderColor: '#15BC83' 64 | }, 65 | '.priority-title': { 66 | display: 'none!important', 67 | }, 68 | '.close': { 69 | display: 'inline-flex!important' 70 | } 71 | }, 72 | '.header': { 73 | display: 'flex', 74 | width: '1005', 75 | justifyContent: 'space-between', 76 | alignItems: 'center', 77 | position: 'relative', 78 | fontSize: '12px', 79 | color: '#15BC83', 80 | textAlign: 'left', 81 | lineHeight: '16px', 82 | 'input': { 83 | outline: 'none' 84 | }, 85 | '.default-title': { 86 | color: 'rgba(25, 31, 37, 0.56)', 87 | display: 'inline-flex', 88 | alignItems: 'center', 89 | gap: '4px' 90 | }, 91 | '.editable-title': { 92 | lineHeight: '15px', 93 | overflow: 'hidden', 94 | whiteSpace: 'nowrap', 95 | textOverflow: 'ellipsis', 96 | // borderBottom: 'dashed 1px transparent' 97 | }, 98 | '.priority-title': { 99 | display: 'inline-block', 100 | float: 'right', 101 | marginRight: '10px', 102 | color: 'rgba(25, 31, 37, 0.56)' 103 | }, 104 | '.close': { 105 | display: 'none', 106 | width: '14px', 107 | height: '14px', 108 | position: 'absolute', 109 | right: '-2px', 110 | top: '-2px', 111 | fontSize: '14px', 112 | textAlign: 'center', 113 | lineHeight: '20px', 114 | zIndex: 2, 115 | color: 'rgba(25, 31, 37, 0.56)' 116 | } 117 | }, 118 | '.body': { 119 | position: 'relative', 120 | fontSize: '14px', 121 | // padding: '16px', 122 | // paddingRight: '30px', 123 | display: 'flex', 124 | marginTop: '6px', 125 | justifyContent: 'space-between', 126 | '.description': { 127 | overflow: 'hidden', 128 | textOverflow: 'ellipsis', 129 | display: '-webkit-box', 130 | whiteSpace: 'pre' 131 | } 132 | } 133 | } 134 | } 135 | }) 136 | 137 | type ConditionActivityProps = IActivity 138 | 139 | export const ConditionActivity: FC = ({ 140 | processNode, 141 | nextActivity, 142 | onClick 143 | }) => { 144 | const inputRef = createRef() 145 | const [editing, setEditing] = useState(false) 146 | 147 | const handleOnClick = (e: any) => { 148 | if (processNode?.defaultCondition) { 149 | return 150 | } 151 | onClick?.(processNode) 152 | } 153 | 154 | const handleInputBlur = (e: any) => { 155 | setEditing(false) 156 | } 157 | 158 | const handleRemove = () => { 159 | processNode.remove() 160 | } 161 | 162 | useEffect(() => { 163 | if (inputRef.current) { 164 | inputRef.current.focus() 165 | } 166 | }, [inputRef]) 167 | 168 | 169 | return 170 | 171 |
172 |
173 |
174 | { 175 | processNode?.defaultCondition ? 176 | <> 177 | 默认条件 178 | 180 | {React.cloneElement(QuestionIcon)} 182 | 183 | 184 | 优先级{(processNode.index || 0) + 1} 186 | : 187 | <>{ 188 | editing ? : <> 191 | setEditing(true)}>{processNode?.title || `条件${(processNode.index || 0) + 1}`} 193 | 优先级{(processNode.index || 0) + 1} 195 | 197 | 198 | } 199 | } 200 |
201 |
202 |
{processNode?.defaultCondition ? '其他条件进入此流程' : (processNode.description || '请设置条件')}
204 |
205 |
206 | 207 |
208 | {nextActivity} 209 |
210 |
211 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/EndActivity.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import styled from "@emotion/styled"; 3 | 4 | const EndActivityStyled = styled('div')({ 5 | 6 | '&.end-activity': { 7 | borderRadius: '50%', 8 | fontSize: '14px', 9 | color: `rgba(25, 31, 37, 0.4)`, 10 | '.end-activity-circle': { 11 | width: '10px', 12 | height: '10px', 13 | margin: 'auto', 14 | borderRadius: '50%', 15 | background: '#DBDCDC' 16 | }, 17 | '.end-activity-text': { 18 | marginTop: '5px', 19 | textAlign: 'center' 20 | } 21 | } 22 | 23 | }) 24 | 25 | export const EndActivity = () => { 26 | return 27 |
28 |
流程结束
29 | 30 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/RouteActivity.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react" 2 | import {RouteBranches} from "./RouteBranches"; 3 | import styled from "@emotion/styled"; 4 | import {IActivity} from "../types"; 5 | 6 | const RouteActivityStyled = styled('div')({ 7 | display: 'flex', 8 | overflow: 'visible', 9 | minWidth: '220px', 10 | minHeight: '180px', 11 | height: 'auto', 12 | borderBottom: '2px solid #cccccc', 13 | borderTop: '2px solid #cccccc', 14 | position: 'relative', 15 | marginTop: '15px', 16 | '.add-branch': { 17 | border: 'none', 18 | outline: 'none', 19 | userSelect: 'none', 20 | justifyContent: 'center', 21 | fontSize: '12px', 22 | padding: '0 10px', 23 | height: '30px', 24 | lineHeight: '30px', 25 | borderRadius: '15px', 26 | color: '#0089FF', 27 | background: '#fff', 28 | boxShadow: '0 2px 4px 0 rgba(0, 0, 0, 0.1)', 29 | position: 'absolute', 30 | top: '-16px', 31 | left: '50%', 32 | transform: 'translateX(-50%)', 33 | transformOrigin: 'center center', 34 | cursor: 'pointer', 35 | zIndex: 1, 36 | display: 'inline-flex', 37 | alignItems: 'center', 38 | transition: 'all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1)' 39 | 40 | } 41 | }) 42 | 43 | type RouteActivityProps = IActivity & { 44 | children?: React.ReactNode, 45 | } 46 | 47 | export const RouteActivity: FC = ({children, processNode, nextActivity}) => { 48 | 49 | const handleAddBranch = () => { 50 | processNode.addConditionBranch() 51 | } 52 | 53 | return <> 54 | 55 | 56 | 57 | {children} 58 | 59 | 60 | {nextActivity} 61 | 62 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/RouteBranches.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React, {FC} from "react"; 3 | import {AddActivityBox} from "./AddActivityBox"; 4 | import {ProcessNode} from "../model"; 5 | 6 | const RouteBranchesStyled = styled('div')({ 7 | display: 'flex', 8 | flexDirection: 'column', 9 | flexWrap: 'wrap', 10 | alignItems: 'center', 11 | minHeight: '270px', 12 | width: '100%', 13 | flexShrink: 0, 14 | }) 15 | 16 | type RouteBranchesProps = { 17 | children?: React.ReactNode 18 | processNode?: ProcessNode 19 | } 20 | 21 | export const RouteBranches: FC = ({children, processNode}) => { 22 | return 23 | {children} 24 | 25 | 26 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/StartActivity.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react" 2 | import {Activity} from "./Activity"; 3 | import {IActivity} from "../types"; 4 | 5 | type StartActivityProps = IActivity 6 | 7 | export const StartActivity: FC = ({ 8 | processNode, 9 | nextActivity, 10 | onClick 11 | 12 | }) => { 13 | return <> 14 | 15 | {nextActivity} 16 | 17 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/activity/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Activity" 2 | export * from "./StartActivity" 3 | export * from "./EndActivity" 4 | export * from "./ApprovalActivity" 5 | export * from "./RouteActivity" 6 | export * from "./CondiitionActivity" 7 | export * from "./CcActivity" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/components/Popover/Popover.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react" 2 | import RcTooltip from "rc-tooltip" 3 | import styled from "@emotion/styled"; 4 | import {css, Global} from "@emotion/react"; 5 | import {TooltipProps as RcTooltipProps} from "rc-tooltip/es/Tooltip"; 6 | 7 | const prefixCls = 'td-popover' 8 | const popoverCss = css({ 9 | [`.${prefixCls}`]: { 10 | position: 'absolute', 11 | zIndex: 1070, 12 | display: 'block', 13 | visibility: 'visible', 14 | lineHeight: 1.5, 15 | fontSize: '12px', 16 | backgroundColor: '#ffffff', 17 | borderRadius:'6px', 18 | padding: '0px', 19 | opacity: .9, 20 | // width: 'max-content', 21 | minWidth: '250px', 22 | color: 'rgba(255,255,255)', 23 | transformOrigin: 'var(--arrow-x, 50%) var(--arrow-y, 50%)', 24 | boxShadow:'0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)', 25 | [`&-hidden`]: { 26 | display: 'none' 27 | }, 28 | [`&-arrow`]: { 29 | zIndex: 1, 30 | width: '16px', 31 | height: '16px', 32 | display: 'block', 33 | overflow: 'hidden', 34 | '&::before': { 35 | position: 'absolute', 36 | bottom: 0, 37 | insetInlineStart: 0, 38 | content: '" "', 39 | width: '16px', 40 | height: '8px', 41 | backgroundColor: 'rgba(255,255,255)', 42 | clipPath: 'path(\'M 0 8 A 4 4 0 0 0 2.82842712474619 6.82842712474619 L 6.585786437626905 3.0710678118654755 A 2 2 0 0 1 9.414213562373096 3.0710678118654755 L 13.17157287525381 6.82842712474619 A 4 4 0 0 0 16 8 Z\')', 43 | pointerEvents: 'none' 44 | }, 45 | '&::after': { 46 | position: 'absolute', 47 | content: '" "', 48 | width: '8.970562748477143px', 49 | height: '8.970562748477143px', 50 | bottom: 0, 51 | insetInline: 0, 52 | margin: 0, 53 | borderRadius: '0 0 2px 0', 54 | transform: 'translateY(50%) rotate(-135deg)', 55 | background: 'transparent', 56 | boxShadow: '2px 2px 5px rgba(0, 0, 0)' 57 | } 58 | }, 59 | [`&-placement-rightTop &-arrow`]:{ 60 | transform:' translateX(-100%) rotate(-90deg)' 61 | }, 62 | [`$-content`]: { 63 | position: 'relative', 64 | margin: 0, 65 | padding: 0 66 | }, 67 | [`&-inner`]: { 68 | textAlign: 'start', 69 | textDecoration: 'none', 70 | wordWrap: 'break-word', 71 | backgroundColor: 'rgba(255,255,255)', 72 | color: '#ffffff', 73 | padding: '6px 8px', 74 | minWidth: '32px', 75 | minHeight: '32px', 76 | borderRadius: '6px', 77 | boxSizing: 'border-box' 78 | } 79 | } 80 | }) 81 | 82 | const PopoverStyled = styled(RcTooltip)((props) => { 83 | return {} 84 | }) 85 | 86 | type PopoverProps = { 87 | content?: React.ReactNode 88 | } & Omit 89 | 90 | export const Popover: FC = ({ 91 | children, 92 | content, 93 | ...props 94 | }) => { 95 | const overlay =
{content}
96 | return <> 97 | 98 | 102 | {children} 103 | 104 | 105 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/components/Popover/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Popover" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/components/Row/Col.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react"; 2 | import styled from "@emotion/styled"; 3 | import classnames from "classnames" 4 | 5 | type Span = 0 | 2 | 3 | 4 | 6 | 8 | 12 6 | 7 | 8 | const ColStyled = styled('div')((props) => { 9 | 10 | return { 11 | flex: `0 0 ${props.style.width}`, 12 | maxWidth: `${props.style.width}`, 13 | boxSizing: 'border-box', 14 | } 15 | }) 16 | 17 | type ColProps = { 18 | children?: React.ReactNode; 19 | className?: string; 20 | style?: React.CSSProperties; 21 | span?: Span; 22 | } 23 | 24 | export const Col: FC = ({ 25 | children, 26 | className, 27 | style, 28 | span 29 | }) => { 30 | const width = span ? `${100 / (24 / span)}%` : '100%' 31 | return 32 |
{children}
33 |
34 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/components/Row/Row.tsx: -------------------------------------------------------------------------------- 1 | import React, {forwardRef} from "react"; 2 | import styled from "@emotion/styled"; 3 | import _ from "lodash"; 4 | 5 | const RowStyled = styled.div` 6 | display: flex; 7 | flex-flow: row wrap; 8 | min-width: 0; 9 | ` 10 | 11 | type RowProps = { 12 | children?: React.ReactNode; 13 | className?: string; 14 | style?: React.CSSProperties; 15 | gutter?: number | number[] 16 | } 17 | 18 | export const Row = forwardRef(({ 19 | children, 20 | className, 21 | style, 22 | gutter 23 | }: RowProps, ref: any) => { 24 | let rowGap: number = 0; 25 | let columnGap: number = 0; 26 | let margin: number = 0; 27 | if (_.isArray(gutter)) { 28 | columnGap = gutter[0] / 2 29 | rowGap = gutter[1] 30 | margin = 0 - gutter[0] / 2 31 | } else if (typeof gutter === "number") { 32 | columnGap = gutter / 2 33 | rowGap = gutter 34 | margin = 0 - gutter / 2 35 | } 36 | 37 | const handleRender = (children: React.ReactNode) => { 38 | const childArray = React.Children.toArray(children); 39 | return childArray.map((child: React.ReactNode) => { 40 | if (React.isValidElement(child)) { 41 | return React.cloneElement(child, _.merge({}, child.props, { 42 | style: { 43 | background: "white", 44 | ...child.props.style, 45 | paddingLeft: columnGap, paddingRight: columnGap 46 | }, ...child.props 47 | })) 48 | } 49 | }) 50 | } 51 | 52 | return {handleRender(children)} 57 | }) -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/components/Row/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Row" 2 | export * from "./Col" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react" 2 | import RcTooltip from "rc-tooltip" 3 | import styled from "@emotion/styled"; 4 | import {css, Global} from "@emotion/react"; 5 | import {TooltipProps as RcTooltipProps} from "rc-tooltip/es/Tooltip"; 6 | 7 | const prefixCls = 'td-tooltip' 8 | const tooltipCss = css({ 9 | [`.${prefixCls}`]: { 10 | position: 'absolute', 11 | zIndex: 1070, 12 | display: 'block', 13 | visibility: 'visible', 14 | lineHeight: 1.5, 15 | fontSize: '12px', 16 | backgroundColor: '#0000000d', 17 | padding: '0px', 18 | opacity: .9, 19 | width: 'max-content', 20 | maxWidth: '250px', 21 | color: 'rgba(0,0,0,0.88)', 22 | boxShadow:'0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)', 23 | [`&-hidden`]: { 24 | display: 'none' 25 | }, 26 | [`&-arrow`]: { 27 | transform: ' translateX(-50%) translateY(100%) rotate(180deg)', 28 | zIndex: 1, 29 | width: '16px', 30 | height: '16px', 31 | display: 'block', 32 | overflow: 'hidden', 33 | '&::before': { 34 | position: 'absolute', 35 | bottom: 0, 36 | insetInlineStart: 0, 37 | content: '" "', 38 | width: '16px', 39 | height: '8px', 40 | backgroundColor: 'rgba(0,0,0,0.85)', 41 | clipPath: 'path(\'M 0 8 A 4 4 0 0 0 2.82842712474619 6.82842712474619 L 6.585786437626905 3.0710678118654755 A 2 2 0 0 1 9.414213562373096 3.0710678118654755 L 13.17157287525381 6.82842712474619 A 4 4 0 0 0 16 8 Z\')', 42 | pointerEvents: 'none' 43 | }, 44 | '&::after': { 45 | position: 'absolute', 46 | content: '" "', 47 | width: '8.970562748477143px', 48 | height: '8.970562748477143px', 49 | bottom: 0, 50 | insetInline: 0, 51 | margin: 0, 52 | borderRadius: '0 0 2px 0', 53 | transform: 'translateY(50%) rotate(-135deg)', 54 | background: 'transparent', 55 | boxShadow: '2px 2px 5px rgba(0, 0, 0, 0.05)' 56 | } 57 | }, 58 | [`$-content`]: { 59 | position: 'relative', 60 | margin:0, 61 | padding:0 62 | }, 63 | [`&-inner`]: { 64 | textAlign: 'start', 65 | textDecoration: 'none', 66 | wordWrap: 'break-word', 67 | backgroundColor: 'rgba(0,0,0,0.85)', 68 | color: '#ffffff', 69 | padding: '6px 8px', 70 | minWidth: '32px', 71 | minHeight: '32px', 72 | borderRadius: '6px', 73 | boxSizing: 'border-box' 74 | }, 75 | } 76 | }) 77 | 78 | const TooltipStyled = styled(RcTooltip)((props) => { 79 | return {} 80 | }) 81 | 82 | type TooltipProps = { 83 | title?: React.ReactNode 84 | } & Omit 85 | 86 | export const Tooltip: FC = ({ 87 | children, 88 | title, 89 | ...props 90 | }) => { 91 | return <> 92 | 93 | 94 | {children} 95 | 96 | 97 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Tooltip" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./Tooltip" 2 | export * from "./Popover" 3 | export * from "./Row" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/container/ApprovalProcessDesigner.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC, useEffect, useMemo, useState} from "react" 2 | import {ApprovalProcessContext} from "../context"; 3 | import {ApprovalProcessEngine} from "../model/ApprovalProcessEngine"; 4 | import {IProcessNode} from "../model"; 5 | import _ from "lodash"; 6 | 7 | type ApprovalProcessDesignerProps = { 8 | children?: React.ReactNode; 9 | engine?: ApprovalProcessEngine 10 | value?: IProcessNode 11 | onChange?: (value: any) => void 12 | } 13 | 14 | export const ApprovalProcessDesigner: FC = ({ 15 | children, 16 | engine, 17 | value, 18 | onChange 19 | }) => { 20 | const [scopeValue, setScopeValue] = useState(value) 21 | 22 | 23 | let designerEngine = useMemo(() => { 24 | let scopeEngine = engine; 25 | if (!engine) { 26 | scopeEngine = new ApprovalProcessEngine({value: value}); 27 | } 28 | return scopeEngine 29 | }, [engine]) 30 | 31 | designerEngine?.setOnchange((value: any) => { 32 | if (!_.isEqual(scopeValue, value)) { 33 | setScopeValue(value) 34 | onChange?.(value) 35 | } 36 | }) 37 | 38 | 39 | useEffect(() => { 40 | if (value && !_.isEqual(value, scopeValue)) { 41 | designerEngine.process.from(value) 42 | } 43 | }, [value]) 44 | 45 | return 46 | {children} 47 | 48 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/container/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ApprovalProcessDesigner" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/context.tsx: -------------------------------------------------------------------------------- 1 | import {createContext} from "react"; 2 | import {ApprovalProcessEngine} from "./model/ApprovalProcessEngine"; 3 | import {IActivities} from "./types"; 4 | import {ProcessNode} from "./model"; 5 | 6 | export const ApprovalProcessContext = createContext(null) 7 | 8 | export const ActivitiesContext = createContext(null) 9 | 10 | export const ProcessNodeContext = createContext(null) -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/hooks/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./useProcessEngine" 2 | export * from "./useActivities" 3 | export * from "./useProcess" 4 | export * from "./useProcessNode" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/hooks/useActivities.tsx: -------------------------------------------------------------------------------- 1 | import {useContext} from "react"; 2 | import {ActivitiesContext} from "../context"; 3 | 4 | export const useActivities = () => { 5 | return useContext(ActivitiesContext) 6 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/hooks/useProcess.tsx: -------------------------------------------------------------------------------- 1 | import {useProcessEngine} from "./useProcessEngine"; 2 | 3 | export const useProcess = () => { 4 | return useProcessEngine().process 5 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/hooks/useProcessEngine.tsx: -------------------------------------------------------------------------------- 1 | import {useContext} from "react"; 2 | import {ApprovalProcessContext} from "../context"; 3 | 4 | export const useProcessEngine = () => { 5 | return useContext(ApprovalProcessContext) 6 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/hooks/useProcessNode.tsx: -------------------------------------------------------------------------------- 1 | import {useContext} from "react"; 2 | import {ProcessNodeContext} from "../context"; 3 | 4 | export const useProcessNode = () => { 5 | return useContext(ProcessNodeContext) 6 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./model" 2 | export * from "./container" 3 | export * from "./activity" 4 | export * from "./widget" 5 | export * from "./panel" 6 | export * from "./types" 7 | export * from "./util" 8 | export * from "./store" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/model/ApprovalProcessEngine.ts: -------------------------------------------------------------------------------- 1 | import {IProcessNode, ProcessNode} from "./ProcessNode"; 2 | import {define, observable} from "@formily/reactive"; 3 | import {GlobalStore} from "../store"; 4 | import {DesignerCore} from "../util"; 5 | import _ from "lodash"; 6 | 7 | interface IApprovalProcessEngine { 8 | value?: IProcessNode 9 | } 10 | 11 | export class ApprovalProcessEngine { 12 | process: ProcessNode; 13 | onChange?: (value: any) => void 14 | 15 | constructor(engine?: IApprovalProcessEngine) { 16 | this.process = new ProcessNode({ 17 | engine: this, 18 | type: 'START', 19 | componentName: 'StartActivity', 20 | title: '开始' 21 | }) 22 | if (engine?.value) { 23 | this.process.from(engine.value) 24 | } 25 | this.makeObservable() 26 | } 27 | 28 | makeObservable() { 29 | define(this, { 30 | process: observable, 31 | addableActivityResources: observable.computed 32 | }) 33 | } 34 | 35 | handleChange = _.debounce((msg: any) => { 36 | this.onChange?.(DesignerCore.transformToSchema(this.process)) 37 | }, 0) 38 | 39 | setOnchange(fn: (value: any) => void) { 40 | this.onChange = fn 41 | } 42 | 43 | get addableActivityResources() { 44 | return GlobalStore.getAddableActivityResources() 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/model/ProcessNode.ts: -------------------------------------------------------------------------------- 1 | import {define, observable, observe, reaction} from "@formily/reactive"; 2 | import Chance from 'chance'; 3 | import _ from "lodash"; 4 | import {GlobalStore} from "../store"; 5 | import {ApprovalProcessEngine} from "./ApprovalProcessEngine"; 6 | 7 | const chance = new Chance(); 8 | 9 | export type ProcessNodeType = 'START' | 'ROUTE' | 'CONDITION' | 'APPROVAL' | 'CC' | 'END' 10 | 11 | export interface IProcessNode { 12 | engine?: ApprovalProcessEngine 13 | isSourceNode?: boolean 14 | id?: string 15 | type: ProcessNodeType 16 | componentName?: string 17 | nextNode?: IProcessNode 18 | conditionNodes?: IProcessNode[] 19 | title?: string 20 | description?: string 21 | props?: any 22 | defaultCondition?: boolean 23 | } 24 | 25 | const ProcessNodes = new Map() 26 | 27 | export class ProcessNode { 28 | engine: ApprovalProcessEngine 29 | isSourceNode: boolean 30 | id: string 31 | type: ProcessNodeType 32 | componentName: string 33 | prevNodeId: string 34 | nextNode: ProcessNode 35 | conditionNodes: ProcessNode[] 36 | title: string 37 | description: string 38 | props: any 39 | defaultCondition?: boolean 40 | 41 | constructor(node: IProcessNode, parentNode?: ProcessNode) { 42 | this.engine = node.engine 43 | this.isSourceNode = node.isSourceNode 44 | this.id = node.id || `Activity_${chance.string({length: 10, alpha: true})}` 45 | this.type = node.type 46 | this.componentName = node.componentName || node.type 47 | this.prevNodeId = parentNode?.id 48 | this.nextNode = null 49 | this.conditionNodes = [] 50 | this.title = node.title 51 | this.description = node.description 52 | this.props = node.props 53 | this.engine = parentNode?.engine 54 | this.defaultCondition = node.defaultCondition 55 | 56 | ProcessNodes.set(this.id, this) 57 | if (node) { 58 | this.from(node) 59 | } 60 | this.makeObservable() 61 | } 62 | 63 | makeObservable() { 64 | define(this, { 65 | prevNodeId: observable.ref, 66 | title: observable.ref, 67 | description: observable.ref, 68 | nextNode: observable.ref, 69 | conditionNodes: observable.shallow, 70 | props: observable, 71 | defaultCondition: observable.ref 72 | }) 73 | 74 | reaction(() => { 75 | return this.prevNodeId + this.title + this.description + this.nextNode?.id + this.conditionNodes.length 76 | }, () => { 77 | if (!this.isSourceNode) { 78 | this.engine.handleChange(`${this.id} something changed`) 79 | } 80 | }) 81 | 82 | observe(this.props, (change) => { 83 | if (!this.isSourceNode) { 84 | this.engine.handleChange(`${this.id} props changed`) 85 | } 86 | }) 87 | 88 | } 89 | 90 | setNextNode(node: ProcessNode) { 91 | if (!node) { 92 | this.nextNode = null 93 | return 94 | } 95 | 96 | 97 | node.nextNode = this.nextNode 98 | node.prevNodeId = this.id 99 | this.nextNode = node 100 | 101 | } 102 | 103 | setConditionNodes(nodes: ProcessNode[]) { 104 | if (_.isEmpty(nodes)) { 105 | return 106 | } 107 | _.forEach(nodes, (node) => { 108 | node.prevNodeId = this.id 109 | }) 110 | this.conditionNodes = nodes 111 | } 112 | 113 | from(node?: IProcessNode) { 114 | if (!node) return 115 | if (node.id && node.id !== this.id) { 116 | ProcessNodes.delete(this.id) 117 | ProcessNodes.set(node.id, this) 118 | this.id = node.id 119 | } 120 | this.type = node.type 121 | this.componentName = node.componentName || node.type 122 | this.title = node.title 123 | this.description = node.description 124 | this.props = node.props ?? {} 125 | if (node.engine) { 126 | this.engine = node.engine 127 | } 128 | 129 | if (node.nextNode) { 130 | this.nextNode = new ProcessNode(node.nextNode, this) 131 | } 132 | if (node.conditionNodes && node.conditionNodes.length > 0) { 133 | this.conditionNodes = node.conditionNodes?.map((node) => { 134 | return new ProcessNode(node, this) 135 | }) || [] 136 | } 137 | } 138 | 139 | clone(parentNode?: ProcessNode) { 140 | const node = new ProcessNode({ 141 | type: this.type, 142 | componentName: this.componentName, 143 | title: this.title, 144 | description: this.description, 145 | props: _.cloneDeep(this.props), 146 | }, parentNode) 147 | if (this.type == 'ROUTE') { 148 | const conditionResource = GlobalStore.getConditionActivityResource() 149 | const firstCondition = conditionResource.node.clone(node) 150 | const conditionDefault = conditionResource.node.clone(node) 151 | // conditionDefault.props = _.assign(conditionDefault.props, {defaultCondition: true}) 152 | conditionDefault.defaultCondition = true 153 | node.setConditionNodes([firstCondition, conditionDefault]) 154 | } 155 | return node 156 | } 157 | 158 | cloneDeep(node?: ProcessNode) { 159 | if (!node) { 160 | return 161 | } 162 | const cloneNode = _.cloneDeep(node) 163 | ProcessNodes.set(cloneNode.id, cloneNode) 164 | return cloneNode; 165 | } 166 | 167 | remove() { 168 | const parentNode = ProcessNodes.get(this.prevNodeId) 169 | 170 | if (this.type == "CONDITION") { //当前节点是条件节点 171 | const linkedIds = this.collectLinkIds() 172 | if (parentNode.conditionNodes.length > 2) { //当分支超过2个时,只需要删除当前节点,否则,清除整个路由节点 173 | parentNode.conditionNodes = _.filter(parentNode.conditionNodes, (conditionNode: any) => { 174 | return conditionNode.id !== this.id 175 | }) 176 | } else { 177 | //只有2个分支的时候,删除当前分支所有链路节点,另一分支,如果有除了条件节点之外的节点,则保留,否则,清除整个路由节点 178 | const parentParentNode = ProcessNodes.get(parentNode.prevNodeId) //条件节点的父节点是路由节点,如果清除整个路由,需要找到父节点的父节点 179 | const parentNodeNextNode = parentNode.nextNode 180 | parentParentNode.setNextNode(null) //这里要断开节点链,否则可能造成递归渲染 181 | 182 | 183 | linkedIds.push(parentNode.id); 184 | const remainNode = _.find(parentNode.conditionNodes, (conditionNode: any) => { 185 | return conditionNode.id !== this.id 186 | }) 187 | const {deleteIds, startNode, endNode} = this.processRaminRouteBranch(remainNode) 188 | linkedIds.push(...deleteIds) 189 | if (startNode) { 190 | parentParentNode.setNextNode(startNode) 191 | endNode.setNextNode(parentNodeNextNode) 192 | } else { 193 | parentParentNode?.setNextNode(parentNodeNextNode) 194 | } 195 | } 196 | _.forEach(linkedIds, (id: string) => { 197 | ProcessNodes.delete(id) 198 | }) 199 | } else { 200 | if (this.nextNode) { 201 | this.nextNode.prevNodeId = this.prevNodeId 202 | } 203 | parentNode.nextNode = this.nextNode 204 | ProcessNodes.delete(this.id) 205 | } 206 | } 207 | 208 | /** 209 | * 添加条件分支 210 | */ 211 | addConditionBranch() { 212 | if (this.type !== "ROUTE") { 213 | return 214 | } 215 | const conditionActivity = GlobalStore.getConditionActivityResource()?.node.clone(this) 216 | if (conditionActivity) { 217 | const newChildren = _.concat(this.conditionNodes.slice(0, this.conditionNodes.length - 1), conditionActivity, this.conditionNodes.slice(this.conditionNodes.length - 1)) 218 | this.setConditionNodes(newChildren) 219 | } 220 | } 221 | 222 | /** 223 | * 获取下面链路上的所有节点id 224 | */ 225 | collectLinkIds() { 226 | let ids = [] 227 | if (this.nextNode) { 228 | ids.push(this.nextNode.id) 229 | ids = ids.concat(this.nextNode.collectLinkIds()) 230 | } 231 | if (this.conditionNodes && this.conditionNodes.length > 0) { 232 | this.conditionNodes.forEach(child => { 233 | ids.push(child.id) 234 | ids = ids.concat(child.collectLinkIds()) 235 | }) 236 | } 237 | return ids 238 | } 239 | 240 | /** 241 | * 处理剩下的分支(default branch),删除条件节点,保留其他节点 242 | * @param node 243 | */ 244 | processRaminRouteBranch = (node: ProcessNode): { 245 | deleteIds?: string[], 246 | startNode?: ProcessNode, 247 | endNode?: ProcessNode 248 | } => { 249 | const ids = [] 250 | const getStartNode = (node: ProcessNode) => { 251 | if (!node) { 252 | return null; 253 | } 254 | let startNode = node 255 | while (startNode?.type === 'CONDITION') { 256 | ids.push(startNode.id) 257 | startNode = startNode.nextNode 258 | } 259 | return startNode 260 | } 261 | 262 | const getEndNode = (node: ProcessNode) => { 263 | if (!node) { 264 | return null 265 | } 266 | let endNode = node 267 | while (endNode?.nextNode) { 268 | endNode = endNode.nextNode 269 | } 270 | return endNode 271 | } 272 | const startNode = getStartNode(node) 273 | const endNode = getEndNode(startNode) 274 | return { 275 | deleteIds: ids, 276 | startNode: startNode, 277 | endNode: endNode 278 | } 279 | } 280 | 281 | get index() { 282 | if (this.type === 'CONDITION') { 283 | const parentNode = ProcessNodes.get(this.prevNodeId) 284 | if (parentNode) { 285 | return parentNode.conditionNodes?.indexOf(this) || 0 286 | } 287 | } 288 | return null 289 | } 290 | 291 | isFirst() { 292 | if (this.type !== 'CONDITION') { 293 | return false 294 | } 295 | return this.index === 0 296 | } 297 | 298 | isLast() { 299 | if (this.type !== 'CONDITION') { 300 | return false 301 | } 302 | const parentNode = ProcessNodes.get(this.prevNodeId) 303 | return this.index === ((parentNode?.conditionNodes?.length || 0) - 1) 304 | } 305 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ProcessNode" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/panel/StudioPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react"; 2 | import {css, Global} from "@emotion/react"; 3 | import {GlobalStore} from "../store"; 4 | import * as Icons from "../Icons" 5 | 6 | type StudioPanelProps = { 7 | children?: React.ReactNode; 8 | } 9 | 10 | export const StudioPanel: FC = ({children}) => { 11 | GlobalStore.registerIcons(Icons) 12 | return
13 | 37 | {children} 38 |
39 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/panel/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./StudioPanel" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/store.tsx: -------------------------------------------------------------------------------- 1 | import {observable} from "@formily/reactive"; 2 | import {ActivityFC, IResource} from "./types"; 3 | import _ from "lodash"; 4 | import {JSX} from "react"; 5 | 6 | export const DESIGNER_ICONS_STORE: { value: Record } = observable.ref({}) 7 | export const DESIGNER_RESOURCES_STORE: { value: Record } = observable.ref({}) 8 | 9 | 10 | export namespace GlobalStore { 11 | export function registerIcons(icons: Record) { 12 | Object.assign(DESIGNER_ICONS_STORE.value, icons) 13 | } 14 | 15 | export function getIcon(iconName: string) { 16 | return DESIGNER_ICONS_STORE.value[iconName] 17 | } 18 | 19 | export function registerActivityResources(activities: Record>) { 20 | const resourceMap = {}; 21 | _.forEach(activities, (activity: ActivityFC, key: string) => { 22 | resourceMap[activity?.Resource?.componentName || key] = activity?.Resource 23 | }) 24 | Object.assign(DESIGNER_RESOURCES_STORE.value, resourceMap) 25 | } 26 | 27 | export function getActivityResource(componentName: string): IResource { 28 | return DESIGNER_RESOURCES_STORE.value[componentName] 29 | } 30 | 31 | export function getAddableActivityResources(): IResource[] { 32 | return _.filter(_.values(DESIGNER_RESOURCES_STORE.value), (resource: IResource) => { 33 | return resource?.addable 34 | }) || [] 35 | } 36 | 37 | 38 | /** 39 | * 获取条件节点资源 40 | */ 41 | export function getConditionActivityResource(): IResource { 42 | return _.find(_.values(DESIGNER_RESOURCES_STORE.value), (resource: IResource) => { 43 | return resource?.type == 'CONDITION' 44 | }) 45 | } 46 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/types.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {ProcessNode, ProcessNodeType} from "./model"; 3 | 4 | export type IActivities={ 5 | [key:string]:ActivityFC 6 | } 7 | 8 | export interface IResourceCreator { 9 | type: ProcessNodeType 10 | icon?: string; 11 | componentName?: string; 12 | title?: string; 13 | description?: string; 14 | props?: any; 15 | addable?: boolean; 16 | } 17 | export interface IResource extends IResourceCreator{ 18 | node?: ProcessNode 19 | } 20 | 21 | export interface IActivity{ 22 | nextActivity: React.ReactNode 23 | processNode: ProcessNode 24 | onClick?: (processNode: ProcessNode) => void 25 | } 26 | 27 | export type ActivityFC

= React.FC

& { 28 | Resource?: IResource; 29 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/util.ts: -------------------------------------------------------------------------------- 1 | import {IResource, IResourceCreator} from "./types"; 2 | import _ from "lodash"; 3 | import {ProcessNode} from "./model"; 4 | 5 | export namespace DesignerCore { 6 | export function createResource(resource: IResourceCreator): IResource { 7 | return _.assign(resource, { 8 | node: new ProcessNode({ 9 | isSourceNode: true, 10 | type: resource.type, 11 | componentName: resource.componentName, 12 | title: resource.title, 13 | description: resource.description, 14 | props: resource.props 15 | }) 16 | }) 17 | } 18 | 19 | export function transformToSchema(processNode: ProcessNode) { 20 | function toSchema(processNode: ProcessNode) { 21 | if (!processNode) { 22 | return null 23 | } 24 | return { 25 | id: processNode.id, 26 | prevNodeId: processNode.prevNodeId, 27 | type: processNode.type, 28 | componentName: processNode.componentName, 29 | title: processNode.title, 30 | description: processNode.description, 31 | props: _.isEmpty(processNode.props) ? null : processNode.props, 32 | nextNode: toSchema(processNode.nextNode), 33 | conditionNodes: processNode.conditionNodes?.map(toSchema) || [], 34 | defaultCondition: processNode.defaultCondition 35 | } 36 | } 37 | 38 | return toSchema(processNode) 39 | } 40 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/widget/ActivityWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC} from "react"; 2 | import {ProcessNode} from "../model"; 3 | import {observer} from "@formily/react"; 4 | import {useActivities} from "../hooks"; 5 | import {ActivityFC} from "../types"; 6 | import _ from "lodash" 7 | import {ProcessNodeContext} from "../context"; 8 | 9 | type ActivityWidgetProps = { 10 | processNode: ProcessNode; 11 | [key: string]: any 12 | } 13 | 14 | export const ActivityWidget: FC = observer(({ 15 | processNode, 16 | ...props 17 | }) => { 18 | const activities = useActivities() 19 | const handleRender = () => { 20 | const Activity: ActivityFC = _.get(activities, [processNode.componentName]); 21 | 22 | const renderChildren = () => { 23 | if (processNode.conditionNodes.length > 0) { 24 | return processNode.conditionNodes.map((child, index) => 25 | ) 26 | } else { 27 | return [] 28 | } 29 | } 30 | 31 | const renderProps = () => { 32 | return { 33 | processNode: processNode, 34 | nextActivity: processNode.nextNode && , 35 | ...props 36 | } 37 | } 38 | 39 | if (Activity) { 40 | return React.createElement(Activity, renderProps(), renderChildren()) 41 | } else { 42 | return null 43 | } 44 | } 45 | 46 | return {handleRender()} 47 | }) -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/widget/AddActivityItemWidget.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import React from "react"; 3 | import {FC} from "react"; 4 | import {IResource} from "../types"; 5 | import {IconWidget} from "./IconWidget"; 6 | import {GlobalStore} from "../store"; 7 | import {ProcessNode} from "../model"; 8 | 9 | const ActivityCardWidgetStyled = styled('div')({ 10 | cursor: 'pointer', 11 | color: 'black', 12 | display: 'flex', 13 | alignItems: 'center', 14 | minWidth: '0px', 15 | width: '100%', 16 | height: '50px', 17 | padding: '10px', 18 | boxSizing: 'border-box', 19 | background: 'rgba(17, 31, 44, 0.02)', 20 | gap: '8px', 21 | [`&:hover`]: { 22 | background: '#FFFFFF', 23 | border: '1px solid #ecedef', 24 | boxShadow: '0 2px 8px 0 rgba(17, 31, 44, 0.08)' 25 | }, 26 | '.activity-icon': { 27 | fontSize: '28px' 28 | } 29 | }) 30 | 31 | type ActivityCardWidgetProps = { 32 | resource: IResource 33 | processNode: ProcessNode 34 | onClick: (processNode: ProcessNode) => void 35 | } 36 | export const AddActivityItemWidget: FC = ({ 37 | resource, 38 | processNode, 39 | onClick 40 | }) => { 41 | const handleClick = () => { 42 | onClick?.(processNode) 43 | const activity = GlobalStore.getActivityResource(resource?.componentName) 44 | processNode.setNextNode(activity?.node.clone(processNode)) 45 | } 46 | return 47 | 48 |

{resource?.title}
49 | 50 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/widget/IconWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, {FC, MouseEventHandler} from "react"; 2 | import styled from "@emotion/styled"; 3 | import classNames from "classnames"; 4 | 5 | const IconWidgetStyled = styled('span')({ 6 | display: 'inline-flex', 7 | justifyContent: 'center', 8 | alignItems: 'center', 9 | 'svg': { 10 | width: '1em', 11 | height: '1em' 12 | } 13 | }) 14 | 15 | type IconWidgetProps = { 16 | icon?: React.JSX.Element; 17 | className?: string; 18 | onClick?: (e: any) => void; 19 | } 20 | export const IconWidget: FC = ({icon, className, onClick}) => { 21 | return <>{icon && 22 | {React.cloneElement(icon)}} 24 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/widget/ProcessWidget.tsx: -------------------------------------------------------------------------------- 1 | import {IActivities} from "../types"; 2 | import React, {FC, useEffect} from "react"; 3 | import {ActivityWidget} from "./ActivityWidget"; 4 | import {useProcess} from "../hooks/useProcess"; 5 | import {ActivitiesContext} from "../context"; 6 | import {EndActivity} from "../activity"; 7 | import styled from "@emotion/styled"; 8 | import {GlobalStore} from "../store"; 9 | 10 | const ProcessWidgetStyled = styled('div')({ 11 | background: '#F0F2F5', 12 | paddingTop: 20, 13 | paddingBottom: 20, 14 | minWidth:'min-content' 15 | }) 16 | 17 | type ProcessWidgetProps = { 18 | activities?: IActivities 19 | } 20 | export const ProcessWidget: FC = ({ 21 | activities 22 | }) => { 23 | const processNode = useProcess() 24 | 25 | GlobalStore.registerActivityResources(activities) 26 | 27 | return 28 | 29 | {processNode && } 30 | 31 | 32 | 33 | } -------------------------------------------------------------------------------- /packages/approval-process-designer-react/src/widget/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./ProcessWidget" -------------------------------------------------------------------------------- /packages/approval-process-designer-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "moduleResolution": "node", 5 | "jsx": "react", 6 | "module": "commonjs", 7 | "target": "es2016", 8 | "allowJs": false, 9 | "noUnusedLocals": false, 10 | "preserveConstEnums": true, 11 | "sourceMap": false, 12 | "declaration": true, 13 | "skipLibCheck": true, 14 | "experimentalDecorators": true, 15 | "downlevelIteration": true, 16 | "baseUrl": "./", 17 | "allowSyntheticDefaultImports": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'examples/*' -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 审批流设计器 2 | 3 | > 参考钉钉的交互模式,开发的审批流设计器,支持自定义扩展 4 | 5 | 6 | ![img.png](images/img.png) 7 | 8 | ## 功能 9 | - 支持 发起,审批,抄送,条件分支 节点类型 10 | - 支持自定义节点类型 11 | - 支持扩展节点属性 12 | 13 | ## 使用 14 | 1. 安装 15 | ```shell 16 | pnpm add @trionesdev/approval-process-designer-react 17 | ``` 18 | 2. 页面使用 19 | ```typescript 20 | GlobalStore.registerIcons(Icons); 21 | return ( 22 |
23 | 24 | 25 | 26 | 33 | 34 | 35 | 36 |
37 | ); 38 | ``` 39 | 参考范例:[examples/approval-process-designer-react](examples/approval-process-designer-react) 40 | 41 | #### 互相吹捧,共同进步 42 |
43 | 44 |
--------------------------------------------------------------------------------