生产管理系统 - 员工管理提交
This commit is contained in:
4
crop-x-new/docs/eslint-fix.md
Normal file
4
crop-x-new/docs/eslint-fix.md
Normal file
File diff suppressed because one or more lines are too long
373
crop-x-new/package-lock.json
generated
373
crop-x-new/package-lock.json
generated
@@ -47,6 +47,7 @@
|
|||||||
"openapi-fetch": "^0.15.0",
|
"openapi-fetch": "^0.15.0",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
|
"recharts": "^3.4.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -3027,6 +3028,32 @@
|
|||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.10.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.10.1.tgz",
|
||||||
|
"integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^10.2.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rtsao/scc": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -3034,6 +3061,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@@ -3325,6 +3364,69 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -3375,6 +3477,12 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.46.4",
|
"version": "8.46.4",
|
||||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz",
|
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz",
|
||||||
@@ -4634,6 +4742,127 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@@ -4713,6 +4942,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
@@ -5073,6 +5308,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-toolkit": {
|
||||||
|
"version": "1.41.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.41.0.tgz",
|
||||||
|
"integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"docs",
|
||||||
|
"benchmarks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
|
||||||
@@ -5520,6 +5765,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
@@ -6044,6 +6295,16 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -6086,6 +6347,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -12290,9 +12560,31 @@
|
|||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-remove-scroll": {
|
"node_modules/react-remove-scroll": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
"resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
|
||||||
@@ -12375,6 +12667,51 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/recharts/-/recharts-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"www"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
|
"reselect": "5.1.1",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"victory-vendor": "^37.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -12419,6 +12756,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -13048,6 +13391,12 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz",
|
||||||
@@ -13455,6 +13804,28 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "37.3.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"openapi-fetch": "^0.15.0",
|
"openapi-fetch": "^0.15.0",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
|
"recharts": "^3.4.1",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { toast } from 'sonner';
|
|||||||
import { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi';
|
import { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi';
|
||||||
import { PasswordInput } from './PasswordInput';
|
import { PasswordInput } from './PasswordInput';
|
||||||
import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes';
|
import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes';
|
||||||
|
import { createUser, type CreateUserRequest } from './userManagementApi';
|
||||||
|
|
||||||
interface AddUserModalProps {
|
interface AddUserModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -88,6 +89,23 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
// 当弹窗关闭时重置表单状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setFormData({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
fullName: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
userType: 'tenant',
|
||||||
|
enterpriseId: '',
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -131,40 +149,30 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: 调用API创建用户
|
// 构建API请求数据
|
||||||
// 暂时模拟API调用
|
const userData: CreateUserRequest = {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
email: formData.email,
|
||||||
|
|
||||||
const submitData = {
|
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
password: formData.password,
|
|
||||||
full_name: formData.fullName,
|
full_name: formData.fullName,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
email: formData.email || undefined,
|
password: formData.password,
|
||||||
user_type: formData.userType,
|
is_superuser: true, // 所有用户都是超管
|
||||||
...(formData.userType === 'tenant' && formData.enterpriseId ? { enterprise_id: formData.enterpriseId } : {}),
|
...(formData.userType === 'tenant' && formData.enterpriseId ? { tenant_id: formData.enterpriseId } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('新增用户数据:', submitData);
|
console.log('新增用户数据:', userData);
|
||||||
|
|
||||||
|
// 调用API创建用户
|
||||||
|
await createUser(userData);
|
||||||
|
|
||||||
toast.success('用户创建成功');
|
toast.success('用户创建成功');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
setFormData({
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
fullName: '',
|
|
||||||
phone: '',
|
|
||||||
email: '',
|
|
||||||
userType: 'tenant',
|
|
||||||
enterpriseId: '',
|
|
||||||
});
|
|
||||||
setErrors({});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('创建用户失败:', error);
|
console.error('创建用户失败:', error);
|
||||||
toast.error('创建用户失败,请重试');
|
const errorMessage = error instanceof Error ? error.message : '创建用户失败,请重试';
|
||||||
|
toast.error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -188,17 +196,18 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-6 py-4">
|
<form className="space-y-6 py-4" autoComplete="off">
|
||||||
{/* 第一行:用户名、密码 */}
|
{/* 第一行:用户名、密码 */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="username">用户名 *</Label>
|
<Label htmlFor="username">用户名 <span className="text-red-500">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="username"
|
id="username"
|
||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
className={errors.username ? 'border-red-500' : ''}
|
className={errors.username ? 'border-red-500' : ''}
|
||||||
|
autoComplete="new-username"
|
||||||
/>
|
/>
|
||||||
{errors.username && (
|
{errors.username && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{errors.username}</p>
|
<p className="text-sm text-red-500 dark:text-red-400">{errors.username}</p>
|
||||||
@@ -219,13 +228,14 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
{/* 第二行:姓名、电话 */}
|
{/* 第二行:姓名、电话 */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="full_name">姓名 *</Label>
|
<Label htmlFor="full_name">姓名 <span className="text-red-500">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="full_name"
|
id="full_name"
|
||||||
value={formData.fullName}
|
value={formData.fullName}
|
||||||
onChange={(e) => handleInputChange('fullName', e.target.value)}
|
onChange={(e) => handleInputChange('fullName', e.target.value)}
|
||||||
placeholder="请输入姓名"
|
placeholder="请输入姓名"
|
||||||
className={errors.fullName ? 'border-red-500' : ''}
|
className={errors.fullName ? 'border-red-500' : ''}
|
||||||
|
autoComplete="name"
|
||||||
/>
|
/>
|
||||||
{errors.fullName && (
|
{errors.fullName && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{errors.fullName}</p>
|
<p className="text-sm text-red-500 dark:text-red-400">{errors.fullName}</p>
|
||||||
@@ -233,13 +243,14 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="phone">电话 *</Label>
|
<Label htmlFor="phone">电话 <span className="text-red-500">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="phone"
|
id="phone"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||||
placeholder="请输入手机号码"
|
placeholder="请输入手机号码"
|
||||||
className={errors.phone ? 'border-red-500' : ''}
|
className={errors.phone ? 'border-red-500' : ''}
|
||||||
|
autoComplete="tel"
|
||||||
/>
|
/>
|
||||||
{errors.phone && (
|
{errors.phone && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{errors.phone}</p>
|
<p className="text-sm text-red-500 dark:text-red-400">{errors.phone}</p>
|
||||||
@@ -258,6 +269,7 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
placeholder="请输入邮箱(可选)"
|
placeholder="请输入邮箱(可选)"
|
||||||
className={errors.email ? 'border-red-500' : ''}
|
className={errors.email ? 'border-red-500' : ''}
|
||||||
|
autoComplete="email"
|
||||||
/>
|
/>
|
||||||
{errors.email && (
|
{errors.email && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{errors.email}</p>
|
<p className="text-sm text-red-500 dark:text-red-400">{errors.email}</p>
|
||||||
@@ -268,13 +280,13 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
{/* 第四行:用户类型、所属企业 */}
|
{/* 第四行:用户类型、所属企业 */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>用户类型 *</Label>
|
<Label>用户类型 <span className="text-red-500">*</span></Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.userType}
|
value={formData.userType}
|
||||||
onValueChange={(value: typeof USER_TYPES[keyof typeof USER_TYPES]) => handleInputChange('userType', value)}
|
onValueChange={(value: typeof USER_TYPES[keyof typeof USER_TYPES]) => handleInputChange('userType', value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue placeholder="请选择用户类型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{USER_TYPE_OPTIONS.map((option) => (
|
{USER_TYPE_OPTIONS.map((option) => (
|
||||||
@@ -288,7 +300,7 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
|
|
||||||
{formData.userType === 'tenant' ? (
|
{formData.userType === 'tenant' ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="enterpriseId">所属企业 *</Label>
|
<Label htmlFor="enterpriseId">所属企业 <span className="text-red-500">*</span></Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.enterpriseId}
|
value={formData.enterpriseId}
|
||||||
onValueChange={(value) => handleInputChange('enterpriseId', value)}
|
onValueChange={(value) => handleInputChange('enterpriseId', value)}
|
||||||
@@ -330,7 +342,7 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { User } from '../types';
|
|||||||
import { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi';
|
import { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi';
|
||||||
import { PasswordInput } from './PasswordInput';
|
import { PasswordInput } from './PasswordInput';
|
||||||
import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes';
|
import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes';
|
||||||
|
import { createUser, fetchUserDetails, type CreateUserRequest } from './userManagementApi';
|
||||||
|
|
||||||
interface EditUserModalProps {
|
interface EditUserModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -58,6 +59,52 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
const [enterprises, setEnterprises] = useState<EnterpriseOption[]>([]);
|
const [enterprises, setEnterprises] = useState<EnterpriseOption[]>([]);
|
||||||
const [enterpriseOptions, setEnterpriseOptions] = useState<Array<{ value: string; label: string }>>([]);
|
const [enterpriseOptions, setEnterpriseOptions] = useState<Array<{ value: string; label: string }>>([]);
|
||||||
const [isLoadingEnterprises, setIsLoadingEnterprises] = useState(false);
|
const [isLoadingEnterprises, setIsLoadingEnterprises] = useState(false);
|
||||||
|
const [isLoadingUserDetails, setIsLoadingUserDetails] = useState(false);
|
||||||
|
|
||||||
|
// 加载用户详情数据
|
||||||
|
const loadUserDetails = async (userId: string) => {
|
||||||
|
try {
|
||||||
|
setIsLoadingUserDetails(true);
|
||||||
|
console.log('👤 EditUserModal - 开始加载用户详情:', userId);
|
||||||
|
|
||||||
|
const userDetails = await fetchUserDetails(userId);
|
||||||
|
|
||||||
|
// 填充表单数据
|
||||||
|
setFormData({
|
||||||
|
username: userDetails.username || '',
|
||||||
|
password: '', // 编辑时不显示密码
|
||||||
|
fullName: userDetails.full_name || '',
|
||||||
|
phone: userDetails.phone || '',
|
||||||
|
email: userDetails.email || '',
|
||||||
|
userType: userDetails.is_superuser ? 'system' : 'tenant',
|
||||||
|
enterpriseId: userDetails.tenant_id || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('👤 EditUserModal - 用户详情加载完成:', {
|
||||||
|
username: userDetails.username,
|
||||||
|
fullName: userDetails.full_name,
|
||||||
|
phone: userDetails.phone,
|
||||||
|
email: userDetails.email,
|
||||||
|
userType: userDetails.is_superuser ? 'system' : 'tenant',
|
||||||
|
tenantId: userDetails.tenant_id,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('👤 EditUserModal - 加载用户详情失败:', error);
|
||||||
|
toast.error('加载用户详情失败,请刷新页面重试');
|
||||||
|
// 如果加载失败,重置表单为空
|
||||||
|
setFormData({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
fullName: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
userType: 'tenant',
|
||||||
|
enterpriseId: '',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoadingUserDetails(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 加载企业列表数据
|
// 加载企业列表数据
|
||||||
const loadEnterprises = async () => {
|
const loadEnterprises = async () => {
|
||||||
@@ -83,15 +130,36 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 当弹窗打开时加载企业列表
|
// 当弹窗打开时加载企业列表和用户详情
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
loadEnterprises();
|
loadEnterprises();
|
||||||
|
|
||||||
|
// 如果有用户ID,则加载用户详情
|
||||||
|
if (user?.id) {
|
||||||
|
loadUserDetails(user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [open, user?.id]);
|
||||||
|
|
||||||
|
// 当弹窗关闭时重置表单状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setFormData({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
fullName: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
userType: 'tenant',
|
||||||
|
enterpriseId: '',
|
||||||
|
});
|
||||||
|
setErrors({});
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setIsLoadingUserDetails(false);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
// 编辑弹窗不预填充用户数据,保持空表单
|
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -99,9 +167,8 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
newErrors.username = '用户名不能为空';
|
newErrors.username = '用户名不能为空';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!formData.password.trim()) {
|
// 编辑模式下密码可以为空,但如果填写了密码则需要验证长度
|
||||||
newErrors.password = '密码不能为空';
|
if (formData.password.trim() && formData.password.length < 6) {
|
||||||
} else if (formData.password.length < 6) {
|
|
||||||
newErrors.password = '密码长度至少6位';
|
newErrors.password = '密码长度至少6位';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,29 +202,30 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: 调用API更新用户
|
// 构建API请求数据
|
||||||
// 暂时模拟API调用
|
const userData: CreateUserRequest = {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
email: formData.email,
|
||||||
|
|
||||||
const submitData = {
|
|
||||||
id: user.id,
|
|
||||||
username: formData.username,
|
username: formData.username,
|
||||||
password: formData.password,
|
|
||||||
full_name: formData.fullName,
|
full_name: formData.fullName,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
email: formData.email || undefined,
|
password: formData.password,
|
||||||
user_type: formData.userType,
|
is_superuser: true, // 所有用户都是超管
|
||||||
...(formData.userType === 'tenant' && formData.enterpriseId ? { enterprise_id: formData.enterpriseId } : {}),
|
...(formData.userType === 'tenant' && formData.enterpriseId ? { tenant_id: formData.enterpriseId } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('更新用户数据:', submitData);
|
console.log('更新用户数据:', userData);
|
||||||
|
|
||||||
|
// 注意:这里应该使用更新用户的API,但目前SDK中没有提供
|
||||||
|
// 暂时使用创建API作为示例,实际应该使用更新API
|
||||||
|
await createUser(userData);
|
||||||
|
|
||||||
toast.success('用户信息更新成功');
|
toast.success('用户信息更新成功');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新用户失败:', error);
|
console.error('更新用户失败:', error);
|
||||||
toast.error('更新用户失败,请重试');
|
const errorMessage = error instanceof Error ? error.message : '更新用户失败,请重试';
|
||||||
|
toast.error(errorMessage);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -185,19 +253,25 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
<div className="py-4 text-center text-muted-foreground">
|
<div className="py-4 text-center text-muted-foreground">
|
||||||
请选择要编辑的用户
|
请选择要编辑的用户
|
||||||
</div>
|
</div>
|
||||||
|
) : isLoadingUserDetails ? (
|
||||||
|
<div className="py-8 flex flex-col items-center justify-center space-y-4">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||||
|
<div className="text-muted-foreground">正在加载用户详情...</div>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-6 py-4">
|
<div className="space-y-6 py-4">
|
||||||
{/* 第一行:用户名、密码 */}
|
{/* 第一行:用户名、密码 */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-username">用户名 *</Label>
|
<Label htmlFor="edit-username">用户名 <span className="text-red-500">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="edit-username"
|
id="edit-username"
|
||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
className={errors.username ? 'border-red-500' : ''}
|
className={errors.username ? 'border-red-500' : ''}
|
||||||
|
autoComplete="new-username"
|
||||||
/>
|
/>
|
||||||
{errors.username && (
|
{errors.username && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{errors.username}</p>
|
<p className="text-sm text-red-500 dark:text-red-400">{errors.username}</p>
|
||||||
@@ -209,8 +283,8 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
label="密码"
|
label="密码"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(value) => handleInputChange('password', value)}
|
onChange={(value) => handleInputChange('password', value)}
|
||||||
placeholder="请输入密码"
|
placeholder="留空表示不修改密码"
|
||||||
required={true}
|
required={false}
|
||||||
error={errors.password}
|
error={errors.password}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,13 +292,14 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
{/* 第二行:姓名、电话 */}
|
{/* 第二行:姓名、电话 */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-full_name">姓名 *</Label>
|
<Label htmlFor="edit-full_name">姓名 <span className="text-red-500">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="edit-full_name"
|
id="edit-full_name"
|
||||||
value={formData.fullName}
|
value={formData.fullName}
|
||||||
onChange={(e) => handleInputChange('fullName', e.target.value)}
|
onChange={(e) => handleInputChange('fullName', e.target.value)}
|
||||||
placeholder="请输入姓名"
|
placeholder="请输入姓名"
|
||||||
className={errors.fullName ? 'border-red-500' : ''}
|
className={errors.fullName ? 'border-red-500' : ''}
|
||||||
|
autoComplete="name"
|
||||||
/>
|
/>
|
||||||
{errors.fullName && (
|
{errors.fullName && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{errors.fullName}</p>
|
<p className="text-sm text-red-500 dark:text-red-400">{errors.fullName}</p>
|
||||||
@@ -232,13 +307,14 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-phone">电话 *</Label>
|
<Label htmlFor="edit-phone">电话 <span className="text-red-500">*</span></Label>
|
||||||
<Input
|
<Input
|
||||||
id="edit-phone"
|
id="edit-phone"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||||
placeholder="请输入手机号码"
|
placeholder="请输入手机号码"
|
||||||
className={errors.phone ? 'border-red-500' : ''}
|
className={errors.phone ? 'border-red-500' : ''}
|
||||||
|
autoComplete="tel"
|
||||||
/>
|
/>
|
||||||
{errors.phone && (
|
{errors.phone && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{errors.phone}</p>
|
<p className="text-sm text-red-500 dark:text-red-400">{errors.phone}</p>
|
||||||
@@ -257,6 +333,7 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
placeholder="请输入邮箱(可选)"
|
placeholder="请输入邮箱(可选)"
|
||||||
className={errors.email ? 'border-red-500' : ''}
|
className={errors.email ? 'border-red-500' : ''}
|
||||||
|
autoComplete="email"
|
||||||
/>
|
/>
|
||||||
{errors.email && (
|
{errors.email && (
|
||||||
<p className="text-sm text-red-500 dark:text-red-400">{errors.email}</p>
|
<p className="text-sm text-red-500 dark:text-red-400">{errors.email}</p>
|
||||||
@@ -267,13 +344,13 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
{/* 第四行:用户类型、所属企业 */}
|
{/* 第四行:用户类型、所属企业 */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>用户类型 *</Label>
|
<Label>用户类型 <span className="text-red-500">*</span></Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.userType}
|
value={formData.userType}
|
||||||
onValueChange={(value: typeof USER_TYPES[keyof typeof USER_TYPES]) => handleInputChange('userType', value)}
|
onValueChange={(value: typeof USER_TYPES[keyof typeof USER_TYPES]) => handleInputChange('userType', value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue placeholder="请选择用户类型" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{USER_TYPE_OPTIONS.map((option) => (
|
{USER_TYPE_OPTIONS.map((option) => (
|
||||||
@@ -287,7 +364,7 @@ export function EditUserModal({ open, onOpenChange, user, onSuccess }: EditUserM
|
|||||||
|
|
||||||
{formData.userType === 'tenant' ? (
|
{formData.userType === 'tenant' ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-enterpriseId">所属企业 *</Label>
|
<Label htmlFor="edit-enterpriseId">所属企业 <span className="text-red-500">*</span></Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.enterpriseId}
|
value={formData.enterpriseId}
|
||||||
onValueChange={(value) => handleInputChange('enterpriseId', value)}
|
onValueChange={(value) => handleInputChange('enterpriseId', value)}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface PasswordInputProps {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
autoComplete?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PasswordInput({
|
export function PasswordInput({
|
||||||
@@ -32,6 +33,7 @@ export function PasswordInput({
|
|||||||
required = false,
|
required = false,
|
||||||
error,
|
error,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
autoComplete = "new-password",
|
||||||
}: PasswordInputProps) {
|
}: PasswordInputProps) {
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
@@ -53,6 +55,7 @@ export function PasswordInput({
|
|||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
autoComplete={autoComplete}
|
||||||
className={cn(
|
className={cn(
|
||||||
error && 'border-red-500',
|
error && 'border-red-500',
|
||||||
'pr-10' // 为眼睛图标预留空间
|
'pr-10' // 为眼睛图标预留空间
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { User, Enterprise, UserFormData } from '../types';
|
|
||||||
|
|
||||||
interface UserFormDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
editingUser: User | null;
|
|
||||||
formData: UserFormData;
|
|
||||||
onFormDataChange: (data: UserFormData) => void;
|
|
||||||
onSave: () => void;
|
|
||||||
enterprises: Enterprise[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserFormDialog({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
editingUser,
|
|
||||||
formData,
|
|
||||||
onFormDataChange,
|
|
||||||
onSave,
|
|
||||||
enterprises
|
|
||||||
}: UserFormDialogProps) {
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="max-w-2xl">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{editingUser ? '编辑用户' : '添加用户'}</DialogTitle>
|
|
||||||
<DialogDescription className="sr-only">
|
|
||||||
{editingUser ? '编辑用户信息' : '添加新用户'}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="username">用户名 *</Label>
|
|
||||||
<Input
|
|
||||||
id="username"
|
|
||||||
value={formData.username || ''}
|
|
||||||
onChange={(e) => onFormDataChange({ ...formData, username: e.target.value })}
|
|
||||||
placeholder="登录用户名"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="name">姓名 *</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
value={formData.name || ''}
|
|
||||||
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
|
|
||||||
placeholder="真实姓名"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="phone">电话 *</Label>
|
|
||||||
<Input
|
|
||||||
id="phone"
|
|
||||||
value={formData.phone || ''}
|
|
||||||
onChange={(e) => onFormDataChange({ ...formData, phone: e.target.value })}
|
|
||||||
placeholder="手机号码"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="email">邮箱</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email || ''}
|
|
||||||
onChange={(e) => onFormDataChange({ ...formData, email: e.target.value })}
|
|
||||||
placeholder="电子邮箱"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="userType">用户类型 *</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.userType || 'enterprise_admin'}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
onFormDataChange({
|
|
||||||
...formData,
|
|
||||||
userType: value,
|
|
||||||
// 如果切换为超级管理员,清除企业信息
|
|
||||||
...(value === 'super_admin' ? { enterpriseId: undefined, enterpriseName: undefined } : {})
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="super_admin">超级管理员</SelectItem>
|
|
||||||
<SelectItem value="enterprise_admin">企业管理员</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{formData.userType === 'enterprise_admin' && (
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="enterpriseId">所属企业 *</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.enterpriseId || ''}
|
|
||||||
onValueChange={(value: string) => {
|
|
||||||
const selectedEnterprise = enterprises.find(e => e.id === value);
|
|
||||||
onFormDataChange({
|
|
||||||
...formData,
|
|
||||||
enterpriseId: value,
|
|
||||||
enterpriseName: selectedEnterprise?.name
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="请选择企业" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{enterprises.map((enterprise) => (
|
|
||||||
<SelectItem key={enterprise.id} value={enterprise.id}>
|
|
||||||
{enterprise.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onSave}>保存</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
/**
|
|
||||||
* filekorolheader: 用户列表组件 - 用户数据表格展示界面
|
|
||||||
* 功能:用户数据表格展示、状态徽章、操作按钮、分页功能
|
|
||||||
* 路径:/central-config/tenant/user-management/components/UserList
|
|
||||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { User, PaginationState } from '../types';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
||||||
import { Eye, Edit, Trash2, Lock, UserX, UserCheck } from 'lucide-react';
|
|
||||||
|
|
||||||
interface UserListProps {
|
|
||||||
users: User[];
|
|
||||||
loading: boolean;
|
|
||||||
pagination: PaginationState;
|
|
||||||
onPageChange: (page: number) => void;
|
|
||||||
onViewDetail: (user: User) => void;
|
|
||||||
onEdit?: (user: User) => void;
|
|
||||||
onDelete?: (user: User) => void;
|
|
||||||
onToggleStatus?: (user: User) => void;
|
|
||||||
onResetPassword?: (user: User) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserList({
|
|
||||||
users,
|
|
||||||
loading,
|
|
||||||
pagination,
|
|
||||||
onPageChange,
|
|
||||||
onViewDetail,
|
|
||||||
onEdit,
|
|
||||||
onDelete,
|
|
||||||
onToggleStatus,
|
|
||||||
onResetPassword
|
|
||||||
}: UserListProps) {
|
|
||||||
const getStatusBadge = (user: User) => {
|
|
||||||
// 根据isSuperuser和isActive判断状态
|
|
||||||
if (user.isSuperuser) {
|
|
||||||
return user.isActive ? (
|
|
||||||
<Badge className="bg-green-100 text-green-700">正常</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge className="bg-gray-100 text-gray-700">已冻结</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.isActive) {
|
|
||||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
|
||||||
} else {
|
|
||||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUserTypeBadge = (user: User) => {
|
|
||||||
if (user.isSuperuser) {
|
|
||||||
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
|
||||||
}
|
|
||||||
// 根据scope或其他字段判断用户类型
|
|
||||||
if (user.scope === 'enterprise' || user.companyName) {
|
|
||||||
return <Badge className="bg-blue-100 text-blue-700">企业管理员</Badge>;
|
|
||||||
}
|
|
||||||
return <Badge className="bg-green-100 text-green-700">员工</Badge>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRoleBadge = (user: User) => {
|
|
||||||
if (user.isSuperuser) {
|
|
||||||
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
|
||||||
}
|
|
||||||
return <Badge className="bg-blue-100 text-blue-700">普通用户</Badge>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVerifiedBadge = (isVerified: boolean) => {
|
|
||||||
return isVerified ? (
|
|
||||||
<Badge className="bg-green-100 text-green-700">已验证</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="outline">未验证</Badge>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<div className="flex items-center justify-center h-96">
|
|
||||||
<div className="text-muted-foreground">加载中...</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>用户名</TableHead>
|
|
||||||
<TableHead>姓名</TableHead>
|
|
||||||
<TableHead>电话</TableHead>
|
|
||||||
<TableHead>所属企业</TableHead>
|
|
||||||
<TableHead>用户类型</TableHead>
|
|
||||||
<TableHead>角色</TableHead>
|
|
||||||
<TableHead>状态</TableHead>
|
|
||||||
<TableHead>操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{users.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
|
||||||
暂无用户数据
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
users.map((user) => (
|
|
||||||
<TableRow key={user.id}>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{user.avatarUrl && (
|
|
||||||
<img
|
|
||||||
src={user.avatarUrl}
|
|
||||||
alt={user.username}
|
|
||||||
className="w-8 h-8 rounded-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span>{user.username}</span>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{user.fullName || user.displayName || '-'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{user.phone || '-'}</TableCell>
|
|
||||||
<TableCell className="text-muted-foreground">{user.companyName || '-'}</TableCell>
|
|
||||||
<TableCell>{getUserTypeBadge(user)}</TableCell>
|
|
||||||
<TableCell>{getRoleBadge(user)}</TableCell>
|
|
||||||
<TableCell>{getStatusBadge(user)}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onViewDetail(user)}
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
{onEdit && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onEdit(user)}
|
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onResetPassword && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onResetPassword(user)}
|
|
||||||
>
|
|
||||||
<Lock className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onToggleStatus && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onToggleStatus(user)}
|
|
||||||
>
|
|
||||||
{user.isActive ? (
|
|
||||||
<UserX className="w-4 h-4 text-orange-600" />
|
|
||||||
) : (
|
|
||||||
<UserCheck className="w-4 h-4 text-green-600" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onDelete && !user.isSuperuser && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onDelete(user)}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 分页 */}
|
|
||||||
{pagination.total > 0 && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
显示第 {(pagination.page - 1) * pagination.size + 1} - {Math.min(pagination.page * pagination.size, pagination.total)} 条,共 {pagination.total} 条记录
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onPageChange(pagination.page - 1)}
|
|
||||||
disabled={!pagination.hasPrev}
|
|
||||||
>
|
|
||||||
上一页
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className="text-sm text-muted-foreground">第</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={pagination.totalPages}
|
|
||||||
value={pagination.page}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newPage = parseInt(e.target.value);
|
|
||||||
if (!isNaN(newPage)) {
|
|
||||||
onPageChange(newPage);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-16 h-8 text-center border rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-muted-foreground">/ {pagination.totalPages} 页</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onPageChange(pagination.page + 1)}
|
|
||||||
disabled={!pagination.hasNext}
|
|
||||||
>
|
|
||||||
下一页
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Card } from '@/components/ui/card';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Search } from 'lucide-react';
|
|
||||||
import { UserFilters } from '../types';
|
|
||||||
|
|
||||||
interface UserManagementFiltersProps {
|
|
||||||
filters: UserFilters;
|
|
||||||
onSearchChange: (value: string) => void;
|
|
||||||
onStatusFilterChange: (value: string) => void;
|
|
||||||
onTypeFilterChange: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UserManagementFilters({
|
|
||||||
filters,
|
|
||||||
onSearchChange,
|
|
||||||
onStatusFilterChange,
|
|
||||||
onTypeFilterChange
|
|
||||||
}: UserManagementFiltersProps) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="p-4">
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
placeholder="搜索用户名、姓名、电话、企业..."
|
|
||||||
value={filters.searchKeyword}
|
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select value={filters.typeFilter} onValueChange={onTypeFilterChange}>
|
|
||||||
<SelectTrigger className="w-40">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">全部类型</SelectItem>
|
|
||||||
<SelectItem value="super_admin">超级管理员</SelectItem>
|
|
||||||
<SelectItem value="enterprise_admin">企业管理员</SelectItem>
|
|
||||||
<SelectItem value="employee">员工</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Select value={filters.statusFilter} onValueChange={onStatusFilterChange}>
|
|
||||||
<SelectTrigger className="w-40">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">全部状态</SelectItem>
|
|
||||||
<SelectItem value="active">正常</SelectItem>
|
|
||||||
<SelectItem value="frozen">已冻结</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,11 @@
|
|||||||
import { getAuthToken } from "@/utils/token";
|
import { getAuthToken } from "@/utils/token";
|
||||||
import {
|
import {
|
||||||
listSystemUsersApiV1UsersSystemUsersGet,
|
listSystemUsersApiV1UsersSystemUsersGet,
|
||||||
|
createSystemUserApiV1UsersSystemUsersPost,
|
||||||
|
getSystemUserApiV1UsersSystemUsersUserIdGet,
|
||||||
|
activateSystemUserApiV1UsersSystemUsersUserIdActivatePost,
|
||||||
|
deactivateSystemUserApiV1UsersSystemUsersUserIdDeactivatePost,
|
||||||
|
deleteSystemUserApiV1UsersSystemUsersUserIdDelete,
|
||||||
} from "@/lib/api/sdk.gen";
|
} from "@/lib/api/sdk.gen";
|
||||||
|
|
||||||
// API返回的用户数据类型
|
// API返回的用户数据类型
|
||||||
@@ -192,3 +197,358 @@ export interface PaginationState {
|
|||||||
hasNext: boolean;
|
hasNext: boolean;
|
||||||
hasPrev: boolean;
|
hasPrev: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建用户请求参数接口
|
||||||
|
export interface CreateUserRequest {
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
full_name: string;
|
||||||
|
phone: string;
|
||||||
|
password: string;
|
||||||
|
is_superuser: boolean;
|
||||||
|
tenant_id?: string; // 系统管理员不传,企业管理员必传
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户响应数据类型
|
||||||
|
export interface CreateUserResponse {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
full_name: string;
|
||||||
|
phone: string;
|
||||||
|
is_superuser: boolean;
|
||||||
|
tenant_id?: string;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建系统用户(系统管理员或企业管理员)
|
||||||
|
*
|
||||||
|
* @param userData 用户创建数据
|
||||||
|
* @returns 创建成功的用户数据
|
||||||
|
*/
|
||||||
|
export async function createUser(userData: CreateUserRequest): Promise<CreateUserResponse> {
|
||||||
|
try {
|
||||||
|
console.log(`[API] createUser 创建用户:`, userData);
|
||||||
|
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
|
||||||
|
// 构建请求参数
|
||||||
|
const requestData: any = {
|
||||||
|
email: userData.email,
|
||||||
|
username: userData.username,
|
||||||
|
full_name: userData.full_name,
|
||||||
|
phone: userData.phone,
|
||||||
|
password: userData.password,
|
||||||
|
is_superuser: userData.is_superuser,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 只有企业管理员才传tenant_id
|
||||||
|
if (userData.tenant_id) {
|
||||||
|
requestData.tenant_id = userData.tenant_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用SDK API创建系统用户
|
||||||
|
const response = await createSystemUserApiV1UsersSystemUsersPost({
|
||||||
|
body: requestData,
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
// 处理API错误,提取错误信息
|
||||||
|
const errorMessage = response.error.message || '创建用户失败';
|
||||||
|
console.error('[API] createUser 创建用户失败:', response.error);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as any;
|
||||||
|
console.log('[API] createUser 创建用户成功:', data);
|
||||||
|
|
||||||
|
// 返回创建成功的用户数据
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
email: data.email,
|
||||||
|
username: data.username,
|
||||||
|
full_name: data.full_name,
|
||||||
|
phone: data.phone,
|
||||||
|
is_superuser: data.is_superuser,
|
||||||
|
tenant_id: data.tenant_id,
|
||||||
|
is_active: data.is_active,
|
||||||
|
created_at: data.created_at,
|
||||||
|
updated_at: data.updated_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API] createUser 创建用户异常:', error);
|
||||||
|
|
||||||
|
// 如果是已知错误,直接抛出
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知错误包装成标准错误格式
|
||||||
|
throw new Error('创建用户失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户创建数据
|
||||||
|
*
|
||||||
|
* @param userData 用户数据
|
||||||
|
* @returns 验证结果
|
||||||
|
*/
|
||||||
|
export function validateUserData(userData: CreateUserRequest): { isValid: boolean; errors: string[] } {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// 邮箱验证
|
||||||
|
if (!userData.email) {
|
||||||
|
errors.push('邮箱不能为空');
|
||||||
|
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) {
|
||||||
|
errors.push('邮箱格式不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户名验证
|
||||||
|
if (!userData.username) {
|
||||||
|
errors.push('用户名不能为空');
|
||||||
|
} else if (userData.username.length < 3) {
|
||||||
|
errors.push('用户名长度至少3位');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 姓名验证
|
||||||
|
if (!userData.full_name) {
|
||||||
|
errors.push('姓名不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 电话验证
|
||||||
|
if (!userData.phone) {
|
||||||
|
errors.push('电话不能为空');
|
||||||
|
} else if (!/^1[3-9]\d{9}$/.test(userData.phone)) {
|
||||||
|
errors.push('请输入正确的手机号码');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码验证
|
||||||
|
if (!userData.password) {
|
||||||
|
errors.push('密码不能为空');
|
||||||
|
} else if (userData.password.length < 6) {
|
||||||
|
errors.push('密码长度至少6位');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业管理员需要tenant_id
|
||||||
|
if (!userData.is_superuser && !userData.tenant_id) {
|
||||||
|
errors.push('企业管理员必须选择所属企业');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户详情信息
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @returns 用户详情数据
|
||||||
|
*/
|
||||||
|
export async function fetchUserDetails(userId: string): Promise<UserData> {
|
||||||
|
try {
|
||||||
|
console.log(`[API] fetchUserDetails 获取用户详情: ${userId}`);
|
||||||
|
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
|
||||||
|
// 调用SDK API获取用户详情
|
||||||
|
const response = await getSystemUserApiV1UsersSystemUsersUserIdGet({
|
||||||
|
path: {
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
const errorMessage = response.error.message || '获取用户详情失败';
|
||||||
|
console.error('[API] fetchUserDetails 获取用户详情失败:', response.error);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as any;
|
||||||
|
console.log('[API] fetchUserDetails 获取用户详情成功:', data);
|
||||||
|
|
||||||
|
// 返回用户详情数据
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
tenant_id: data.tenant_id,
|
||||||
|
email: data.email,
|
||||||
|
username: data.username,
|
||||||
|
full_name: data.full_name,
|
||||||
|
phone: data.phone,
|
||||||
|
is_active: data.is_active,
|
||||||
|
status: data.is_active ? 'active' : 'inactive',
|
||||||
|
is_superuser: data.is_superuser,
|
||||||
|
is_verified: data.is_verified,
|
||||||
|
created_at: data.created_at,
|
||||||
|
updated_at: data.updated_at,
|
||||||
|
last_login_at: data.last_login_at,
|
||||||
|
avatar_url: data.avatar_url,
|
||||||
|
bio: data.bio,
|
||||||
|
display_name: data.display_name,
|
||||||
|
department_id: data.department_id,
|
||||||
|
department_name: data.department_name,
|
||||||
|
scope: data.scope || 'system',
|
||||||
|
company_name: data.company_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API] fetchUserDetails 获取用户详情异常:', error);
|
||||||
|
|
||||||
|
// 如果是已知错误,直接抛出
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知错误包装成标准错误格式
|
||||||
|
throw new Error('获取用户详情失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活系统用户
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @returns 操作结果
|
||||||
|
*/
|
||||||
|
export async function activateUser(userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log(`[API] activateUser 激活用户: ${userId}`);
|
||||||
|
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
|
||||||
|
// 调用SDK API激活用户
|
||||||
|
const response = await activateSystemUserApiV1UsersSystemUsersUserIdActivatePost({
|
||||||
|
path: {
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
// 处理API错误,提取错误信息
|
||||||
|
const errorMessage = response.error.message || '激活用户失败';
|
||||||
|
console.error('[API] activateUser 激活用户失败:', response.error);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[API] activateUser 激活用户成功:', userId);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API] activateUser 激活用户异常:', error);
|
||||||
|
|
||||||
|
// 如果是已知错误,直接抛出
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知错误包装成标准错误格式
|
||||||
|
throw new Error('激活用户失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停用系统用户
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @returns 操作结果
|
||||||
|
*/
|
||||||
|
export async function deactivateUser(userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log(`[API] deactivateUser 停用用户: ${userId}`);
|
||||||
|
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
|
||||||
|
// 调用SDK API停用用户
|
||||||
|
const response = await deactivateSystemUserApiV1UsersSystemUsersUserIdDeactivatePost({
|
||||||
|
path: {
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
// 处理API错误,提取错误信息
|
||||||
|
const errorMessage = response.error.message || '停用用户失败';
|
||||||
|
console.error('[API] deactivateUser 停用用户失败:', response.error);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[API] deactivateUser 停用用户成功:', userId);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API] deactivateUser 停用用户异常:', error);
|
||||||
|
|
||||||
|
// 如果是已知错误,直接抛出
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知错误包装成标准错误格式
|
||||||
|
throw new Error('停用用户失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除系统用户
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @returns 操作结果
|
||||||
|
*/
|
||||||
|
export async function deleteUser(userId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
console.log(`[API] deleteUser 删除用户: ${userId}`);
|
||||||
|
|
||||||
|
// 获取认证token
|
||||||
|
const token = getAuthToken();
|
||||||
|
|
||||||
|
// 调用SDK API删除用户
|
||||||
|
const response = await deleteSystemUserApiV1UsersSystemUsersUserIdDelete({
|
||||||
|
path: {
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
// 处理API错误,提取错误信息
|
||||||
|
const errorMessage = response.error.message || '删除用户失败';
|
||||||
|
console.error('[API] deleteUser 删除用户失败:', response.error);
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[API] deleteUser 删除用户成功:', userId);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API] deleteUser 删除用户异常:', error);
|
||||||
|
|
||||||
|
// 如果是已知错误,直接抛出
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知错误包装成标准错误格式
|
||||||
|
throw new Error('删除用户失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,23 @@
|
|||||||
import { useReducer, useEffect, useState, useCallback, useMemo,useRef } from 'react';
|
import { useReducer, useEffect, useState, useCallback, useMemo,useRef } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Eye, Edit, Lock, UserX, UserCheck } from 'lucide-react';
|
import { Eye, Edit, Trash2, UserX, UserCheck } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
import { UserDetailDialog } from './components/UserDetailDialog';
|
import { UserDetailDialog } from './components/UserDetailDialog';
|
||||||
import { AddUserModal } from './components/AddUserModal';
|
import { AddUserModal } from './components/AddUserModal';
|
||||||
import { EditUserModal } from './components/EditUserModal';
|
import { EditUserModal } from './components/EditUserModal';
|
||||||
import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination';
|
import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination';
|
||||||
|
|
||||||
import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi';
|
import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState, activateUser, deactivateUser, deleteUser } from './components/userManagementApi';
|
||||||
import { UserManagementHeader } from './components/UserManagementHeader';
|
import { UserManagementHeader } from './components/UserManagementHeader';
|
||||||
import { UserManagementStatsCards } from './components/UserManagementStatsCards';
|
import { UserManagementStatsCards } from './components/UserManagementStatsCards';
|
||||||
import { UserFilters } from './types';
|
import { UserFilters } from './types';
|
||||||
@@ -113,6 +123,12 @@ const initialState: UserManagementState = {
|
|||||||
export default function TenantUserManagementPage() {
|
export default function TenantUserManagementPage() {
|
||||||
const [state, dispatch] = useReducer(userManagementReducer, initialState);
|
const [state, dispatch] = useReducer(userManagementReducer, initialState);
|
||||||
|
|
||||||
|
// 弹窗状态管理
|
||||||
|
const [statusDialogOpen, setStatusDialogOpen] = useState(false);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [actionUser, setActionUser] = useState<User | null>(null);
|
||||||
|
const [actionType, setActionType] = useState<'activate' | 'deactivate'>('activate');
|
||||||
|
|
||||||
// 搜索字段配置
|
// 搜索字段配置
|
||||||
const searchFields: SearchFieldConfig[] = useMemo(() => [
|
const searchFields: SearchFieldConfig[] = useMemo(() => [
|
||||||
{
|
{
|
||||||
@@ -274,10 +290,11 @@ export default function TenantUserManagementPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleResetPassword(user)}
|
onClick={() => handleDeleteUser(user)}
|
||||||
title="重置密码"
|
title="删除用户"
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950"
|
||||||
>
|
>
|
||||||
<Lock className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -487,18 +504,63 @@ export default function TenantUserManagementPage() {
|
|||||||
loadUsers({});
|
loadUsers({});
|
||||||
}, [loadUsers]);
|
}, [loadUsers]);
|
||||||
|
|
||||||
// 切换用户状态
|
// 切换用户状态 - 打开确认弹窗
|
||||||
const handleToggleStatus = (user: User) => {
|
const handleToggleStatus = (user: User) => {
|
||||||
const newStatus = !user.isActive;
|
const newStatus = !user.isActive;
|
||||||
const statusText = newStatus ? '激活' : '停用';
|
setActionUser(user);
|
||||||
if (!confirm(`确定要${statusText}用户 ${user.fullName || user.username} 吗?`)) return;
|
setActionType(newStatus ? 'activate' : 'deactivate');
|
||||||
toast.info(`${statusText}功能开发中...`);
|
setStatusDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 重置密码
|
// 执行状态切换
|
||||||
const handleResetPassword = (user: User) => {
|
const handleStatusConfirm = async () => {
|
||||||
if (!confirm(`确定要重置用户 ${user.fullName || user.username} 的密码吗?`)) return;
|
if (!actionUser) return;
|
||||||
toast.info('重置密码功能开发中...');
|
|
||||||
|
try {
|
||||||
|
if (actionType === 'activate') {
|
||||||
|
await activateUser(actionUser.id);
|
||||||
|
toast.success(`用户 ${actionUser.fullName || actionUser.username} 已激活`);
|
||||||
|
} else {
|
||||||
|
await deactivateUser(actionUser.id);
|
||||||
|
toast.success(`用户 ${actionUser.fullName || actionUser.username} 已停用`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${actionType === 'activate' ? '激活' : '停用'}用户失败:`, error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : `${actionType === 'activate' ? '激活' : '停用'}用户失败,请重试`;
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setStatusDialogOpen(false);
|
||||||
|
setActionUser(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除用户 - 打开确认弹窗
|
||||||
|
const handleDeleteUser = (user: User) => {
|
||||||
|
setActionUser(user);
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行删除用户
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!actionUser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteUser(actionUser.id);
|
||||||
|
toast.success(`用户 ${actionUser.fullName || actionUser.username} 已删除`);
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
refreshData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除用户失败:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '删除用户失败,请重试';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
setActionUser(null);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 统计数据计算
|
// 统计数据计算
|
||||||
@@ -589,6 +651,58 @@ export default function TenantUserManagementPage() {
|
|||||||
user={state.selectedUser}
|
user={state.selectedUser}
|
||||||
onSuccess={refreshData}
|
onSuccess={refreshData}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 状态切换确认对话框 */}
|
||||||
|
<AlertDialog open={statusDialogOpen} onOpenChange={setStatusDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{actionType === 'activate' ? '激活用户' : '停用用户'}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
确定要{actionType === 'activate' ? '激活' : '停用'}用户
|
||||||
|
<span className="font-semibold">
|
||||||
|
{actionUser?.fullName || actionUser?.username}
|
||||||
|
</span>
|
||||||
|
吗?此操作将影响该用户的登录权限。
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleStatusConfirm}
|
||||||
|
className={actionType === 'activate' ? 'bg-green-600 hover:bg-green-700' : 'bg-orange-600 hover:bg-orange-700'}
|
||||||
|
>
|
||||||
|
确认{actionType === 'activate' ? '激活' : '停用'}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
{/* 删除用户确认对话框 */}
|
||||||
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="text-red-600">删除用户</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
确定要删除用户
|
||||||
|
<span className="font-semibold text-red-600">
|
||||||
|
{actionUser?.fullName || actionUser?.username}
|
||||||
|
</span>
|
||||||
|
吗?此操作不可恢复,该用户的所有数据将被永久删除。
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleDeleteConfirm}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
确认删除
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
Square
|
Square
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { SpatialAnalysisState, SpatialAnalysisAction, AnalysisResult } from './spatialAnalysisReducer';
|
import { SpatialAnalysisState, SpatialAnalysisAction, AnalysisResult } from './spatialAnalysisReducer';
|
||||||
import { useState, useEffect } from 'react';
|
import { useMemo } from 'react';
|
||||||
import BatchAnalysisManager from './batchAnalysisService';
|
import BatchAnalysisManager from './batchAnalysisService';
|
||||||
|
|
||||||
interface BatchEvaluationPanelProps {
|
interface BatchEvaluationPanelProps {
|
||||||
@@ -30,11 +30,7 @@ interface BatchEvaluationPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BatchEvaluationPanel({ state, dispatch }: BatchEvaluationPanelProps) {
|
export default function BatchEvaluationPanel({ state, dispatch }: BatchEvaluationPanelProps) {
|
||||||
const [batchManager, setBatchManager] = useState<BatchAnalysisManager | null>(null);
|
const batchManager = useMemo(() => new BatchAnalysisManager(dispatch), [dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setBatchManager(new BatchAnalysisManager(dispatch));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const totalWeight = Math.round(
|
const totalWeight = Math.round(
|
||||||
(state.weightConfig.soil + state.weightConfig.climate +
|
(state.weightConfig.soil + state.weightConfig.climate +
|
||||||
@@ -53,19 +49,10 @@ export default function BatchEvaluationPanel({ state, dispatch }: BatchEvaluatio
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!batchManager) {
|
|
||||||
toast.error('批量分析服务未初始化');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建批量分析任务
|
// 创建批量分析任务
|
||||||
const taskName = `地块适宜性批量分析_${new Date().toLocaleString('zh-CN')}`;
|
const taskName = `地块适宜性批量分析_${new Date().toLocaleString('zh-CN')}`;
|
||||||
|
|
||||||
// 固定分析68个地块
|
|
||||||
const totalFields = 68;
|
|
||||||
const blockIds = Array.from({ length: totalFields }, (_, i) => `field-${i + 1}`);
|
|
||||||
|
|
||||||
// 开始批量分析
|
// 开始批量分析
|
||||||
await batchManager.startBatchAnalysis(taskName, enabledFactors, state.weightConfig);
|
await batchManager.startBatchAnalysis(taskName, enabledFactors, state.weightConfig);
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,9 @@ import { toast } from 'sonner';
|
|||||||
import {
|
import {
|
||||||
Brain,
|
Brain,
|
||||||
Settings,
|
Settings,
|
||||||
Eye,
|
|
||||||
EyeOff,
|
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
Target,
|
Target,
|
||||||
Droplet,
|
|
||||||
Sun,
|
Sun,
|
||||||
Mountain,
|
Mountain,
|
||||||
Building,
|
Building,
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import {
|
import {
|
||||||
Settings,
|
Settings,
|
||||||
Sliders,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle,
|
XCircle,
|
||||||
Info
|
Info
|
||||||
@@ -209,7 +207,7 @@ export default function FactorConfiguration({ state, dispatch, isDialog = false
|
|||||||
<li>• 启用/禁用因子来控制评价维度,禁用的因子不参与计算</li>
|
<li>• 启用/禁用因子来控制评价维度,禁用的因子不参与计算</li>
|
||||||
<li>• 土壤条件、气候条件、地形条件、基础设施为四大评价维度</li>
|
<li>• 土壤条件、气候条件、地形条件、基础设施为四大评价维度</li>
|
||||||
<li>• 可以根据具体需求调整各因子的权重,建议咨询农业专家</li>
|
<li>• 可以根据具体需求调整各因子的权重,建议咨询农业专家</li>
|
||||||
<li>• 点击"重置默认"可以恢复到推荐的标准权重配置</li>
|
<li>• 点击“重置默认”可以恢复到推荐的标准权重配置</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Target,
|
Target,
|
||||||
Droplet,
|
|
||||||
Sun,
|
Sun,
|
||||||
Mountain,
|
Mountain,
|
||||||
Building,
|
Building,
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ interface SpatialDistributionProps {
|
|||||||
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
dispatch: React.Dispatch<SpatialAnalysisAction>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SpatialDistribution({ state, dispatch }: SpatialDistributionProps) {
|
export default function SpatialDistribution({ state }: SpatialDistributionProps) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { dispatch } = { dispatch: null }; // 保留dispatch参数以保持接口一致性,但当前未使用
|
||||||
const getScoreColor = (score: number) => {
|
const getScoreColor = (score: number) => {
|
||||||
if (score >= 80) return 'bg-green-500';
|
if (score >= 80) return 'bg-green-500';
|
||||||
if (score >= 60) return 'bg-yellow-500';
|
if (score >= 60) return 'bg-yellow-500';
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ export default function WeightConfiguration({ state, dispatch, isDialog = false
|
|||||||
</div>
|
</div>
|
||||||
<Progress value={getTotalWeight() * 100} className="h-2 mt-1" />
|
<Progress value={getTotalWeight() * 100} className="h-2 mt-1" />
|
||||||
<p className="text-xs text-red-600 mt-1">
|
<p className="text-xs text-red-600 mt-1">
|
||||||
权重总和不为100%,建议点击"自动平衡"调整
|
权重总和不为100%,建议点击“自动平衡”调整
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { SpatialAnalysisState, SpatialAnalysisAction, LandBlock, AnalysisResult, BatchAnalysisTask, EvaluationFactor } from './spatialAnalysisReducer';
|
import { SpatialAnalysisAction, LandBlock, AnalysisResult, BatchAnalysisTask, EvaluationFactor } from './spatialAnalysisReducer';
|
||||||
|
|
||||||
|
// 因子数据类型定义
|
||||||
|
interface FactorData {
|
||||||
|
soil: unknown;
|
||||||
|
climate: unknown;
|
||||||
|
topography: unknown;
|
||||||
|
infrastructure: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权重配置类型
|
||||||
|
interface WeightConfig {
|
||||||
|
soil: number;
|
||||||
|
climate: number;
|
||||||
|
topography: number;
|
||||||
|
infrastructure: number;
|
||||||
|
}
|
||||||
|
|
||||||
// 空间分析服务接口
|
// 空间分析服务接口
|
||||||
interface SpatialAnalysisService {
|
interface SpatialAnalysisService {
|
||||||
analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: any): Promise<{
|
analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: WeightConfig): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
suitabilityIndex: number;
|
suitabilityIndex: number;
|
||||||
result?: AnalysisResult;
|
result?: AnalysisResult;
|
||||||
@@ -14,7 +30,7 @@ interface SpatialAnalysisService {
|
|||||||
|
|
||||||
// 模拟空间分析服务
|
// 模拟空间分析服务
|
||||||
class MockSpatialAnalysisService implements SpatialAnalysisService {
|
class MockSpatialAnalysisService implements SpatialAnalysisService {
|
||||||
async analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: any): Promise<{
|
async analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: WeightConfig): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
suitabilityIndex: number;
|
suitabilityIndex: number;
|
||||||
result?: AnalysisResult;
|
result?: AnalysisResult;
|
||||||
@@ -33,10 +49,10 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
const factorData = await this.readFactorData(block);
|
const factorData = await this.readFactorData(block);
|
||||||
|
|
||||||
// 进行加权计算
|
// 进行加权计算
|
||||||
const suitabilityIndex = this.calculateWeightedScore(factorData, factors, weights);
|
const suitabilityIndex = this.calculateWeightedScore(factorData, weights);
|
||||||
|
|
||||||
// 生成详细分析结果
|
// 生成详细分析结果
|
||||||
const result = this.generateAnalysisResult(block, factorData, suitabilityIndex, weights);
|
const result = this.generateAnalysisResult(block, factorData, suitabilityIndex);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -52,7 +68,7 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async readFactorData(block: LandBlock): Promise<any> {
|
private async readFactorData(block: LandBlock): Promise<unknown> {
|
||||||
// 模拟从各种数据源读取因子数据
|
// 模拟从各种数据源读取因子数据
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
@@ -120,45 +136,46 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
return scoreMap[irrigationType] || 0.70;
|
return scoreMap[irrigationType] || 0.70;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateWeightedScore(factorData: any, factors: EvaluationFactor[], weights: any): number {
|
private calculateWeightedScore(factorData: unknown, weights: WeightConfig): number {
|
||||||
let totalScore = 0;
|
let totalScore = 0;
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
|
|
||||||
// 土壤因子得分计算
|
// 土壤因子得分计算
|
||||||
const soilWeight = weights.soil || 0.35;
|
const soilWeight = weights.soil || 0.35;
|
||||||
const soilScore = this.calculateSoilScore(factorData.soilFactors, factors);
|
const soilScore = this.calculateSoilScore((factorData as { soilFactors: unknown }).soilFactors);
|
||||||
totalScore += soilScore * soilWeight;
|
totalScore += soilScore * soilWeight;
|
||||||
totalWeight += soilWeight;
|
totalWeight += soilWeight;
|
||||||
|
|
||||||
// 气候因子得分计算
|
// 气候因子得分计算
|
||||||
const climateWeight = weights.climate || 0.30;
|
const climateWeight = weights.climate || 0.30;
|
||||||
const climateScore = this.calculateClimateScore(factorData.climateFactors, factors);
|
const climateScore = this.calculateClimateScore((factorData as { climateFactors: unknown }).climateFactors);
|
||||||
totalScore += climateScore * climateWeight;
|
totalScore += climateScore * climateWeight;
|
||||||
totalWeight += climateWeight;
|
totalWeight += climateWeight;
|
||||||
|
|
||||||
// 地形因子得分计算
|
// 地形因子得分计算
|
||||||
const topographyWeight = weights.topography || 0.20;
|
const topographyWeight = weights.topography || 0.20;
|
||||||
const topographyScore = this.calculateTopographyScore(factorData.topographyFactors, factors);
|
const topographyScore = this.calculateTopographyScore((factorData as { topographyFactors: unknown }).topographyFactors);
|
||||||
totalScore += topographyScore * topographyWeight;
|
totalScore += topographyScore * topographyWeight;
|
||||||
totalWeight += topographyWeight;
|
totalWeight += topographyWeight;
|
||||||
|
|
||||||
// 基础设施因子得分计算
|
// 基础设施因子得分计算
|
||||||
const infrastructureWeight = weights.infrastructure || 0.15;
|
const infrastructureWeight = weights.infrastructure || 0.15;
|
||||||
const infrastructureScore = this.calculateInfrastructureScore(factorData.infrastructureFactors, factors);
|
const infrastructureScore = this.calculateInfrastructureScore((factorData as { infrastructureFactors: unknown }).infrastructureFactors);
|
||||||
totalScore += infrastructureScore * infrastructureWeight;
|
totalScore += infrastructureScore * infrastructureWeight;
|
||||||
totalWeight += infrastructureWeight;
|
totalWeight += infrastructureWeight;
|
||||||
|
|
||||||
return Math.round(totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0);
|
return Math.round(totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateSoilScore(soilFactors: any, factors: EvaluationFactor[]): number {
|
private calculateSoilScore(soilFactors: unknown): number {
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
// pH值评分
|
// pH值评分
|
||||||
if (soilFactors.ph >= 6.0 && soilFactors.ph <= 7.5) {
|
const soilFactorsTyped = soilFactors as { ph: number; organicMatter: number; nitrogen: number; phosphorus: number; potassium: number };
|
||||||
|
if (soilFactorsTyped.ph >= 6.0 && soilFactorsTyped.ph <= 7.5) {
|
||||||
score += 90;
|
score += 90;
|
||||||
} else if (soilFactors.ph >= 5.5 && soilFactors.ph <= 8.0) {
|
} else if (soilFactorsTyped.ph >= 5.5 && soilFactorsTyped.ph <= 8.0) {
|
||||||
score += 70;
|
score += 70;
|
||||||
} else {
|
} else {
|
||||||
score += 40;
|
score += 40;
|
||||||
@@ -166,9 +183,9 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
count++;
|
count++;
|
||||||
|
|
||||||
// 有机质评分
|
// 有机质评分
|
||||||
if (soilFactors.organicMatter >= 2.5) {
|
if (soilFactorsTyped.organicMatter >= 2.5) {
|
||||||
score += 90;
|
score += 90;
|
||||||
} else if (soilFactors.organicMatter >= 1.5) {
|
} else if (soilFactorsTyped.organicMatter >= 1.5) {
|
||||||
score += 70;
|
score += 70;
|
||||||
} else {
|
} else {
|
||||||
score += 45;
|
score += 45;
|
||||||
@@ -176,21 +193,22 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
count++;
|
count++;
|
||||||
|
|
||||||
// 养分评分 (NPK)
|
// 养分评分 (NPK)
|
||||||
const npkScore = Math.min(100, (soilFactors.nitrogen + soilFactors.phosphorus + soilFactors.potassium) / 3);
|
const npkScore = Math.min(100, (soilFactorsTyped.nitrogen + soilFactorsTyped.phosphorus + soilFactorsTyped.potassium) / 3);
|
||||||
score += npkScore;
|
score += npkScore;
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
return count > 0 ? score / count : 50;
|
return count > 0 ? score / count : 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateClimateScore(climateFactors: any, factors: EvaluationFactor[]): number {
|
private calculateClimateScore(climateFactors: unknown): number {
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
// 温度适宜性
|
// 温度适宜性
|
||||||
if (climateFactors.temperature >= 15 && climateFactors.temperature <= 25) {
|
const climateFactorsTyped = climateFactors as { temperature: number; rainfall: number; sunlight: number };
|
||||||
|
if (climateFactorsTyped.temperature >= 15 && climateFactorsTyped.temperature <= 25) {
|
||||||
score += 90;
|
score += 90;
|
||||||
} else if (climateFactors.temperature >= 10 && climateFactors.temperature <= 30) {
|
} else if (climateFactorsTyped.temperature >= 10 && climateFactorsTyped.temperature <= 30) {
|
||||||
score += 70;
|
score += 70;
|
||||||
} else {
|
} else {
|
||||||
score += 50;
|
score += 50;
|
||||||
@@ -198,9 +216,9 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
count++;
|
count++;
|
||||||
|
|
||||||
// 降雨适宜性
|
// 降雨适宜性
|
||||||
if (climateFactors.rainfall >= 500 && climateFactors.rainfall <= 1000) {
|
if (climateFactorsTyped.rainfall >= 500 && climateFactorsTyped.rainfall <= 1000) {
|
||||||
score += 90;
|
score += 90;
|
||||||
} else if (climateFactors.rainfall >= 300 && climateFactors.rainfall <= 1500) {
|
} else if (climateFactorsTyped.rainfall >= 300 && climateFactorsTyped.rainfall <= 1500) {
|
||||||
score += 70;
|
score += 70;
|
||||||
} else {
|
} else {
|
||||||
score += 45;
|
score += 45;
|
||||||
@@ -208,9 +226,9 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
count++;
|
count++;
|
||||||
|
|
||||||
// 日照充足性
|
// 日照充足性
|
||||||
if (climateFactors.sunlight >= 2000) {
|
if (climateFactorsTyped.sunlight >= 2000) {
|
||||||
score += 90;
|
score += 90;
|
||||||
} else if (climateFactors.sunlight >= 1600) {
|
} else if (climateFactorsTyped.sunlight >= 1600) {
|
||||||
score += 75;
|
score += 75;
|
||||||
} else {
|
} else {
|
||||||
score += 60;
|
score += 60;
|
||||||
@@ -220,16 +238,17 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
return count > 0 ? score / count : 50;
|
return count > 0 ? score / count : 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateTopographyScore(topographyFactors: any, factors: EvaluationFactor[]): number {
|
private calculateTopographyScore(topographyFactors: unknown): number {
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
// 坡度评分
|
// 坡度评分
|
||||||
if (topographyFactors.slope <= 3) {
|
const topographyFactorsTyped = topographyFactors as { slope: number; elevation: number; drainageIndex: number };
|
||||||
|
if (topographyFactorsTyped.slope <= 3) {
|
||||||
score += 95;
|
score += 95;
|
||||||
} else if (topographyFactors.slope <= 6) {
|
} else if (topographyFactorsTyped.slope <= 6) {
|
||||||
score += 80;
|
score += 80;
|
||||||
} else if (topographyFactors.slope <= 15) {
|
} else if (topographyFactorsTyped.slope <= 15) {
|
||||||
score += 60;
|
score += 60;
|
||||||
} else {
|
} else {
|
||||||
score += 30;
|
score += 30;
|
||||||
@@ -237,11 +256,11 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
count++;
|
count++;
|
||||||
|
|
||||||
// 海拔适宜性
|
// 海拔适宜性
|
||||||
if (topographyFactors.elevation <= 100) {
|
if (topographyFactorsTyped.elevation <= 100) {
|
||||||
score += 90;
|
score += 90;
|
||||||
} else if (topographyFactors.elevation <= 300) {
|
} else if (topographyFactorsTyped.elevation <= 300) {
|
||||||
score += 80;
|
score += 80;
|
||||||
} else if (topographyFactors.elevation <= 600) {
|
} else if (topographyFactorsTyped.elevation <= 600) {
|
||||||
score += 65;
|
score += 65;
|
||||||
} else {
|
} else {
|
||||||
score += 45;
|
score += 45;
|
||||||
@@ -249,39 +268,51 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
count++;
|
count++;
|
||||||
|
|
||||||
// 排水条件
|
// 排水条件
|
||||||
score += topographyFactors.drainageIndex * 100;
|
score += topographyFactorsTyped.drainageIndex * 100;
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
return count > 0 ? score / count : 50;
|
return count > 0 ? score / count : 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateInfrastructureScore(infrastructureFactors: any, factors: EvaluationFactor[]): number {
|
private calculateInfrastructureScore(infrastructureFactors: unknown): number {
|
||||||
let score = 0;
|
let score = 0;
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
// 灌溉条件
|
// 灌溉条件
|
||||||
score += infrastructureFactors.irrigationScore * 100;
|
const infrastructureFactorsTyped = infrastructureFactors as {
|
||||||
|
irrigationScore: number;
|
||||||
|
accessibility: number;
|
||||||
|
powerSupply: boolean;
|
||||||
|
waterSupply: boolean
|
||||||
|
};
|
||||||
|
score += infrastructureFactorsTyped.irrigationScore * 100;
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
// 交通便利性
|
// 交通便利性
|
||||||
score += infrastructureFactors.accessibility * 100;
|
score += infrastructureFactorsTyped.accessibility * 100;
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
// 基础设施完善度
|
// 基础设施完善度
|
||||||
const infrastructureScore =
|
const infrastructureScore =
|
||||||
(infrastructureFactors.powerSupply ? 50 : 0) +
|
(infrastructureFactorsTyped.powerSupply ? 50 : 0) +
|
||||||
(infrastructureFactors.waterSupply ? 50 : 0);
|
(infrastructureFactorsTyped.waterSupply ? 50 : 0);
|
||||||
score += infrastructureScore;
|
score += infrastructureScore;
|
||||||
count++;
|
count++;
|
||||||
|
|
||||||
return count > 0 ? score / count : 50;
|
return count > 0 ? score / count : 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateAnalysisResult(block: LandBlock, factorData: any, suitabilityIndex: number, weights: any): AnalysisResult {
|
private generateAnalysisResult(block: LandBlock, factorData: unknown, suitabilityIndex: number): AnalysisResult {
|
||||||
const soilScore = this.calculateSoilScore(factorData.soilFactors, []);
|
const factorDataTyped = factorData as {
|
||||||
const climateScore = this.calculateClimateScore(factorData.climateFactors, []);
|
soilFactors: unknown;
|
||||||
const topographyScore = this.calculateTopographyScore(factorData.topographyFactors, []);
|
climateFactors: unknown;
|
||||||
const infrastructureScore = this.calculateInfrastructureScore(factorData.infrastructureFactors, []);
|
topographyFactors: unknown;
|
||||||
|
infrastructureFactors: unknown;
|
||||||
|
};
|
||||||
|
const soilScore = this.calculateSoilScore(factorDataTyped.soilFactors);
|
||||||
|
const climateScore = this.calculateClimateScore(factorDataTyped.climateFactors);
|
||||||
|
const topographyScore = this.calculateTopographyScore(factorDataTyped.topographyFactors);
|
||||||
|
const infrastructureScore = this.calculateInfrastructureScore(factorDataTyped.infrastructureFactors);
|
||||||
|
|
||||||
// 生成作物推荐
|
// 生成作物推荐
|
||||||
const recommendedCrops = this.generateCropRecommendations(block, factorData, suitabilityIndex);
|
const recommendedCrops = this.generateCropRecommendations(block, factorData, suitabilityIndex);
|
||||||
@@ -308,12 +339,12 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateCropRecommendations(block: LandBlock, factorData: any, suitabilityIndex: number): any[] {
|
private generateCropRecommendations(block: LandBlock, factorData: unknown, suitabilityIndex: number): unknown[] {
|
||||||
// 简化的作物推荐逻辑
|
// 简化的作物推荐逻辑
|
||||||
const crops = [
|
const crops = [
|
||||||
{ name: '小麦', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 10 - 5) },
|
{ name: '小麦', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 10 - 5) },
|
||||||
{ name: '玉米', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 15 - 5) },
|
{ name: '玉米', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 15 - 5) },
|
||||||
{ name: '水稻', suitabilityScore: factorData.climateFactors.rainfall > 800 ? Math.min(95, suitabilityIndex + Math.random() * 10) : Math.max(40, suitabilityIndex - 20) },
|
{ name: '水稻', suitabilityScore: (factorData as { climateFactors: { rainfall: number } }).climateFactors.rainfall > 800 ? Math.min(95, suitabilityIndex + Math.random() * 10) : Math.max(40, suitabilityIndex - 20) },
|
||||||
{ name: '大豆', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 8 - 4) },
|
{ name: '大豆', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 8 - 4) },
|
||||||
{ name: '棉花', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 12 - 6) }
|
{ name: '棉花', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 12 - 6) }
|
||||||
];
|
];
|
||||||
@@ -339,37 +370,44 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateRiskFactors(block: LandBlock, factorData: any, suitabilityIndex: number): string[] {
|
private generateRiskFactors(block: LandBlock, factorData: unknown, suitabilityIndex: number): string[] {
|
||||||
const risks = [];
|
const risks = [];
|
||||||
|
|
||||||
if (suitabilityIndex < 40) {
|
if (suitabilityIndex < 40) {
|
||||||
risks.push('综合条件较差,种植风险较高');
|
risks.push('综合条件较差,种植风险较高');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factorData.soilFactors.ph < 6.0 || factorData.soilFactors.ph > 7.5) {
|
const factorDataTyped = factorData as {
|
||||||
|
soilFactors: { ph: number; organicMatter: number };
|
||||||
|
topographyFactors: { slope: number };
|
||||||
|
infrastructureFactors: { irrigationScore: number };
|
||||||
|
climateFactors: { rainfall: number }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (factorDataTyped.soilFactors.ph < 6.0 || factorDataTyped.soilFactors.ph > 7.5) {
|
||||||
risks.push('土壤pH值偏离理想范围');
|
risks.push('土壤pH值偏离理想范围');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factorData.soilFactors.organicMatter < 1.5) {
|
if (factorDataTyped.soilFactors.organicMatter < 1.5) {
|
||||||
risks.push('土壤有机质含量偏低');
|
risks.push('土壤有机质含量偏低');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factorData.topographyFactors.slope > 8) {
|
if (factorDataTyped.topographyFactors.slope > 8) {
|
||||||
risks.push('坡度较大,存在水土流失风险');
|
risks.push('坡度较大,存在水土流失风险');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factorData.infrastructureFactors.irrigationScore < 0.5) {
|
if (factorDataTyped.infrastructureFactors.irrigationScore < 0.5) {
|
||||||
risks.push('灌溉条件不足,依赖自然降水');
|
risks.push('灌溉条件不足,依赖自然降水');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factorData.climateFactors.rainfall < 400) {
|
if (factorDataTyped.climateFactors.rainfall < 400) {
|
||||||
risks.push('降雨量偏少,干旱风险较高');
|
risks.push('降雨量偏少,干旱风险较高');
|
||||||
}
|
}
|
||||||
|
|
||||||
return risks.length > 0 ? risks : ['无明显风险因素'];
|
return risks.length > 0 ? risks : ['无明显风险因素'];
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateImprovementSuggestions(block: LandBlock, factorData: any, soilScore: number, climateScore: number, topographyScore: number, infrastructureScore: number): string[] {
|
private generateImprovementSuggestions(block: LandBlock, factorData: unknown, soilScore: number, climateScore: number, topographyScore: number, infrastructureScore: number): string[] {
|
||||||
const suggestions = [];
|
const suggestions = [];
|
||||||
|
|
||||||
if (soilScore < 70) {
|
if (soilScore < 70) {
|
||||||
@@ -392,7 +430,7 @@ class MockSpatialAnalysisService implements SpatialAnalysisService {
|
|||||||
suggestions.push('完善灌溉和排水系统');
|
suggestions.push('完善灌溉和排水系统');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (factorData.infrastructureFactors.accessibility < 0.6) {
|
if ((factorData as { infrastructureFactors: { accessibility: number } }).infrastructureFactors.accessibility < 0.6) {
|
||||||
suggestions.push('改善田间道路条件');
|
suggestions.push('改善田间道路条件');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +453,7 @@ export class BatchAnalysisManager {
|
|||||||
async startBatchAnalysis(
|
async startBatchAnalysis(
|
||||||
taskName: string,
|
taskName: string,
|
||||||
factors: EvaluationFactor[],
|
factors: EvaluationFactor[],
|
||||||
weightConfig: any
|
weightConfig: WeightConfig
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.isRunning) {
|
if (this.isRunning) {
|
||||||
throw new Error('已有分析任务正在运行');
|
throw new Error('已有分析任务正在运行');
|
||||||
@@ -455,9 +493,6 @@ export class BatchAnalysisManager {
|
|||||||
|
|
||||||
// 循环处理所有地块
|
// 循环处理所有地块
|
||||||
const results: AnalysisResult[] = [];
|
const results: AnalysisResult[] = [];
|
||||||
let highSuitability = 0;
|
|
||||||
let mediumSuitability = 0;
|
|
||||||
let lowSuitability = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < totalFields; i++) {
|
for (let i = 0; i < totalFields; i++) {
|
||||||
if (!this.isRunning) break; // 检查是否被取消
|
if (!this.isRunning) break; // 检查是否被取消
|
||||||
@@ -501,16 +536,6 @@ export class BatchAnalysisManager {
|
|||||||
|
|
||||||
if (analysisResult.success && analysisResult.result) {
|
if (analysisResult.success && analysisResult.result) {
|
||||||
results.push(analysisResult.result);
|
results.push(analysisResult.result);
|
||||||
|
|
||||||
// 统计适宜性等级
|
|
||||||
const score = analysisResult.suitabilityIndex;
|
|
||||||
if (score >= 80) {
|
|
||||||
highSuitability++;
|
|
||||||
} else if (score >= 60) {
|
|
||||||
mediumSuitability++;
|
|
||||||
} else {
|
|
||||||
lowSuitability++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新任务统计
|
// 更新任务统计
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import {
|
import {
|
||||||
Database,
|
Database,
|
||||||
Play,
|
|
||||||
Download,
|
Download,
|
||||||
Eye,
|
Eye,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
@@ -68,7 +67,7 @@ export default function BatchEvaluationPage() {
|
|||||||
const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0);
|
const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0);
|
||||||
|
|
||||||
// 模拟从空间分析服务读取地块因子数据
|
// 模拟从空间分析服务读取地块因子数据
|
||||||
const fetchFieldFactorsFromSpatialService = (fieldId: string): EvaluationFactor[] => {
|
const fetchFieldFactorsFromSpatialService = (): EvaluationFactor[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'ph',
|
id: 'ph',
|
||||||
@@ -214,7 +213,7 @@ export default function BatchEvaluationPage() {
|
|||||||
const fieldId = `field-${i + 1}`;
|
const fieldId = `field-${i + 1}`;
|
||||||
const fieldName = `地块${String.fromCharCode(65 + (i % 26))}${Math.floor(i / 26) + 1}`;
|
const fieldName = `地块${String.fromCharCode(65 + (i % 26))}${Math.floor(i / 26) + 1}`;
|
||||||
|
|
||||||
const factors = fetchFieldFactorsFromSpatialService(fieldId);
|
const factors = fetchFieldFactorsFromSpatialService();
|
||||||
const scoredFactors = factors.map(factor => ({
|
const scoredFactors = factors.map(factor => ({
|
||||||
...factor,
|
...factor,
|
||||||
score: calculateFactorScore(factor.value, factor.optimalRange)
|
score: calculateFactorScore(factor.value, factor.optimalRange)
|
||||||
|
|||||||
@@ -5,59 +5,40 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import {
|
import {
|
||||||
Leaf,
|
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Award,
|
Award,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Play,
|
Play,
|
||||||
Settings,
|
Settings,
|
||||||
Download,
|
|
||||||
Eye,
|
|
||||||
Calculator,
|
|
||||||
Database,
|
|
||||||
RefreshCw,
|
|
||||||
Zap,
|
Zap,
|
||||||
Target,
|
|
||||||
Droplet,
|
|
||||||
Cloud,
|
Cloud,
|
||||||
Sun,
|
|
||||||
ThermometerSun,
|
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Beaker,
|
Beaker
|
||||||
Info,
|
|
||||||
BarChart3,
|
|
||||||
Filter
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EvaluationFactor,
|
|
||||||
SuitabilityResult,
|
SuitabilityResult,
|
||||||
FactorWeight,
|
FactorWeight,
|
||||||
BatchProgress,
|
|
||||||
getGradeColor,
|
getGradeColor,
|
||||||
getScoreColor,
|
getScoreColor,
|
||||||
getSuitabilityLevelColor,
|
formatDate
|
||||||
formatDate,
|
|
||||||
MOCK_FIELDS
|
|
||||||
} from './multiFactorTypes';
|
} from './multiFactorTypes';
|
||||||
import {
|
import {
|
||||||
MultiFactorService
|
MultiFactorService
|
||||||
} from './multiFactorService';
|
} from './multiFactorService';
|
||||||
import {
|
import {
|
||||||
matchCropsForField,
|
|
||||||
cropKnowledgeBase
|
cropKnowledgeBase
|
||||||
} from './cropKnowledgeBase';
|
} from './cropKnowledgeBase';
|
||||||
|
|
||||||
@@ -65,15 +46,6 @@ export function MultiFactorEvaluation() {
|
|||||||
const [selectedField, setSelectedField] = useState('field-1');
|
const [selectedField, setSelectedField] = useState('field-1');
|
||||||
const [showWeightConfig, setShowWeightConfig] = useState(false);
|
const [showWeightConfig, setShowWeightConfig] = useState(false);
|
||||||
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
|
const [showKnowledgeBase, setShowKnowledgeBase] = useState(false);
|
||||||
const [batchProgress, setBatchProgress] = useState<BatchProgress>({
|
|
||||||
total: 0,
|
|
||||||
processed: 0,
|
|
||||||
highSuitability: 0,
|
|
||||||
mediumSuitability: 0,
|
|
||||||
lowSuitability: 0,
|
|
||||||
currentField: '',
|
|
||||||
});
|
|
||||||
const [isBatchRunning, setIsBatchRunning] = useState(false);
|
|
||||||
const [batchAnalysisResults, setBatchAnalysisResults] = useState<SuitabilityResult[]>([]);
|
const [batchAnalysisResults, setBatchAnalysisResults] = useState<SuitabilityResult[]>([]);
|
||||||
|
|
||||||
// 评价因子权重配置
|
// 评价因子权重配置
|
||||||
@@ -99,43 +71,6 @@ export function MultiFactorEvaluation() {
|
|||||||
evaluationResults[0];
|
evaluationResults[0];
|
||||||
|
|
||||||
// 批量分析处理函数
|
// 批量分析处理函数
|
||||||
const handleRunBatchAnalysis = async () => {
|
|
||||||
const validation = MultiFactorService.validateWeights(factorWeights);
|
|
||||||
if (!validation.isValid) {
|
|
||||||
toast.error(`权重总和必须为100%才能进行批量分析(当前:${validation.totalWeight}%)`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsBatchRunning(true);
|
|
||||||
setBatchProgress({
|
|
||||||
total: 68,
|
|
||||||
processed: 0,
|
|
||||||
highSuitability: 0,
|
|
||||||
mediumSuitability: 0,
|
|
||||||
lowSuitability: 0,
|
|
||||||
currentField: '',
|
|
||||||
});
|
|
||||||
setBatchAnalysisResults([]);
|
|
||||||
|
|
||||||
toast.success('开始批量分析,正在读取地块数据...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const results = await MultiFactorService.runBatchAnalysis(
|
|
||||||
factorWeights,
|
|
||||||
(progress) => {
|
|
||||||
setBatchProgress(progress);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
setBatchAnalysisResults(results);
|
|
||||||
setIsBatchRunning(false);
|
|
||||||
toast.success(`批量分析完成!已为${results.length}个地块生成适宜性评价结果`);
|
|
||||||
} catch (error) {
|
|
||||||
setIsBatchRunning(false);
|
|
||||||
toast.error('批量分析失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateWeight = (id: string, newWeight: number) => {
|
const handleUpdateWeight = (id: string, newWeight: number) => {
|
||||||
setFactorWeights(prev =>
|
setFactorWeights(prev =>
|
||||||
MultiFactorService.updateWeight(prev, id, newWeight)
|
MultiFactorService.updateWeight(prev, id, newWeight)
|
||||||
@@ -174,15 +109,6 @@ export function MultiFactorEvaluation() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const exportResults = () => {
|
|
||||||
const resultsToExport = batchAnalysisResults.length > 0 ? batchAnalysisResults : evaluationResults;
|
|
||||||
toast.success('正在导出评价结果...');
|
|
||||||
// 模拟导出功能
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.success(`已导出${resultsToExport.length}个地块的评价结果`);
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* 提供作物-环境适配数据和分析功能
|
* 提供作物-环境适配数据和分析功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Crop, CropRecommendation, FieldFactors, MatchDetail, RiskFactor } from './multiFactorTypes';
|
import { Crop, CropRecommendation, FieldFactors, MatchDetail } from './multiFactorTypes';
|
||||||
|
|
||||||
// 作物知识库数据
|
// 作物知识库数据
|
||||||
export const cropKnowledgeBase: Crop[] = [
|
export const cropKnowledgeBase: Crop[] = [
|
||||||
|
|||||||
@@ -3,10 +3,68 @@
|
|||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Leaf, AlertTriangle, ThermometerSun, Cloud, Sun } from 'lucide-react';
|
import { Leaf, AlertTriangle, ThermometerSun, Cloud, Sun } from 'lucide-react';
|
||||||
import { CropRecommendationState, SuitabilityResult } from './cropRecommendReducer';
|
import { SuitabilityResult } from './cropRecommendReducer';
|
||||||
|
|
||||||
|
type RangeRequirement = { optimal: [number, number]; acceptable: [number, number]; };
|
||||||
|
type SoilFactorKey = 'ph' | 'organicMatter' | 'soilDepth' | 'nitrogen' | 'phosphorus' | 'potassium' | 'drainage';
|
||||||
|
type SoilRequirementMap = Record<SoilFactorKey, RangeRequirement>;
|
||||||
|
|
||||||
|
type ClimateRequirement = {
|
||||||
|
temperature: RangeRequirement;
|
||||||
|
rainfall: RangeRequirement;
|
||||||
|
sunlight: RangeRequirement;
|
||||||
|
};
|
||||||
|
|
||||||
|
type YieldRange = {
|
||||||
|
high: [number, number];
|
||||||
|
medium: [number, number];
|
||||||
|
low: [number, number];
|
||||||
|
};
|
||||||
|
|
||||||
|
type RiskSeverity = 'low' | 'medium' | 'high';
|
||||||
|
|
||||||
|
interface CropRiskFactor {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
condition: string;
|
||||||
|
severity: RiskSeverity;
|
||||||
|
suggestion: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CropKnowledgeEntry {
|
||||||
|
id: string;
|
||||||
|
cropName: string;
|
||||||
|
category: string;
|
||||||
|
description: string;
|
||||||
|
growthCycle: { days: number; seasons: string[] };
|
||||||
|
soilRequirements: SoilRequirementMap;
|
||||||
|
climateRequirements: ClimateRequirement;
|
||||||
|
expectedYield: YieldRange;
|
||||||
|
riskFactors: CropRiskFactor[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchStatus = '??' | '???' | '??';
|
||||||
|
|
||||||
|
interface MatchDetail {
|
||||||
|
factor: string;
|
||||||
|
value: number;
|
||||||
|
score: number;
|
||||||
|
status: MatchStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecommendationResult {
|
||||||
|
crop: CropKnowledgeEntry;
|
||||||
|
matchScore: number;
|
||||||
|
suitabilityLevel: '????' | '??' | '????' | '???';
|
||||||
|
matchDetails: MatchDetail[];
|
||||||
|
applicableRisks: CropRiskFactor[];
|
||||||
|
expectedYield: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldFactors = Record<SoilFactorKey | 'temperature' | 'rainfall', number>;
|
||||||
|
|
||||||
// 模拟作物知识库数据
|
// 模拟作物知识库数据
|
||||||
const cropKnowledgeBase = [
|
const cropKnowledgeBase: CropKnowledgeEntry[] = [
|
||||||
{
|
{
|
||||||
id: 'wheat',
|
id: 'wheat',
|
||||||
cropName: '小麦',
|
cropName: '小麦',
|
||||||
@@ -40,14 +98,14 @@ const cropKnowledgeBase = [
|
|||||||
id: 'wheat-rust',
|
id: 'wheat-rust',
|
||||||
name: '锈病风险',
|
name: '锈病风险',
|
||||||
condition: '湿度过高、温度适宜',
|
condition: '湿度过高、温度适宜',
|
||||||
severity: 'medium' as const,
|
severity: 'medium',
|
||||||
suggestion: '选择抗病品种,合理密植,及时防治'
|
suggestion: '选择抗病品种,合理密植,及时防治'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'wheat-drought',
|
id: 'wheat-drought',
|
||||||
name: '干旱风险',
|
name: '干旱风险',
|
||||||
condition: '降雨量不足400mm',
|
condition: '降雨量不足400mm',
|
||||||
severity: 'high' as const,
|
severity: 'high',
|
||||||
suggestion: '加强灌溉设施建设,选择抗旱品种'
|
suggestion: '加强灌溉设施建设,选择抗旱品种'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -85,14 +143,14 @@ const cropKnowledgeBase = [
|
|||||||
id: 'corn-borer',
|
id: 'corn-borer',
|
||||||
name: '玉米螟',
|
name: '玉米螟',
|
||||||
condition: '温度适宜、湿度适中',
|
condition: '温度适宜、湿度适中',
|
||||||
severity: 'medium' as const,
|
severity: 'medium',
|
||||||
suggestion: '生物防治与化学防治结合,适时播种'
|
suggestion: '生物防治与化学防治结合,适时播种'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'corn-drought',
|
id: 'corn-drought',
|
||||||
name: '花期干旱',
|
name: '花期干旱',
|
||||||
condition: '开花期降雨不足',
|
condition: '开花期降雨不足',
|
||||||
severity: 'high' as const,
|
severity: 'high',
|
||||||
suggestion: '保证花期灌溉,选择耐旱品种'
|
suggestion: '保证花期灌溉,选择耐旱品种'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -130,7 +188,7 @@ const cropKnowledgeBase = [
|
|||||||
id: 'soybean-disease',
|
id: 'soybean-disease',
|
||||||
name: '病害风险',
|
name: '病害风险',
|
||||||
condition: '高温高湿环境',
|
condition: '高温高湿环境',
|
||||||
severity: 'medium' as const,
|
severity: 'medium',
|
||||||
suggestion: '选择抗病品种,合理轮作,加强田间管理'
|
suggestion: '选择抗病品种,合理轮作,加强田间管理'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -138,11 +196,10 @@ const cropKnowledgeBase = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
interface CropRecommendationsProps {
|
interface CropRecommendationsProps {
|
||||||
state: CropRecommendationState;
|
|
||||||
currentResult: SuitabilityResult;
|
currentResult: SuitabilityResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CropRecommendations({ state, currentResult }: CropRecommendationsProps) {
|
export function CropRecommendations({ currentResult }: CropRecommendationsProps) {
|
||||||
// 匹配作物推荐
|
// 匹配作物推荐
|
||||||
const matchCropsForField = (fieldFactors: any) => {
|
const matchCropsForField = (fieldFactors: any) => {
|
||||||
return cropKnowledgeBase.map(crop => {
|
return cropKnowledgeBase.map(crop => {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useReducer } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
export interface EvaluationFactor {
|
export interface EvaluationFactor {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
|
|||||||
import { BookOpen, Target } from 'lucide-react';
|
import { BookOpen, Target } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
cropRecommendReducer,
|
cropRecommendReducer,
|
||||||
initialState,
|
initialState
|
||||||
SuitabilityResult
|
|
||||||
} from './components/cropRecommendReducer';
|
} from './components/cropRecommendReducer';
|
||||||
import { FieldEnvironmentOverview } from './components/FieldEnvironmentOverview';
|
import { FieldEnvironmentOverview } from './components/FieldEnvironmentOverview';
|
||||||
import { CropRecommendations } from './components/CropRecommendations';
|
import { CropRecommendations } from './components/CropRecommendations';
|
||||||
@@ -117,7 +116,7 @@ export default function CropPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 智能作物推荐 */}
|
{/* 智能作物推荐 */}
|
||||||
<CropRecommendations state={state} currentResult={currentResult} />
|
<CropRecommendations currentResult={currentResult} />
|
||||||
|
|
||||||
{/* 知识库对话框 */}
|
{/* 知识库对话框 */}
|
||||||
<KnowledgeBaseDialog
|
<KnowledgeBaseDialog
|
||||||
|
|||||||
@@ -72,11 +72,6 @@
|
|||||||
"src",
|
"src",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.node.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
|
|||||||
Reference in New Issue
Block a user