Procházet zdrojové kódy

流程设计页面 新增流程设计页面

pm před 5 měsíci
rodič
revize
e2152ba5bf

+ 380 - 0
package-lock.json

@@ -52,6 +52,7 @@
                         "react-i18next": "^16.3.5",
                         "react-resizable-panels": "^2.1.7",
                         "react-router-dom": "^7.10.1",
+                        "reactflow": "^11.11.4",
                         "recharts": "^2.15.2",
                         "sonner": "^2.0.3",
                         "tailwind-merge": "*",
@@ -2795,6 +2796,102 @@
                         "react-dom": ">=18.0.0"
                   }
             },
+            "node_modules/@reactflow/background": {
+                  "version": "11.3.14",
+                  "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
+                  "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
+                  "dependencies": {
+                        "@reactflow/core": "11.11.4",
+                        "classcat": "^5.0.3",
+                        "zustand": "^4.4.1"
+                  },
+                  "peerDependencies": {
+                        "react": ">=17",
+                        "react-dom": ">=17"
+                  }
+            },
+            "node_modules/@reactflow/controls": {
+                  "version": "11.2.14",
+                  "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
+                  "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
+                  "dependencies": {
+                        "@reactflow/core": "11.11.4",
+                        "classcat": "^5.0.3",
+                        "zustand": "^4.4.1"
+                  },
+                  "peerDependencies": {
+                        "react": ">=17",
+                        "react-dom": ">=17"
+                  }
+            },
+            "node_modules/@reactflow/core": {
+                  "version": "11.11.4",
+                  "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
+                  "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
+                  "dependencies": {
+                        "@types/d3": "^7.4.0",
+                        "@types/d3-drag": "^3.0.1",
+                        "@types/d3-selection": "^3.0.3",
+                        "@types/d3-zoom": "^3.0.1",
+                        "classcat": "^5.0.3",
+                        "d3-drag": "^3.0.0",
+                        "d3-selection": "^3.0.0",
+                        "d3-zoom": "^3.0.0",
+                        "zustand": "^4.4.1"
+                  },
+                  "peerDependencies": {
+                        "react": ">=17",
+                        "react-dom": ">=17"
+                  }
+            },
+            "node_modules/@reactflow/minimap": {
+                  "version": "11.7.14",
+                  "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
+                  "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
+                  "dependencies": {
+                        "@reactflow/core": "11.11.4",
+                        "@types/d3-selection": "^3.0.3",
+                        "@types/d3-zoom": "^3.0.1",
+                        "classcat": "^5.0.3",
+                        "d3-selection": "^3.0.0",
+                        "d3-zoom": "^3.0.0",
+                        "zustand": "^4.4.1"
+                  },
+                  "peerDependencies": {
+                        "react": ">=17",
+                        "react-dom": ">=17"
+                  }
+            },
+            "node_modules/@reactflow/node-resizer": {
+                  "version": "2.2.14",
+                  "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
+                  "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
+                  "dependencies": {
+                        "@reactflow/core": "11.11.4",
+                        "classcat": "^5.0.4",
+                        "d3-drag": "^3.0.0",
+                        "d3-selection": "^3.0.0",
+                        "zustand": "^4.4.1"
+                  },
+                  "peerDependencies": {
+                        "react": ">=17",
+                        "react-dom": ">=17"
+                  }
+            },
+            "node_modules/@reactflow/node-toolbar": {
+                  "version": "1.3.14",
+                  "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
+                  "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
+                  "dependencies": {
+                        "@reactflow/core": "11.11.4",
+                        "classcat": "^5.0.3",
+                        "zustand": "^4.4.1"
+                  },
+                  "peerDependencies": {
+                        "react": ">=17",
+                        "react-dom": ">=17"
+                  }
+            },
             "node_modules/@rolldown/pluginutils": {
                   "version": "1.0.0-beta.27",
                   "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -3300,21 +3397,142 @@
                         "@swc/counter": "^0.1.3"
                   }
             },
+            "node_modules/@types/d3": {
+                  "version": "7.4.3",
+                  "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+                  "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+                  "dependencies": {
+                        "@types/d3-array": "*",
+                        "@types/d3-axis": "*",
+                        "@types/d3-brush": "*",
+                        "@types/d3-chord": "*",
+                        "@types/d3-color": "*",
+                        "@types/d3-contour": "*",
+                        "@types/d3-delaunay": "*",
+                        "@types/d3-dispatch": "*",
+                        "@types/d3-drag": "*",
+                        "@types/d3-dsv": "*",
+                        "@types/d3-ease": "*",
+                        "@types/d3-fetch": "*",
+                        "@types/d3-force": "*",
+                        "@types/d3-format": "*",
+                        "@types/d3-geo": "*",
+                        "@types/d3-hierarchy": "*",
+                        "@types/d3-interpolate": "*",
+                        "@types/d3-path": "*",
+                        "@types/d3-polygon": "*",
+                        "@types/d3-quadtree": "*",
+                        "@types/d3-random": "*",
+                        "@types/d3-scale": "*",
+                        "@types/d3-scale-chromatic": "*",
+                        "@types/d3-selection": "*",
+                        "@types/d3-shape": "*",
+                        "@types/d3-time": "*",
+                        "@types/d3-time-format": "*",
+                        "@types/d3-timer": "*",
+                        "@types/d3-transition": "*",
+                        "@types/d3-zoom": "*"
+                  }
+            },
             "node_modules/@types/d3-array": {
                   "version": "3.2.2",
                   "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
                   "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="
             },
+            "node_modules/@types/d3-axis": {
+                  "version": "3.0.6",
+                  "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+                  "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+                  "dependencies": {
+                        "@types/d3-selection": "*"
+                  }
+            },
+            "node_modules/@types/d3-brush": {
+                  "version": "3.0.6",
+                  "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+                  "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+                  "dependencies": {
+                        "@types/d3-selection": "*"
+                  }
+            },
+            "node_modules/@types/d3-chord": {
+                  "version": "3.0.6",
+                  "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+                  "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="
+            },
             "node_modules/@types/d3-color": {
                   "version": "3.1.3",
                   "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
                   "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
             },
+            "node_modules/@types/d3-contour": {
+                  "version": "3.0.6",
+                  "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+                  "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+                  "dependencies": {
+                        "@types/d3-array": "*",
+                        "@types/geojson": "*"
+                  }
+            },
+            "node_modules/@types/d3-delaunay": {
+                  "version": "6.0.4",
+                  "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+                  "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="
+            },
+            "node_modules/@types/d3-dispatch": {
+                  "version": "3.0.7",
+                  "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+                  "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="
+            },
+            "node_modules/@types/d3-drag": {
+                  "version": "3.0.7",
+                  "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+                  "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+                  "dependencies": {
+                        "@types/d3-selection": "*"
+                  }
+            },
+            "node_modules/@types/d3-dsv": {
+                  "version": "3.0.7",
+                  "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+                  "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="
+            },
             "node_modules/@types/d3-ease": {
                   "version": "3.0.2",
                   "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
                   "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
             },
+            "node_modules/@types/d3-fetch": {
+                  "version": "3.0.7",
+                  "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+                  "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+                  "dependencies": {
+                        "@types/d3-dsv": "*"
+                  }
+            },
+            "node_modules/@types/d3-force": {
+                  "version": "3.0.10",
+                  "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+                  "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="
+            },
+            "node_modules/@types/d3-format": {
+                  "version": "3.0.4",
+                  "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+                  "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="
+            },
+            "node_modules/@types/d3-geo": {
+                  "version": "3.1.0",
+                  "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+                  "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+                  "dependencies": {
+                        "@types/geojson": "*"
+                  }
+            },
+            "node_modules/@types/d3-hierarchy": {
+                  "version": "3.1.7",
+                  "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+                  "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="
+            },
             "node_modules/@types/d3-interpolate": {
                   "version": "3.0.4",
                   "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
@@ -3328,6 +3546,21 @@
                   "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
                   "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
             },
+            "node_modules/@types/d3-polygon": {
+                  "version": "3.0.2",
+                  "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+                  "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="
+            },
+            "node_modules/@types/d3-quadtree": {
+                  "version": "3.0.6",
+                  "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+                  "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="
+            },
+            "node_modules/@types/d3-random": {
+                  "version": "3.0.3",
+                  "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+                  "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="
+            },
             "node_modules/@types/d3-scale": {
                   "version": "4.0.9",
                   "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
@@ -3336,6 +3569,16 @@
                         "@types/d3-time": "*"
                   }
             },
+            "node_modules/@types/d3-scale-chromatic": {
+                  "version": "3.1.0",
+                  "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+                  "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="
+            },
+            "node_modules/@types/d3-selection": {
+                  "version": "3.0.11",
+                  "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+                  "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="
+            },
             "node_modules/@types/d3-shape": {
                   "version": "3.1.7",
                   "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
@@ -3349,17 +3592,44 @@
                   "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
                   "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
             },
+            "node_modules/@types/d3-time-format": {
+                  "version": "4.0.3",
+                  "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+                  "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="
+            },
             "node_modules/@types/d3-timer": {
                   "version": "3.0.2",
                   "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
                   "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
             },
+            "node_modules/@types/d3-transition": {
+                  "version": "3.0.9",
+                  "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+                  "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+                  "dependencies": {
+                        "@types/d3-selection": "*"
+                  }
+            },
+            "node_modules/@types/d3-zoom": {
+                  "version": "3.0.8",
+                  "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+                  "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+                  "dependencies": {
+                        "@types/d3-interpolate": "*",
+                        "@types/d3-selection": "*"
+                  }
+            },
             "node_modules/@types/estree": {
                   "version": "1.0.8",
                   "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
                   "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
                   "dev": true
             },
+            "node_modules/@types/geojson": {
+                  "version": "7946.0.16",
+                  "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+                  "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
+            },
             "node_modules/@types/history": {
                   "version": "4.7.11",
                   "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
@@ -3530,6 +3800,11 @@
                         "url": "https://polar.sh/cva"
                   }
             },
+            "node_modules/classcat": {
+                  "version": "5.0.5",
+                  "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+                  "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="
+            },
             "node_modules/classnames": {
                   "version": "2.5.1",
                   "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -3610,6 +3885,26 @@
                         "node": ">=12"
                   }
             },
+            "node_modules/d3-dispatch": {
+                  "version": "3.0.1",
+                  "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+                  "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+                  "engines": {
+                        "node": ">=12"
+                  }
+            },
+            "node_modules/d3-drag": {
+                  "version": "3.0.0",
+                  "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+                  "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+                  "dependencies": {
+                        "d3-dispatch": "1 - 3",
+                        "d3-selection": "3"
+                  },
+                  "engines": {
+                        "node": ">=12"
+                  }
+            },
             "node_modules/d3-ease": {
                   "version": "3.0.1",
                   "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
@@ -3660,6 +3955,14 @@
                         "node": ">=12"
                   }
             },
+            "node_modules/d3-selection": {
+                  "version": "3.0.0",
+                  "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+                  "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+                  "engines": {
+                        "node": ">=12"
+                  }
+            },
             "node_modules/d3-shape": {
                   "version": "3.2.0",
                   "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
@@ -3701,6 +4004,39 @@
                         "node": ">=12"
                   }
             },
+            "node_modules/d3-transition": {
+                  "version": "3.0.1",
+                  "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+                  "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+                  "dependencies": {
+                        "d3-color": "1 - 3",
+                        "d3-dispatch": "1 - 3",
+                        "d3-ease": "1 - 3",
+                        "d3-interpolate": "1 - 3",
+                        "d3-timer": "1 - 3"
+                  },
+                  "engines": {
+                        "node": ">=12"
+                  },
+                  "peerDependencies": {
+                        "d3-selection": "2 - 3"
+                  }
+            },
+            "node_modules/d3-zoom": {
+                  "version": "3.0.0",
+                  "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+                  "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+                  "dependencies": {
+                        "d3-dispatch": "1 - 3",
+                        "d3-drag": "2 - 3",
+                        "d3-interpolate": "1 - 3",
+                        "d3-selection": "2 - 3",
+                        "d3-transition": "2 - 3"
+                  },
+                  "engines": {
+                        "node": ">=12"
+                  }
+            },
             "node_modules/date-fns": {
                   "version": "3.6.0",
                   "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
@@ -4555,6 +4891,23 @@
                         "react-dom": ">=16.6.0"
                   }
             },
+            "node_modules/reactflow": {
+                  "version": "11.11.4",
+                  "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
+                  "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
+                  "dependencies": {
+                        "@reactflow/background": "11.3.14",
+                        "@reactflow/controls": "11.2.14",
+                        "@reactflow/core": "11.11.4",
+                        "@reactflow/minimap": "11.7.14",
+                        "@reactflow/node-resizer": "2.2.14",
+                        "@reactflow/node-toolbar": "1.3.14"
+                  },
+                  "peerDependencies": {
+                        "react": ">=17",
+                        "react-dom": ">=17"
+                  }
+            },
             "node_modules/recharts": {
                   "version": "2.15.4",
                   "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
@@ -4892,6 +5245,33 @@
                   "engines": {
                         "node": ">=0.10.0"
                   }
+            },
+            "node_modules/zustand": {
+                  "version": "4.5.7",
+                  "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+                  "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+                  "dependencies": {
+                        "use-sync-external-store": "^1.2.2"
+                  },
+                  "engines": {
+                        "node": ">=12.7.0"
+                  },
+                  "peerDependencies": {
+                        "@types/react": ">=16.8",
+                        "immer": ">=9.0.6",
+                        "react": ">=16.8"
+                  },
+                  "peerDependenciesMeta": {
+                        "@types/react": {
+                              "optional": true
+                        },
+                        "immer": {
+                              "optional": true
+                        },
+                        "react": {
+                              "optional": true
+                        }
+                  }
             }
       }
 }

+ 1 - 0
package.json

@@ -47,6 +47,7 @@
             "react-i18next": "^16.3.5",
             "react-resizable-panels": "^2.1.7",
             "react-router-dom": "^7.10.1",
+            "reactflow": "^11.11.4",
             "recharts": "^2.15.2",
             "sonner": "^2.0.3",
             "tailwind-merge": "*",

+ 64 - 2
src/Dashboard.tsx

@@ -551,8 +551,22 @@ export default function Dashboard() {
       return;
     }
     
-    // 如果路径是 /dashboard,不处理(使用当前状态)
+    // 如果路径是 /dashboard,检查是否有上次的菜单状态
     if (location.pathname === '/dashboard') {
+      const lastActiveMenu = sessionStorage.getItem('lastActiveMenu');
+      if (lastActiveMenu) {
+        try {
+          const menuInfo = JSON.parse(lastActiveMenu);
+          if (menuInfo.menu && menuInfo.subMenu) {
+            console.log('恢复上次菜单状态:', menuInfo);
+            setActiveMenu(menuInfo.menu);
+            setActiveSubMenu(menuInfo.subMenu);
+            return;
+          }
+        } catch (e) {
+          console.error('解析菜单信息失败:', e);
+        }
+      }
       return;
     }
     
@@ -560,6 +574,8 @@ export default function Dashboard() {
     const path = location.pathname;
     const menuKey = mapBackendPathToFrontendKey(path);
     
+    console.log('路径变化处理:', { path, menuKey, filteredSubMenuConfig: Object.keys(filteredSubMenuConfig) });
+    
     if (menuKey) {
       console.log('根据路径更新菜单:', { path, menuKey });
       
@@ -591,10 +607,21 @@ export default function Dashboard() {
         // 硬件管理的子菜单
         setActiveMenu('hardwareManagement');
         setActiveSubMenu(menuKey);
+      } else if (menuKey === 'processDesign' || menuKey === 'sopManagement' || menuKey === 'workManagement' || menuKey === 'formManagement') {
+        // 隔离作业的子菜单
+        console.log('设置隔离作业子菜单:', { menuKey: 'isolationWork', subMenuKey: menuKey });
+        setActiveMenu('isolationWork');
+        setActiveSubMenu(menuKey);
       } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
         // 其他菜单,设置第一个子菜单
         setActiveMenu(menuKey);
         setActiveSubMenu(filteredSubMenuConfig[menuKey][0].key);
+      } else if (menuKey === 'isolationWork') {
+        // 如果只是隔离作业主菜单,设置第一个子菜单
+        if (filteredSubMenuConfig.isolationWork && filteredSubMenuConfig.isolationWork.length > 0) {
+          setActiveMenu('isolationWork');
+          setActiveSubMenu(filteredSubMenuConfig.isolationWork[0].key);
+        }
       }
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -608,6 +635,23 @@ export default function Dashboard() {
       return;
     }
     
+    // 检查是否有从其他页面跳转过来的菜单信息
+    const navigateToMenu = sessionStorage.getItem('navigateToMenu');
+    if (navigateToMenu) {
+      try {
+        const menuInfo = JSON.parse(navigateToMenu);
+        if (menuInfo.menu && menuInfo.subMenu) {
+          setActiveMenu(menuInfo.menu);
+          setActiveSubMenu(menuInfo.subMenu);
+          sessionStorage.removeItem('navigateToMenu');
+          return;
+        }
+      } catch (e) {
+        console.error('解析菜单信息失败:', e);
+        sessionStorage.removeItem('navigateToMenu');
+      }
+    }
+    
     // 只在路径不是 /dashboard 时才根据路径初始化(因为应用使用状态管理而非路由)
     // 如果路径是 /dashboard,则使用默认状态
     if (location.pathname === '/dashboard') {
@@ -648,6 +692,11 @@ export default function Dashboard() {
         // 硬件管理的子菜单
         setActiveMenu('hardwareManagement');
         setActiveSubMenu(menuKey);
+      } else if (menuKey === 'processDesign' || menuKey === 'sopManagement' || menuKey === 'workManagement' || menuKey === 'formManagement') {
+        // 隔离作业的子菜单
+        console.log('设置隔离作业子菜单:', { menuKey: 'isolationWork', subMenuKey: menuKey });
+        setActiveMenu('isolationWork');
+        setActiveSubMenu(menuKey);
       } else if (filteredSubMenuConfig[menuKey] && filteredSubMenuConfig[menuKey].length > 0) {
         // 其他菜单,设置第一个子菜单
         setActiveMenu(menuKey);
@@ -812,6 +861,11 @@ export default function Dashboard() {
                                       if (location.pathname.startsWith('/lock-cabinet/detail')) {
                                         navigate('/dashboard');
                                       }
+                                      // 保存菜单状态到 sessionStorage
+                                      sessionStorage.setItem('lastActiveMenu', JSON.stringify({
+                                        menu: item.key,
+                                        subMenu: subItem.key
+                                      }));
                                       setActiveMenu(item.key);
                                       setActiveSubMenu(subItem.key);
                                       setShowDropdownMenu(null);
@@ -844,12 +898,20 @@ export default function Dashboard() {
                     <button
                       key={item.key}
                       onClick={() => {
+                        // 保存菜单状态到 sessionStorage
+                        const firstSubMenu = filteredSubMenuConfig[item.key]?.[0];
+                        if (firstSubMenu) {
+                          sessionStorage.setItem('lastActiveMenu', JSON.stringify({
+                            menu: item.key,
+                            subMenu: firstSubMenu.key
+                          }));
+                        }
                         // 如果当前在详情页,导航回对应的菜单页面
                         if (location.pathname.startsWith('/lock-cabinet/detail')) {
                           navigate('/dashboard');
                         }
                         setActiveMenu(item.key);
-                        setActiveSubMenu(filteredSubMenuConfig[item.key]?.[0]?.key || '');
+                        setActiveSubMenu(firstSubMenu?.key || '');
                       }}
                       className={`flex items-center gap-2 px-4 py-2.5 rounded-xl transition-all duration-300 ${
                         isActive

+ 11 - 10
src/components/IsolationWork.tsx

@@ -1,4 +1,5 @@
 import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
 import { Plus, Search, Edit2, Trash2, MoreVertical, FileText, Eye, Play, CheckCircle, RefreshCw, Workflow } from 'lucide-react';
 import { Button, Input, Space, Select, Table as AntdTable } from 'antd';
 import { Button as UIButton } from './ui/button';
@@ -14,6 +15,7 @@ interface IsolationWorkProps {
 }
 
 export default function IsolationWork({ subMenu }: IsolationWorkProps) {
+  const navigate = useNavigate();
   const [searchTerm, setSearchTerm] = useState('');
   const [showAddModal, setShowAddModal] = useState(false);
   const [editingItem, setEditingItem] = useState<TableRow | null>(null);
@@ -1187,16 +1189,15 @@ export default function IsolationWork({ subMenu }: IsolationWorkProps) {
                   重置
                 </Button>
                 
-                <Button
-                  type="primary"
-                  icon={<Plus className="w-4 h-4" />}
-                  onClick={() => {
-                    setEditingItem(null);
-                    setShowAddModal(true);
-                  }}
-                >
-                  新建流程
-                </Button>
+              <Button
+                type="primary"
+                icon={<Plus className="w-4 h-4" />}
+                onClick={() => {
+                  navigate('/process-designer');
+                }}
+              >
+                新建流程
+              </Button>
               </Space>
             </div>
           </div>

+ 838 - 0
src/components/ProcessDesigner.tsx

@@ -0,0 +1,838 @@
+import React, { useState, useCallback, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import ReactFlow, {
+  Node,
+  Edge,
+  addEdge,
+  Connection,
+  useNodesState,
+  useEdgesState,
+  Controls,
+  Background,
+  MiniMap,
+  NodeTypes,
+  BackgroundVariant,
+  Handle,
+  Position,
+} from 'reactflow';
+import 'reactflow/dist/style.css';
+import {
+  Save,
+  ArrowLeft,
+  Undo2,
+  Redo2,
+  FileText,
+  CheckCircle,
+  ClipboardCheck,
+  PenTool,
+  Shield,
+  Unlock,
+  LockKeyhole,
+  CheckSquare,
+  X,
+  ZoomIn,
+  ZoomOut,
+  Wrench,
+} from 'lucide-react';
+import { Button, Input, Select, Checkbox, Tabs } from 'antd';
+import { toast } from 'sonner';
+
+// 节点配置
+const nodeConfigs = [
+  {
+    type: 'createJob',
+    label: '创建作业',
+    icon: Wrench,
+    color: 'bg-blue-500',
+    bgColor: 'bg-blue-50',
+  },
+  {
+    type: 'confirm',
+    label: '确认',
+    icon: CheckCircle,
+    color: 'bg-green-500',
+    bgColor: 'bg-green-50',
+  },
+  {
+    type: 'review',
+    label: '审核',
+    icon: ClipboardCheck,
+    color: 'bg-orange-500',
+    bgColor: 'bg-orange-50',
+  },
+  {
+    type: 'inputInfo',
+    label: '录入信息',
+    icon: PenTool,
+    color: 'bg-purple-500',
+    bgColor: 'bg-purple-50',
+  },
+  {
+    type: 'isolation',
+    label: '隔离/方案',
+    icon: Shield,
+    color: 'bg-red-500',
+    bgColor: 'bg-red-50',
+  },
+  {
+    type: 'releaseIsolation',
+    label: '解除隔离',
+    icon: Unlock,
+    color: 'bg-yellow-500',
+    bgColor: 'bg-yellow-50',
+  },
+  {
+    type: 'returnLock',
+    label: '还锁',
+    icon: LockKeyhole,
+    color: 'bg-indigo-500',
+    bgColor: 'bg-indigo-50',
+  },
+  {
+    type: 'complete',
+    label: '完成/结束',
+    icon: CheckSquare,
+    color: 'bg-gray-500',
+    bgColor: 'bg-gray-50',
+  },
+];
+
+// 自定义节点组件
+function CustomNode({ data, selected, id }: any) {
+  const config = nodeConfigs.find(c => c.type === data.type);
+  const Icon = config?.icon || FileText;
+  // 从节点ID中提取序号,或使用data中的nodeId
+  const nodeId = data.nodeId || (id ? String(parseInt(id.split('-').pop() || '0') % 1000).padStart(3, '0') : '001');
+  
+  return (
+    <div
+      className={`relative px-3 py-2.5 rounded-lg shadow-sm border-2 w-auto min-w-[100px] max-w-[200px] bg-white ${
+        selected
+          ? 'border-blue-500 shadow-md ring-1 ring-blue-200'
+          : 'border-gray-200 hover:border-gray-300'
+      } transition-all`}
+    >
+      {/* 连接点 */}
+      <Handle
+        type="target"
+        position={Position.Top}
+        className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
+        style={{ top: -6, left: '50%', transform: 'translateX(-50%)' }}
+      />
+      <Handle
+        type="source"
+        position={Position.Bottom}
+        className="!w-3 !h-3 !bg-green-500 !border-2 !border-white !rounded-full !shadow-sm"
+        style={{ bottom: -6, left: '50%', transform: 'translateX(-50%)' }}
+      />
+      
+      <div className="flex items-center gap-2">
+        <div className={`${config?.color || 'bg-gray-500'} p-1.5 rounded flex-shrink-0 shadow-sm`}>
+          <Icon className="w-4 h-4 text-white" />
+        </div>
+        <div className="flex-1 min-w-0">
+          <div className="font-semibold text-xs text-gray-900 mb-0.5 leading-tight whitespace-nowrap">
+            {data.label || config?.label}
+          </div>
+          <div className="text-[10px] text-gray-500">
+            ID: {nodeId}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+// 各个节点类型
+function CreateJobNode(props: any) {
+  return <CustomNode {...props} />;
+}
+
+function ConfirmNode(props: any) {
+  return <CustomNode {...props} />;
+}
+
+function ReviewNode(props: any) {
+  return <CustomNode {...props} />;
+}
+
+function InputInfoNode(props: any) {
+  return <CustomNode {...props} />;
+}
+
+function IsolationNode(props: any) {
+  return <CustomNode {...props} />;
+}
+
+function ReleaseIsolationNode(props: any) {
+  return <CustomNode {...props} />;
+}
+
+function ReturnLockNode(props: any) {
+  return <CustomNode {...props} />;
+}
+
+function CompleteNode(props: any) {
+  return <CustomNode {...props} />;
+}
+
+// 自定义节点类型映射
+const nodeTypes = {
+  createJob: CreateJobNode,
+  confirm: ConfirmNode,
+  review: ReviewNode,
+  inputInfo: InputInfoNode,
+  isolation: IsolationNode,
+  releaseIsolation: ReleaseIsolationNode,
+  returnLock: ReturnLockNode,
+  complete: CompleteNode,
+};
+
+export default function ProcessDesigner() {
+  const navigate = useNavigate();
+  const [nodes, setNodes, onNodesChange] = useNodesState([]);
+  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+  const [selectedNode, setSelectedNode] = useState<Node | null>(null);
+  const reactFlowWrapper = useRef<HTMLDivElement>(null);
+  const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);
+  const [zoom, setZoom] = useState(1);
+
+  // 节点配置状态
+  const [nodeConfig, setNodeConfig] = useState({
+    nodeName: '',
+    nodeIcon: '',
+    responsible: '',
+    remark: '',
+    submitForm: '',
+    notificationMethods: {
+      sms: false,
+      message: false,
+      email: false,
+      app: false,
+    },
+    notificationPerson: '',
+    notificationTime: '',
+  });
+
+  // 历史记录(用于撤销/重做)
+  const [history, setHistory] = useState<{ nodes: Node[]; edges: Edge[] }[]>([]);
+  const [historyIndex, setHistoryIndex] = useState(-1);
+
+  // 拖拽处理
+  const onDragStart = (event: React.DragEvent, nodeType: string) => {
+    event.dataTransfer.setData('application/reactflow', nodeType);
+    event.dataTransfer.effectAllowed = 'move';
+  };
+
+  // 连接处理
+  const onConnect = useCallback(
+    (params: Connection) => {
+      setEdges((eds) => {
+        const newEdges = addEdge(params, eds);
+        // 保存历史
+        const newHistory = history.slice(0, historyIndex + 1);
+        newHistory.push({ nodes: [...nodes], edges: [...newEdges] });
+        setHistory(newHistory);
+        setHistoryIndex(newHistory.length - 1);
+        return newEdges;
+      });
+    },
+    [setEdges, history, historyIndex, nodes]
+  );
+
+  // 节点点击处理
+  const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
+    setSelectedNode(node);
+    // 加载节点配置
+    const nodeData = node.data || {};
+    const config = nodeConfigs.find(c => c.type === nodeData.type);
+    setNodeConfig({
+      nodeName: nodeData.label || config?.label || '',
+      nodeIcon: nodeData.type || '',
+      responsible: nodeData.responsible || '',
+      remark: nodeData.remark || '',
+      submitForm: nodeData.submitForm || '',
+      notificationMethods: nodeData.notificationMethods || {
+        sms: false,
+        message: false,
+        email: false,
+        app: false,
+      },
+      notificationPerson: nodeData.notificationPerson || '',
+      notificationTime: nodeData.notificationTime || '',
+    });
+  }, []);
+
+  // 画布点击处理(取消选择)
+  const onPaneClick = useCallback(() => {
+    setSelectedNode(null);
+  }, []);
+
+  // 撤销
+  const handleUndo = useCallback(() => {
+    if (historyIndex > 0) {
+      const prevState = history[historyIndex - 1];
+      setNodes(prevState.nodes);
+      setEdges(prevState.edges);
+      setHistoryIndex(historyIndex - 1);
+    }
+  }, [history, historyIndex, setNodes, setEdges]);
+
+  // 重做
+  const handleRedo = useCallback(() => {
+    if (historyIndex < history.length - 1) {
+      const nextState = history[historyIndex + 1];
+      setNodes(nextState.nodes);
+      setEdges(nextState.edges);
+      setHistoryIndex(historyIndex + 1);
+    }
+  }, [history, historyIndex, setNodes, setEdges]);
+
+  // 拖放处理
+  const onDrop = useCallback(
+    (event: React.DragEvent) => {
+      event.preventDefault();
+
+      const type = event.dataTransfer.getData('application/reactflow');
+      if (!type || !reactFlowInstance) return;
+
+      const position = reactFlowInstance.screenToFlowPosition({
+        x: event.clientX,
+        y: event.clientY,
+      });
+
+      const config = nodeConfigs.find(c => c.type === type);
+      const timestamp = Date.now();
+      const nodeId = `${type}-${timestamp}`;
+      const nodeNumber = nodes.length + 1;
+      const newNode: Node = {
+        id: nodeId,
+        type,
+        position,
+        data: {
+          label: config?.label || type,
+          type,
+          nodeId: String(nodeNumber).padStart(3, '0'),
+        },
+      };
+
+      setNodes((nds) => {
+        const newNodes = nds.concat(newNode);
+        // 保存历史
+        const newHistory = history.slice(0, historyIndex + 1);
+        newHistory.push({ nodes: [...newNodes], edges: [...edges] });
+        setHistory(newHistory);
+        setHistoryIndex(newHistory.length - 1);
+        return newNodes;
+      });
+    },
+    [reactFlowInstance, setNodes, history, historyIndex, edges, nodes.length]
+  );
+
+  const onDragOver = useCallback((event: React.DragEvent) => {
+    event.preventDefault();
+    event.dataTransfer.dropEffect = 'move';
+  }, []);
+
+  // 更新节点配置
+  const updateNodeConfig = useCallback(() => {
+    if (!selectedNode) return;
+
+    setNodes((nds) => {
+      const updatedNodes = nds.map((node) => {
+        if (node.id === selectedNode.id) {
+          return {
+            ...node,
+            data: {
+              ...node.data,
+              label: nodeConfig.nodeName,
+              responsible: nodeConfig.responsible,
+              remark: nodeConfig.remark,
+              submitForm: nodeConfig.submitForm,
+              notificationMethods: nodeConfig.notificationMethods,
+              notificationPerson: nodeConfig.notificationPerson,
+              notificationTime: nodeConfig.notificationTime,
+            },
+          };
+        }
+        return node;
+      });
+      // 保存历史
+      const newHistory = history.slice(0, historyIndex + 1);
+      newHistory.push({ nodes: [...updatedNodes], edges: [...edges] });
+      setHistory(newHistory);
+      setHistoryIndex(newHistory.length - 1);
+      return updatedNodes;
+    });
+    toast.success('节点配置已保存');
+  }, [selectedNode, nodeConfig, setNodes, history, historyIndex, edges]);
+
+  // 保存流程
+  const handleSave = () => {
+    console.log('保存流程:', { nodes, edges });
+    toast.success('流程保存成功');
+  };
+
+  // 返回
+  const handleBack = () => {
+    // 导航到 dashboard,Dashboard 会从 sessionStorage 读取上次的菜单状态
+    navigate('/dashboard');
+  };
+
+  // 缩放控制
+  const handleZoomIn = () => {
+    if (reactFlowInstance) {
+      reactFlowInstance.zoomIn();
+    }
+  };
+
+  const handleZoomOut = () => {
+    if (reactFlowInstance) {
+      reactFlowInstance.zoomOut();
+    }
+  };
+
+  // 获取节点描述
+  const getNodeDescription = (type: string) => {
+    const descriptions: { [key: string]: string } = {
+      createJob: '该节点用于创建新的作业任务,是流程的起始节点。',
+      confirm: '该节点用于确认操作,通常需要相关人员确认后才能继续。',
+      review: '该节点用于审核流程,需要审核人员审批。',
+      inputInfo: '该节点用于录入相关信息,可以填写作业所需的各种信息。',
+      isolation: '该节点为作业隔离类型选择,主要包括盲板、上锁挂牌、拆除等。',
+      releaseIsolation: '该节点用于解除隔离状态,恢复设备正常运行。',
+      returnLock: '该节点用于归还锁具,完成锁具管理流程。',
+      complete: '该节点表示流程完成或结束,是流程的终止节点。',
+    };
+    return descriptions[type] || '该节点的功能描述。';
+  };
+
+  return (
+    <div className="h-screen w-screen flex flex-col bg-gray-50">
+      {/* 顶部工具栏 */}
+      <div className="h-14 bg-white border-b border-gray-200 flex items-center justify-between px-4 shadow-sm z-10">
+        <div className="flex items-center gap-2">
+          <span className="text-lg font-bold text-blue-600 mr-2">流程设计器</span>
+          <div className="h-5 w-px bg-gray-300 mx-1" />
+          <Button
+            type="primary"
+            className="flex items-center gap-1.5"
+            onClick={handleSave}
+          >
+            <Save className="w-4 h-4" />
+            <span>保存</span>
+          </Button>
+          <div className="h-5 w-px bg-gray-300 mx-1" />
+          <Button 
+            size="small" 
+            className="flex items-center gap-1.5"
+            onClick={handleBack}
+          >
+            <ArrowLeft className="w-4 h-4" />
+            <span>返回</span>
+          </Button>
+          <div className="h-5 w-px bg-gray-300 mx-1" />
+          <Button
+            size="small"
+            className="flex items-center gap-1.5"
+            onClick={handleUndo}
+            disabled={historyIndex <= 0}
+          >
+            <Undo2 className="w-4 h-4" />
+            <span>撤销</span>
+          </Button>
+          <Button
+            size="small"
+            className="flex items-center gap-1.5"
+            onClick={handleRedo}
+            disabled={historyIndex >= history.length - 1}
+          >
+            <Redo2 className="w-4 h-4" />
+            <span>重做</span>
+          </Button>
+          <div className="h-5 w-px bg-gray-300 mx-1" />
+          <Button
+            size="small"
+            icon={<ZoomOut className="w-4 h-4" />}
+            onClick={handleZoomOut}
+          />
+          <span className="text-sm text-gray-600 px-2 min-w-[50px] text-center">
+            {Math.round(zoom * 100)}%
+          </span>
+          <Button
+            size="small"
+            icon={<ZoomIn className="w-4 h-4" />}
+            onClick={handleZoomIn}
+          />
+        </div>
+      </div>
+
+      {/* 主内容区 */}
+      <div className="flex-1 flex overflow-hidden">
+        {/* 左侧节点面板 */}
+        <div className="w-56 bg-gray-50 border-r border-gray-200 overflow-y-auto">
+          <div className="p-2 space-y-3">
+            {nodeConfigs.map((config) => {
+              const Icon = config.icon;
+              return (
+                <div
+                  key={config.type}
+                  draggable
+                  onDragStart={(e) => onDragStart(e, config.type)}
+                  className={`flex flex-col items-center gap-1.5 p-2 rounded-lg ${config.bgColor} border border-gray-200 hover:border-blue-400 hover:shadow-sm cursor-move transition-all`}
+                >
+                  <div className={`${config.color} p-1.5 rounded-lg shadow-sm`}>
+                    <Icon className="w-5 h-5 text-white" />
+                  </div>
+                  <span className="text-xs font-medium text-gray-700 text-center leading-tight">
+                    {config.label}
+                  </span>
+                </div>
+              );
+            })}
+          </div>
+        </div>
+
+        {/* 中间画布 */}
+        <div className="flex-1 relative bg-white" ref={reactFlowWrapper}>
+          <ReactFlow
+            nodes={nodes}
+            edges={edges}
+            onNodesChange={onNodesChange}
+            onEdgesChange={onEdgesChange}
+            onConnect={onConnect}
+            onNodeClick={onNodeClick}
+            onPaneClick={onPaneClick}
+            onDrop={onDrop}
+            onDragOver={onDragOver}
+            nodeTypes={nodeTypes as NodeTypes}
+            fitView
+            onInit={setReactFlowInstance}
+            onMove={(_, viewport) => setZoom(viewport.zoom)}
+            defaultEdgeOptions={{
+              style: { strokeWidth: 2, stroke: '#94a3b8' },
+              type: 'smoothstep',
+            }}
+          >
+            <Controls className="!bg-white !border !border-gray-200 !rounded-lg !shadow-md" />
+            <Background variant={BackgroundVariant.Dots} gap={16} size={1} color="#e5e7eb" />
+            <MiniMap
+              nodeColor={(node) => {
+                const config = nodeConfigs.find(c => c.type === node.type);
+                if (config?.color === 'bg-blue-500') return '#3b82f6';
+                if (config?.color === 'bg-green-500') return '#10b981';
+                if (config?.color === 'bg-orange-500') return '#f97316';
+                if (config?.color === 'bg-red-500') return '#ef4444';
+                if (config?.color === 'bg-yellow-500') return '#eab308';
+                if (config?.color === 'bg-indigo-500') return '#6366f1';
+                if (config?.color === 'bg-purple-500') return '#a855f7';
+                return '#6b7280';
+              }}
+              maskColor="rgba(0, 0, 0, 0.05)"
+              className="!bg-white !border !border-gray-200 !rounded-lg"
+            />
+          </ReactFlow>
+        </div>
+
+        {/* 右侧属性面板 */}
+        <div className="w-80 bg-gray-50 border-l border-gray-200 overflow-y-auto">
+          {selectedNode ? (
+            <div className="h-full flex flex-col">
+              {/* 头部 */}
+              <div className="p-4 bg-white border-b border-gray-200 flex items-center justify-between sticky top-0 z-10 shadow-sm">
+                <h3 className="text-base font-semibold text-gray-900">
+                  {nodeConfig.nodeName || selectedNode.data?.label || '节点'} 设置
+                </h3>
+                <button
+                  onClick={() => setSelectedNode(null)}
+                  className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors"
+                >
+                  <X className="w-4 h-4 text-gray-500" />
+                </button>
+              </div>
+
+              {/* 内容 */}
+              <div className="flex-1 p-4 overflow-y-auto">
+                <Tabs
+                  defaultActiveKey="info"
+                  className="[&_.ant-tabs-tab]:!px-4 [&_.ant-tabs-tab]:!py-2.5 [&_.ant-tabs-tab-active]:!text-blue-600 [&_.ant-tabs-tab-active]:!font-semibold [&_.ant-tabs-ink-bar]:!bg-blue-600 [&_.ant-tabs-tab]:!text-gray-600 [&_.ant-tabs-tab]:!border-b-2 [&_.ant-tabs-tab]:!border-transparent [&_.ant-tabs-tab:hover]:!text-blue-500"
+                  items={[
+                    {
+                      key: 'info',
+                      label: '节点信息',
+                      children: (
+                        <div className="space-y-5">
+                          {/* 描述 */}
+                          <div className="text-sm text-gray-600 leading-relaxed bg-gray-50 p-3 rounded-lg">
+                            {getNodeDescription(selectedNode.data?.type || '')}
+                          </div>
+
+                          {/* 节点名称 */}
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-2">
+                              节点名称 <span className="text-red-500">*</span>
+                            </label>
+                            <p className="text-xs text-gray-500 mb-2.5 leading-relaxed">
+                              (默认名称: {nodeConfigs.find(c => c.type === selectedNode.data?.type)?.label || '节点名称'},可根据需求调整)
+                            </p>
+                            <Input
+                              value={nodeConfig.nodeName}
+                              onChange={(e) =>
+                                setNodeConfig({ ...nodeConfig, nodeName: e.target.value })
+                              }
+                              placeholder="请输入节点名称"
+                              className="rounded-lg border-gray-200 h-10"
+                            />
+                          </div>
+
+                          {/* 显示图标 */}
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-2">
+                              显示图标
+                            </label>
+                            <div className="border border-gray-200 rounded-lg p-3 bg-white flex items-center gap-3 cursor-pointer hover:border-blue-400 transition-colors shadow-sm">
+                              {(() => {
+                                const iconType = nodeConfig.nodeIcon || selectedNode.data?.type;
+                                const config = nodeConfigs.find(c => c.type === iconType);
+                                const Icon = config?.icon || FileText;
+                                return (
+                                  <>
+                                    <div className={`${config?.color || 'bg-gray-500'} p-2 rounded-lg shadow-sm`}>
+                                      <Icon className="w-5 h-5 text-white" />
+                                    </div>
+                                    <span className="text-sm text-gray-600">选择图标</span>
+                                  </>
+                                );
+                              })()}
+                            </div>
+                          </div>
+
+                          {/* 负责人 */}
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-2">
+                              负责人
+                            </label>
+                            <Select
+                              value={nodeConfig.responsible}
+                              onChange={(value) =>
+                                setNodeConfig({ ...nodeConfig, responsible: value })
+                              }
+                              placeholder="请选择负责人"
+                              className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
+                              allowClear
+                            >
+                              <Select.Option value="user1">张三</Select.Option>
+                              <Select.Option value="user2">李四</Select.Option>
+                              <Select.Option value="user3">王五</Select.Option>
+                            </Select>
+                            <p className="text-xs text-gray-500 mt-1.5 leading-relaxed">
+                              对该任务或步骤节点进行处理的人员,若不选则需要在创建作业时进行选择
+                            </p>
+                          </div>
+
+                          {/* 备注 */}
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-2">
+                              备注
+                            </label>
+                            <Input.TextArea
+                              value={nodeConfig.remark}
+                              onChange={(e) =>
+                                setNodeConfig({ ...nodeConfig, remark: e.target.value })
+                              }
+                              placeholder="请输入备注"
+                              rows={3}
+                              className="rounded-lg border-gray-200"
+                            />
+                          </div>
+
+                          <Button
+                            type="primary"
+                            onClick={updateNodeConfig}
+                            className="w-full rounded-lg"
+                            size="large"
+                          >
+                            保存配置
+                          </Button>
+                        </div>
+                      ),
+                    },
+                    {
+                      key: 'form',
+                      label: '提交表单',
+                      children: (
+                        <div className="space-y-5">
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-2">
+                              提交表单 <span className="text-red-500">*</span>
+                            </label>
+                            <Select
+                              value={nodeConfig.submitForm}
+                              onChange={(value) =>
+                                setNodeConfig({ ...nodeConfig, submitForm: value })
+                              }
+                              placeholder="请选择"
+                              className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
+                              allowClear
+                            >
+                              <Select.Option value="form1">表单1</Select.Option>
+                              <Select.Option value="form2">表单2</Select.Option>
+                              <Select.Option value="form3">表单3</Select.Option>
+                            </Select>
+                          </div>
+                          <Button
+                            type="primary"
+                            onClick={updateNodeConfig}
+                            className="w-full rounded-lg"
+                            size="large"
+                          >
+                            保存配置
+                          </Button>
+                        </div>
+                      ),
+                    },
+                    {
+                      key: 'notification',
+                      label: '通知消息',
+                      children: (
+                        <div className="space-y-5">
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-3">
+                              通知方式
+                            </label>
+                            <div className="space-y-3 bg-gray-50 p-3 rounded-lg">
+                              <Checkbox
+                                checked={nodeConfig.notificationMethods.sms}
+                                onChange={(e) =>
+                                  setNodeConfig({
+                                    ...nodeConfig,
+                                    notificationMethods: {
+                                      ...nodeConfig.notificationMethods,
+                                      sms: e.target.checked,
+                                    },
+                                  })
+                                }
+                              >
+                                短信
+                              </Checkbox>
+                              <div>
+                                <Checkbox
+                                  checked={nodeConfig.notificationMethods.message}
+                                  onChange={(e) =>
+                                    setNodeConfig({
+                                      ...nodeConfig,
+                                      notificationMethods: {
+                                        ...nodeConfig.notificationMethods,
+                                        message: e.target.checked,
+                                      },
+                                    })
+                                  }
+                                >
+                                  站内信
+                                </Checkbox>
+                              </div>
+                              <div>
+                                <Checkbox
+                                  checked={nodeConfig.notificationMethods.email}
+                                  onChange={(e) =>
+                                    setNodeConfig({
+                                      ...nodeConfig,
+                                      notificationMethods: {
+                                        ...nodeConfig.notificationMethods,
+                                        email: e.target.checked,
+                                      },
+                                    })
+                                  }
+                                >
+                                  邮件
+                                </Checkbox>
+                              </div>
+                              <div>
+                                <Checkbox
+                                  checked={nodeConfig.notificationMethods.app}
+                                  onChange={(e) =>
+                                    setNodeConfig({
+                                      ...nodeConfig,
+                                      notificationMethods: {
+                                        ...nodeConfig.notificationMethods,
+                                        app: e.target.checked,
+                                      },
+                                    })
+                                  }
+                                >
+                                  APP通知
+                                </Checkbox>
+                              </div>
+                            </div>
+                          </div>
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-2">
+                              通知人
+                            </label>
+                            <Select
+                              value={nodeConfig.notificationPerson}
+                              onChange={(value) =>
+                                setNodeConfig({ ...nodeConfig, notificationPerson: value })
+                              }
+                              placeholder="请选择通知人"
+                              className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
+                              allowClear
+                            >
+                              <Select.Option value="user1">张三</Select.Option>
+                              <Select.Option value="user2">李四</Select.Option>
+                              <Select.Option value="user3">王五</Select.Option>
+                            </Select>
+                          </div>
+                          <div>
+                            <label className="block text-sm font-medium text-gray-700 mb-2">
+                              通知时间
+                            </label>
+                            <Select
+                              value={nodeConfig.notificationTime}
+                              onChange={(value) =>
+                                setNodeConfig({ ...nodeConfig, notificationTime: value })
+                              }
+                              placeholder="请选择通知时间"
+                              className="w-full [&_.ant-select-selector]:!rounded-lg [&_.ant-select-selector]:!h-10"
+                              allowClear
+                            >
+                              <Select.Option value="before">执行前(上一个节点结束后)</Select.Option>
+                              <Select.Option value="after">执行后(该节点结束后)</Select.Option>
+                              <Select.Option value="time">选择时间</Select.Option>
+                              <Select.Option value="30min">任务开始前30分钟</Select.Option>
+                              <Select.Option value="1h">任务开始前1小时</Select.Option>
+                              <Select.Option value="2h">任务开始前2小时</Select.Option>
+                              <Select.Option value="4h">任务开始前4小时</Select.Option>
+                              <Select.Option value="5h">任务开始前5小时</Select.Option>
+                              <Select.Option value="8h">任务开始前8小时</Select.Option>
+                              <Select.Option value="12h">任务开始前12小时</Select.Option>
+                              <Select.Option value="24h">任务开始前24小时</Select.Option>
+                              <Select.Option value="48h">任务开始前48小时</Select.Option>
+                            </Select>
+                          </div>
+                          <Button
+                            type="primary"
+                            onClick={updateNodeConfig}
+                            className="w-full rounded-lg"
+                            size="large"
+                          >
+                            保存配置
+                          </Button>
+                        </div>
+                      ),
+                    },
+                  ]}
+                  className="[&_.ant-tabs-tab]:!px-4 [&_.ant-tabs-tab]:!py-2 [&_.ant-tabs-tab-active]:!text-blue-600 [&_.ant-tabs-ink-bar]:!bg-blue-600"
+                />
+              </div>
+            </div>
+          ) : (
+            <div className="p-4 text-center text-gray-500 mt-20">
+              <p className="text-sm">请点击画布中的节点查看和编辑属性</p>
+            </div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 17 - 0
src/routes/index.tsx

@@ -3,6 +3,7 @@ import App from '../App';
 import Dashboard from '../Dashboard';
 import Login from '../views/Login';
 import ProtectedRoute from '../components/ProtectedRoute';
+import ProcessDesigner from '../components/ProcessDesigner';
 
 // 路由配置
 export const router = createBrowserRouter([
@@ -44,6 +45,22 @@ export const router = createBrowserRouter([
       </ProtectedRoute>
     ),
   },
+  {
+    path: '/process-designer',
+    element: (
+      <ProtectedRoute>
+        <ProcessDesigner />
+      </ProtectedRoute>
+    ),
+  },
+  {
+    path: '/clientSystem/*',
+    element: (
+      <ProtectedRoute>
+        <Dashboard />
+      </ProtectedRoute>
+    ),
+  },
   {
     path: '*',
     element: <Navigate to="/login" replace />,