feat: 增加了模特的维护
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
28
bun.lock
28
bun.lock
@@ -4,12 +4,15 @@
|
||||
"": {
|
||||
"name": "web",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"axios": "^1.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -17,10 +20,13 @@
|
||||
"lucide-react": "^0.523.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.25.67",
|
||||
"zustand": "^5.0.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.29.0",
|
||||
@@ -42,6 +48,8 @@
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@10.1.0", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.11", "@types/lodash.clonedeep": "^4.5.7", "js-yaml": "^4.1.0", "lodash.clonedeep": "^4.5.0" } }, "sha512-3e+viyMuXdrcK8v5pvP+SDoAQ77FH6OyRmuK48SZKmdHJRFm87RsSs8qm6kP39a/pOPURByJw+OXzQIqcfmKtA=="],
|
||||
@@ -126,6 +134,8 @@
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
|
||||
|
||||
"@hookform/resolvers": ["@hookform/resolvers@5.1.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
@@ -154,16 +164,22 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||
|
||||
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="],
|
||||
|
||||
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="],
|
||||
|
||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
|
||||
@@ -182,6 +198,8 @@
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.5", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA=="],
|
||||
|
||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
@@ -258,6 +276,8 @@
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="],
|
||||
|
||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.12.7", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.12.7", "@swc/core-darwin-x64": "1.12.7", "@swc/core-linux-arm-gnueabihf": "1.12.7", "@swc/core-linux-arm64-gnu": "1.12.7", "@swc/core-linux-arm64-musl": "1.12.7", "@swc/core-linux-x64-gnu": "1.12.7", "@swc/core-linux-x64-musl": "1.12.7", "@swc/core-win32-arm64-msvc": "1.12.7", "@swc/core-win32-ia32-msvc": "1.12.7", "@swc/core-win32-x64-msvc": "1.12.7" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-bcpllEihyUSnqp0UtXTvXc19CT4wp3tGWLENhWnjr4B5iEOkzqMu+xHGz1FI5IBatjfqOQb29tgIfv6IL05QaA=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.12.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w6BBT0hBRS56yS+LbReVym0h+iB7/PpCddqrn1ha94ra4rZ4R/A91A/rkv+LnQlPqU/+fhqdlXtCJU9mrhCBtA=="],
|
||||
@@ -312,6 +332,8 @@
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.11", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "postcss": "^8.4.41", "tailwindcss": "4.1.11" } }, "sha512-q/EAIIpF6WpLhKEuQSEVMZNMIY8KhWoAemZ9eylNAih9jxMGAYPPWBn3I9QL/2jZ+e7OEz/tZkX5HwbBR4HohA=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
@@ -650,6 +672,8 @@
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
|
||||
"react-hook-form": ["react-hook-form@7.58.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
@@ -746,6 +770,10 @@
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@3.25.67", "", {}, "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw=="],
|
||||
|
||||
"zustand": ["zustand@5.0.6", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
"generate-api:prod": "OPENAPI_URL=https://your-api-domain.com/openapi.json node scripts/generate-api.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"axios": "^1.6.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -26,10 +29,13 @@
|
||||
"lucide-react": "^0.523.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.25.67",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.29.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||
import ModelPage from './pages/ModelPage';
|
||||
import { SidebarProvider, Sidebar, SidebarContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton } from '@/components/ui/sidebar';
|
||||
import { ConfirmDialog } from '@/components/block/confirm-dialog';
|
||||
import { Sidebar, SidebarContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from '@/components/ui/sidebar';
|
||||
import { Home, Users } from 'lucide-react';
|
||||
import { Link, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import ModelPage from './pages/ModelPage';
|
||||
|
||||
const menu = [
|
||||
{ path: '/', label: '首页', icon: <Home className='mr-2' /> },
|
||||
@@ -36,6 +37,7 @@ function App() {
|
||||
<Route path='/models' element={<ModelPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<ConfirmDialog />
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
@@ -23,10 +23,8 @@ export type { Body_opt_local_generate_image_api_local_batch_generate_image_lates
|
||||
export type { Body_sync_custom_edit_image_api_sync_custom_image_post } from './models/Body_sync_custom_edit_image_api_sync_custom_image_post';
|
||||
export type { Body_update_model_api_models__model_id__put } from './models/Body_update_model_api_models__model_id__put';
|
||||
export type { Body_upload_image_api_upload_post } from './models/Body_upload_image_api_upload_post';
|
||||
export type { Body_upload_img_material_api_img_upload_material_post } from './models/Body_upload_img_material_api_img_upload_material_post';
|
||||
export type { HTTPValidationError } from './models/HTTPValidationError';
|
||||
export type { ImageEditItem } from './models/ImageEditItem';
|
||||
export type { ScenarioItem } from './models/ScenarioItem';
|
||||
export type { TaskQueryRequest } from './models/TaskQueryRequest';
|
||||
export type { UploadResponse } from './models/UploadResponse';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
@@ -50,10 +48,8 @@ export { $Body_opt_local_generate_image_api_local_batch_generate_image_latest_po
|
||||
export { $Body_sync_custom_edit_image_api_sync_custom_image_post } from './schemas/$Body_sync_custom_edit_image_api_sync_custom_image_post';
|
||||
export { $Body_update_model_api_models__model_id__put } from './schemas/$Body_update_model_api_models__model_id__put';
|
||||
export { $Body_upload_image_api_upload_post } from './schemas/$Body_upload_image_api_upload_post';
|
||||
export { $Body_upload_img_material_api_img_upload_material_post } from './schemas/$Body_upload_img_material_api_img_upload_material_post';
|
||||
export { $HTTPValidationError } from './schemas/$HTTPValidationError';
|
||||
export { $ImageEditItem } from './schemas/$ImageEditItem';
|
||||
export { $ScenarioItem } from './schemas/$ScenarioItem';
|
||||
export { $TaskQueryRequest } from './schemas/$TaskQueryRequest';
|
||||
export { $UploadResponse } from './schemas/$UploadResponse';
|
||||
export { $ValidationError } from './schemas/$ValidationError';
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_upload_img_material_api_img_upload_material_post = {
|
||||
category: string;
|
||||
file: Blob;
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ScenarioItem = {
|
||||
category: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
@@ -14,10 +14,8 @@ export * from './Body_opt_local_generate_image_api_local_batch_generate_image_la
|
||||
export * from './Body_sync_custom_edit_image_api_sync_custom_image_post';
|
||||
export * from './Body_update_model_api_models__model_id__put';
|
||||
export * from './Body_upload_image_api_upload_post';
|
||||
export * from './Body_upload_img_material_api_img_upload_material_post';
|
||||
export * from './HTTPValidationError';
|
||||
export * from './ImageEditItem';
|
||||
export * from './ScenarioItem';
|
||||
export * from './TaskQueryRequest';
|
||||
export * from './UploadResponse';
|
||||
export * from './ValidationError';
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $Body_upload_img_material_api_img_upload_material_post = {
|
||||
properties: {
|
||||
category: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
file: {
|
||||
type: 'binary',
|
||||
isRequired: true,
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -1,16 +0,0 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export const $ScenarioItem = {
|
||||
properties: {
|
||||
category: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
isRequired: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -6,8 +6,6 @@ import type { BatchVideoGenerateRequest } from '../models/BatchVideoGenerateRequ
|
||||
import type { BatchVideoTaskRequest } from '../models/BatchVideoTaskRequest';
|
||||
import type { Body_create_model_api_models_create_post } from '../models/Body_create_model_api_models_create_post';
|
||||
import type { Body_update_model_api_models__model_id__put } from '../models/Body_update_model_api_models__model_id__put';
|
||||
import type { Body_upload_img_material_api_img_upload_material_post } from '../models/Body_upload_img_material_api_img_upload_material_post';
|
||||
import type { ScenarioItem } from '../models/ScenarioItem';
|
||||
import type { VideoGenerateRequest } from '../models/VideoGenerateRequest';
|
||||
import type { VideoTaskStatus } from '../models/VideoTaskStatus';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
@@ -92,33 +90,39 @@ export class Service {
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 上传素材
|
||||
* 上传图片素材
|
||||
* 创建模型
|
||||
* 创建模型
|
||||
*
|
||||
* Args:
|
||||
* category: 素材所属的类别, 例如, women man
|
||||
* file: 上传的图片文件
|
||||
* tags: 模型标签,格式:性别_类别_尺寸_材质_颜色
|
||||
* cover_image: 封面图片文件
|
||||
* status: 状态,默认为1
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含上传结果的响应
|
||||
* dict: 包含创建结果的响应
|
||||
* {
|
||||
* 'status': 上传的状态,
|
||||
* 'status': 创建状态,
|
||||
* 'data': {
|
||||
* 'id': 素材id,
|
||||
* 'img_url': '素材列表'
|
||||
* }
|
||||
* 'id': 模型ID,
|
||||
* 'tag': {'sex': '性别', 'category': '类别', 'size': '尺寸', 'material': '材质', 'color': '颜色'},
|
||||
* 'cover_url': '封面URL',
|
||||
* 'status': 状态,
|
||||
* 'create_time': '创建时间'
|
||||
* },
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static uploadImgMaterialApiImgUploadMaterialPost({
|
||||
public static createModelApiModelsCreatePost({
|
||||
formData,
|
||||
}: {
|
||||
formData: Body_upload_img_material_api_img_upload_material_post,
|
||||
formData: Body_create_model_api_models_create_post,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/img/upload/material',
|
||||
url: '/api/models/create',
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
@@ -127,554 +131,272 @@ export class Service {
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取模特素材分类列表
|
||||
* 获取模特素材分类列表
|
||||
* 获取模型列表
|
||||
* 获取模型列表
|
||||
*
|
||||
* Args:
|
||||
* status: 状态筛选,1启用,0禁用,为空时获取所有
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含分类列表的响应
|
||||
* dict: 包含模型列表的响应
|
||||
* {
|
||||
* 'status': 查询状态,
|
||||
* 'data': ['category1', 'category2', ...],
|
||||
* 'data': [模型信息列表],
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getImgCategoryListApiImgCategoriesGet(): CancelablePromise<any> {
|
||||
public static getModelsListApiModelsListGet({
|
||||
status,
|
||||
}: {
|
||||
/**
|
||||
* 状态筛选,1启用,0禁用
|
||||
*/
|
||||
status?: (number | null),
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/img/categories',
|
||||
url: '/api/models/list',
|
||||
query: {
|
||||
'status': status,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取某个类别下模特素材列表,类别为空获取所有
|
||||
* 获取某个类别下的模特素材列表
|
||||
* 更新模型
|
||||
* 更新模型信息
|
||||
*
|
||||
* Args:
|
||||
* category: 素材类别列表,支持多个类别,为空时获取所有模特素材
|
||||
* 例如: ?category=women&category=men&category=kids
|
||||
* model_id: 模型ID
|
||||
* tags: 更新的标签,格式:性别_类别_尺寸_材质_颜色(可选)
|
||||
* cover_image: 更新的封面图片文件(可选)
|
||||
* status: 更新的状态(可选)
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含素材列表的响应
|
||||
* dict: 包含更新结果的响应
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static updateModelApiModelsModelIdPut({
|
||||
modelId,
|
||||
formData,
|
||||
}: {
|
||||
modelId: number,
|
||||
formData?: Body_update_model_api_models__model_id__put,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
url: '/api/models/{model_id}',
|
||||
path: {
|
||||
'model_id': modelId,
|
||||
},
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 删除单个模型
|
||||
* 删除单个模型(软删除)
|
||||
*
|
||||
* Args:
|
||||
* model_id: 模型ID
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 删除结果
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteModelApiModelsModelIdDelete({
|
||||
modelId,
|
||||
}: {
|
||||
modelId: number,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/api/models/{model_id}',
|
||||
path: {
|
||||
'model_id': modelId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 删除模型,支持批量
|
||||
* 批量删除模型(软删除)
|
||||
*
|
||||
* Args:
|
||||
* model_ids: 模型ID列表
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 删除结果
|
||||
* {
|
||||
* 'status': 查询状态,
|
||||
* 'data': ['图片URL1', '图片URL2', ...],
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getModelListApiImgModelListGet({
|
||||
category,
|
||||
}: {
|
||||
category?: (Array<string> | null),
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/img/model/list',
|
||||
query: {
|
||||
'category': category,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 删除图片,支持批量
|
||||
* 批量删除图片(软删除)
|
||||
*
|
||||
* Args:
|
||||
* img_url: 图片URL列表
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 删除结果
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static removeImageApiImgRemovePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: Array<string>,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/img/remove',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 添加场景信息
|
||||
* 添加场景信息
|
||||
*
|
||||
* Args:
|
||||
* data: 场景数据,包含category和description
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含添加结果的响应
|
||||
* {
|
||||
* 'status': 添加状态,
|
||||
* 'data': {
|
||||
* 'id': 场景ID,
|
||||
* 'category': '类别',
|
||||
* 'description': '描述'
|
||||
* },
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static addScenarioInfoApiScenarioAddPost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: ScenarioItem,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/scenario/add',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
* 'status': 删除状态,
|
||||
* 'data': {
|
||||
* 'total': 总数,
|
||||
* 'success': 成功数,
|
||||
* 'failed': 失败数
|
||||
* },
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static removeModelsApiModelsRemovePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: Array<number>,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/models/remove',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 根据标签筛选模型列表
|
||||
* 根据标签筛选模型列表
|
||||
* 支持格式:性别_类别_尺寸_材质_颜色,使用特殊匹配规则
|
||||
*
|
||||
* 匹配规则:
|
||||
* - sex(性别)和 category(类别):等值匹配(必须完全相同,AND逻辑)
|
||||
* - size(尺寸)、material(材质)、color(颜色):非等值匹配(所有字段都必须不同,AND逻辑)
|
||||
*
|
||||
* Args:
|
||||
* tags: 标签字符串,格式:性别_类别_尺寸_材质_颜色
|
||||
* status: 状态筛选,默认为1
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含筛选结果的响应
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getModelsByTagApiModelsByTagsGet({
|
||||
tags,
|
||||
status,
|
||||
}: {
|
||||
/**
|
||||
* 获取场景信息列表
|
||||
* 获取场景信息列表
|
||||
*
|
||||
* Args:
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含场景分类列表的响应
|
||||
* {
|
||||
* 'status': 查询状态,
|
||||
* 'data': ['类别1', '类别2', ...],
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getScenarioListApiScenarioListGet(): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/scenario/list',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取场景详情列表
|
||||
* 获取场景详情列表
|
||||
*
|
||||
* Args:
|
||||
* category: 场景类别列表,支持多个类别,为空时获取所有场景详情
|
||||
* 例如: ?category=indoor&category=outdoor&category=studio
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含场景描述列表的响应
|
||||
* {
|
||||
* 'status': 查询状态,
|
||||
* 'data': ['描述1', '描述2', ...],
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getScenarioDetailListApiScenarioDetailListGet({
|
||||
category,
|
||||
}: {
|
||||
category?: (Array<string> | null),
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/scenario/detail/list',
|
||||
query: {
|
||||
'category': category,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 删除场景,支持批量
|
||||
* 批量删除场景(软删除)
|
||||
*
|
||||
* Args:
|
||||
* data_list: 场景值列表
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 删除结果
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static removeScenarioApiScenarioRemovePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: Array<string>,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/scenario/remove',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 创建模型
|
||||
* 创建模型
|
||||
*
|
||||
* Args:
|
||||
* tags: 模型标签,格式:性别_类别_尺寸_材质_颜色
|
||||
* cover_image: 封面图片文件
|
||||
* status: 状态,默认为1
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含创建结果的响应
|
||||
* {
|
||||
* 'status': 创建状态,
|
||||
* 'data': {
|
||||
* 'id': 模型ID,
|
||||
* 'tag': {'sex': '性别', 'category': '类别', 'size': '尺寸', 'material': '材质', 'color': '颜色'},
|
||||
* 'cover_url': '封面URL',
|
||||
* 'status': 状态,
|
||||
* 'create_time': '创建时间'
|
||||
* },
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createModelApiModelsCreatePost({
|
||||
formData,
|
||||
}: {
|
||||
formData: Body_create_model_api_models_create_post,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/models/create',
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取模型列表
|
||||
* 获取模型列表
|
||||
*
|
||||
* Args:
|
||||
* status: 状态筛选,1启用,0禁用,为空时获取所有
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含模型列表的响应
|
||||
* {
|
||||
* 'status': 查询状态,
|
||||
* 'data': [模型信息列表],
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getModelsListApiModelsListGet({
|
||||
status,
|
||||
}: {
|
||||
/**
|
||||
* 状态筛选,1启用,0禁用
|
||||
*/
|
||||
status?: (number | null),
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/models/list',
|
||||
query: {
|
||||
'status': status,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 更新模型
|
||||
* 更新模型信息
|
||||
*
|
||||
* Args:
|
||||
* model_id: 模型ID
|
||||
* tags: 更新的标签,格式:性别_类别_尺寸_材质_颜色(可选)
|
||||
* cover_image: 更新的封面图片文件(可选)
|
||||
* status: 更新的状态(可选)
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含更新结果的响应
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static updateModelApiModelsModelIdPut({
|
||||
modelId,
|
||||
formData,
|
||||
}: {
|
||||
modelId: number,
|
||||
formData?: Body_update_model_api_models__model_id__put,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
url: '/api/models/{model_id}',
|
||||
path: {
|
||||
'model_id': modelId,
|
||||
},
|
||||
formData: formData,
|
||||
mediaType: 'multipart/form-data',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 删除单个模型
|
||||
* 删除单个模型(软删除)
|
||||
*
|
||||
* Args:
|
||||
* model_id: 模型ID
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 删除结果
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteModelApiModelsModelIdDelete({
|
||||
modelId,
|
||||
}: {
|
||||
modelId: number,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/api/models/{model_id}',
|
||||
path: {
|
||||
'model_id': modelId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 删除模型,支持批量
|
||||
* 批量删除模型(软删除)
|
||||
*
|
||||
* Args:
|
||||
* model_ids: 模型ID列表
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 删除结果
|
||||
* {
|
||||
* 'status': 删除状态,
|
||||
* 'data': {
|
||||
* 'total': 总数,
|
||||
* 'success': 成功数,
|
||||
* 'failed': 失败数
|
||||
* },
|
||||
* 'msg': '提示信息'
|
||||
* }
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static removeModelsApiModelsRemovePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: Array<number>,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/models/remove',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 根据标签筛选模型列表
|
||||
* 根据标签筛选模型列表
|
||||
* 支持格式:性别_类别_尺寸_材质_颜色,使用特殊匹配规则
|
||||
*
|
||||
* 匹配规则:
|
||||
* - sex(性别)和 category(类别):等值匹配(必须完全相同,AND逻辑)
|
||||
* - size(尺寸)、material(材质)、color(颜色):非等值匹配(所有字段都必须不同,AND逻辑)
|
||||
*
|
||||
* Args:
|
||||
* tags: 标签字符串,格式:性别_类别_尺寸_材质_颜色
|
||||
* status: 状态筛选,默认为1
|
||||
* db: 数据库会话
|
||||
*
|
||||
* Returns:
|
||||
* dict: 包含筛选结果的响应
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getModelsByTagApiModelsByTagsGet({
|
||||
tags,
|
||||
status,
|
||||
}: {
|
||||
/**
|
||||
* 标签,格式:性别_类别_尺寸_材质_颜色。例如: '女_上衣_长款_牛仔_红'
|
||||
*/
|
||||
tags: string,
|
||||
/**
|
||||
* 状态筛选,1启用,0禁用
|
||||
*/
|
||||
status?: (number | null),
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/models/by/tags',
|
||||
query: {
|
||||
'tags': tags,
|
||||
'status': status,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取风格标签结构(外层风格 + 内层场景)
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static fetchStyleAvailableTagsApiTagStyleTagListGet(): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/tag/style/tag_list',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取指定风格和多个场景的值列表
|
||||
* 获取指定风格和多个场景下的值列表(最内层数组合并)
|
||||
*
|
||||
* Args:
|
||||
* style: 风格名称,如:少女、马来
|
||||
* scenes: 场景名称,多个用逗号分隔,如:室内,室外
|
||||
*
|
||||
* Returns:
|
||||
* dict: 合并后的值列表
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static fetchStyleSceneValuesApiTagStyleValuesGet({
|
||||
style,
|
||||
scenes,
|
||||
}: {
|
||||
style: string,
|
||||
scenes: string,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/tag/style/values',
|
||||
query: {
|
||||
'style': style,
|
||||
'scenes': scenes,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取模特标签列表
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static fetchModelTagsApiTagModelTagListGet(): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/tag/model/tag_list',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 极梦视频生成任务提交
|
||||
* 生成视频API
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static generateVideoApiVideoGeneratePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: VideoGenerateRequest,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/video/generate',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 最大支持同时提交5个任务
|
||||
* 批量生成视频API - 并发处理,最多同时5个任务
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static batchGenerateVideoApiBatchVideoGeneratePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: BatchVideoGenerateRequest,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/batch/video/generate',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 极梦视频生成进度查询
|
||||
* 查询视频生成状态API
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static queryVideoStatusApiVideoQueryTaskIdGet({
|
||||
taskId,
|
||||
}: {
|
||||
taskId: string,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/video/query/{task_id}',
|
||||
path: {
|
||||
'task_id': taskId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
* 标签,格式:性别_类别_尺寸_材质_颜色。例如: '女_上衣_长款_牛仔_红'
|
||||
*/
|
||||
tags: string,
|
||||
/**
|
||||
* 状态筛选,1启用,0禁用
|
||||
*/
|
||||
status?: (number | null),
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/models/by/tags',
|
||||
query: {
|
||||
'tags': tags,
|
||||
'status': status,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取风格标签结构(外层风格 + 内层场景)
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static fetchStyleAvailableTagsApiTagStyleTagListGet(): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/tag/style/tag_list',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取模特标签列表
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static fetchModelTagsApiTagModelTagListGet(): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/tag/model/tag_list',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 极梦视频生成任务提交
|
||||
* 生成视频API
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static generateVideoApiVideoGeneratePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: VideoGenerateRequest,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/video/generate',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 最大支持同时提交5个任务
|
||||
* 批量生成视频API - 并发处理,最多同时5个任务
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static batchGenerateVideoApiBatchVideoGeneratePost({
|
||||
requestBody,
|
||||
}: {
|
||||
requestBody: BatchVideoGenerateRequest,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/batch/video/generate',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 极梦视频生成进度查询
|
||||
* 查询视频生成状态API
|
||||
* @returns any Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static queryVideoStatusApiVideoQueryTaskIdGet({
|
||||
taskId,
|
||||
}: {
|
||||
taskId: string,
|
||||
}): CancelablePromise<any> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/api/video/query/{task_id}',
|
||||
path: {
|
||||
'task_id': taskId,
|
||||
},
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
145
src/components/block/confirm-dialog.tsx
Normal file
145
src/components/block/confirm-dialog.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogFooter } from '@/components/ui/dialog';
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import type { FieldValues, UseFormReturn } from 'react-hook-form';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ConfirmDialogStore {
|
||||
open: boolean;
|
||||
content: React.ReactNode;
|
||||
className?: string;
|
||||
buttonText?: { cancel?: string; confirm?: string; loading?: string };
|
||||
operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[];
|
||||
onConfirm?: () => void | Promise<void>;
|
||||
setOpen: (value: boolean) => void;
|
||||
setContent: (content: React.ReactNode) => void;
|
||||
setOnConfirm: (onConfirm?: () => void) => void;
|
||||
setButtonText: (buttonText?: { cancel?: string; confirm?: string; loading?: string }) => void;
|
||||
setOperations: (operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[]) => void;
|
||||
setClassName: (className?: string) => void;
|
||||
}
|
||||
|
||||
const useConfirmDialogStore = create<ConfirmDialogStore>()(
|
||||
devtools(set => ({
|
||||
open: false,
|
||||
content: null,
|
||||
className: undefined,
|
||||
buttonText: { cancel: '取消', confirm: '', loading: '处理中' },
|
||||
operations: () => [],
|
||||
onConfirm: undefined,
|
||||
setOpen: (value: boolean) => set({ open: value }),
|
||||
setContent: content => set({ content }),
|
||||
setOnConfirm: onConfirm => set({ onConfirm }),
|
||||
setButtonText: buttonText => set({ buttonText }),
|
||||
setOperations: operations => set({ operations }),
|
||||
setClassName: className => set({ className }),
|
||||
}))
|
||||
);
|
||||
|
||||
export const ConfirmDialog = () => {
|
||||
const {
|
||||
open,
|
||||
content,
|
||||
buttonText = { cancel: '取消', confirm: '确认', loading: '处理中' },
|
||||
operations,
|
||||
onConfirm,
|
||||
setOpen,
|
||||
className,
|
||||
} = useConfirmDialogStore();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onConfirm?.();
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={value => !isSubmitting && setOpen(value)}>
|
||||
<DialogContent className={cn('w-[380px] pb-5 pl-6 pr-4 pt-8', className)}>
|
||||
<div>{content}</div>
|
||||
<DialogFooter>
|
||||
{operations ? (
|
||||
operations({ setOpen })
|
||||
) : (
|
||||
<>
|
||||
<Button className='w-24' variant='outline' onClick={() => setOpen(false)} disabled={isSubmitting}>
|
||||
{buttonText?.cancel || '取消'}
|
||||
</Button>
|
||||
{buttonText?.confirm && (
|
||||
<Button className='w-24' onClick={handleConfirm} disabled={isSubmitting}>
|
||||
{isSubmitting ? buttonText?.loading || '处理中' : buttonText?.confirm || '确定'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
ConfirmDialog.displayName = 'ConfirmDialog';
|
||||
|
||||
function confirm<F extends FieldValues = FieldValues, R = any>(props: {
|
||||
content: React.ReactNode | ((props: { form?: UseFormReturn<F> }) => React.ReactNode);
|
||||
className?: string;
|
||||
buttonText?: { cancel?: string; confirm?: string; loading?: '处理中' };
|
||||
operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[];
|
||||
onConfirm?: (formData?: F) => R | Promise<R>;
|
||||
form?: UseFormReturn<F>;
|
||||
}) {
|
||||
const { setOpen, setContent, setOnConfirm, setButtonText, setOperations, setClassName } = useConfirmDialogStore.getState();
|
||||
|
||||
const wrappedOnConfirm = async () => {
|
||||
if (props.form) {
|
||||
const formData = props.form.getValues();
|
||||
await props.onConfirm?.(formData);
|
||||
} else {
|
||||
await props.onConfirm?.();
|
||||
}
|
||||
};
|
||||
|
||||
setContent(typeof props.content === 'function' ? props.content({ form: props.form }) : props.content);
|
||||
setOnConfirm(wrappedOnConfirm);
|
||||
setOperations(props.operations);
|
||||
setButtonText(props.buttonText);
|
||||
setClassName(props.className);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
async function asyncConfirm<F extends FieldValues = FieldValues, R = any>(props: {
|
||||
content: React.ReactNode | ((props: { form?: UseFormReturn<F> }) => React.ReactNode);
|
||||
buttonText?: { cancel?: string; confirm?: string };
|
||||
operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[];
|
||||
onConfirm?: (formData?: F) => R | Promise<R>;
|
||||
form?: UseFormReturn<F>;
|
||||
}) {
|
||||
const promise = new Promise<R>(resolve => {
|
||||
confirm<F>({
|
||||
content: props.content,
|
||||
buttonText: props.buttonText,
|
||||
operations: props.operations,
|
||||
form: props.form,
|
||||
onConfirm: async (formData?: F) => {
|
||||
if (!props.onConfirm) {
|
||||
resolve(undefined as any);
|
||||
return;
|
||||
}
|
||||
const res = await props.onConfirm(formData as any);
|
||||
resolve(res);
|
||||
},
|
||||
});
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
ConfirmDialog.confirm = confirm;
|
||||
ConfirmDialog.asyncConfirm = asyncConfirm;
|
||||
@@ -1,87 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
|
||||
interface ModelFormProps {
|
||||
initialData?: any;
|
||||
onSubmit: (data: any, id?: number) => void;
|
||||
onCancel: () => void;
|
||||
editMode?: boolean;
|
||||
}
|
||||
|
||||
const defaultTag = { sex: '', category: '', size: '', material: '', color: '' };
|
||||
|
||||
const ModelForm: React.FC<ModelFormProps> = ({ initialData, onSubmit, onCancel, editMode }) => {
|
||||
const [tag, setTag] = useState(initialData?.tag || defaultTag);
|
||||
const [cover, setCover] = useState<File | null>(null);
|
||||
const [status, setStatus] = useState(initialData?.status ?? 1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTag({ ...tag, [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
setCover(e.target.files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
const formData = new FormData();
|
||||
formData.append('tags', Object.values(tag).join('_'));
|
||||
if (cover) formData.append('cover_image', cover);
|
||||
formData.append('status', String(status));
|
||||
setLoading(false);
|
||||
onSubmit(formData, initialData?.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className='space-y-4'>
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
<div>
|
||||
<Label htmlFor='sex'>性别</Label>
|
||||
<Input name='sex' value={tag.sex} onChange={handleChange} required />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='category'>类别</Label>
|
||||
<Input name='category' value={tag.category} onChange={handleChange} required />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='size'>尺寸</Label>
|
||||
<Input name='size' value={tag.size} onChange={handleChange} required />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='material'>材质</Label>
|
||||
<Input name='material' value={tag.material} onChange={handleChange} required />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='color'>颜色</Label>
|
||||
<Input name='color' value={tag.color} onChange={handleChange} required />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor='cover'>封面图片</Label>
|
||||
<Input type='file' name='cover' accept='image/*' onChange={handleFileChange} />
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Label htmlFor='status'>启用</Label>
|
||||
<Switch checked={status === 1} onCheckedChange={v => setStatus(v ? 1 : 0)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex justify-end space-x-2'>
|
||||
<Button type='button' variant='outline' onClick={onCancel} disabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type='submit' disabled={loading}>
|
||||
{loading ? '加载中...' : editMode ? '保存' : '创建'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelForm;
|
||||
@@ -1,39 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from 'react';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
@@ -41,19 +38,13 @@ function Button({
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return <Comp data-slot='button' className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
165
src/components/ui/form.tsx
Normal file
165
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState } = useFormContext()
|
||||
const formState = useFormState({ name: fieldContext.name })
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
className={cn("grid gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-error={!!error}
|
||||
className={cn("data-[error=true]:text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
id={formDescriptionId}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message ?? "") : props.children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
id={formMessageId}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
183
src/components/ui/select.tsx
Normal file
183
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { ApiClient } from '../api-client';
|
||||
import { ApiClient } from '@/lib/api-client';
|
||||
|
||||
// 创建API客户端实例
|
||||
const apiClient = new ApiClient({
|
||||
@@ -1,8 +1,4 @@
|
||||
import axios, {
|
||||
type AxiosInstance,
|
||||
type AxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
} from "axios";
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
|
||||
|
||||
export interface ApiClientConfig {
|
||||
BASE: string;
|
||||
@@ -21,13 +17,13 @@ export class ApiClient {
|
||||
baseURL: config.BASE,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
this.client.interceptors.request.use(
|
||||
(config) => {
|
||||
config => {
|
||||
// 添加认证token
|
||||
if (this.config.TOKEN) {
|
||||
const token = this.config.TOKEN();
|
||||
@@ -50,7 +46,7 @@ export class ApiClient {
|
||||
(error: any) => {
|
||||
// 统一错误处理
|
||||
if (error.response) {
|
||||
console.error("API错误:", error.response.data);
|
||||
console.error('API错误:', error.response.data);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -64,21 +60,13 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
// POST请求
|
||||
async post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.post<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
async put<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
async put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.put<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
@@ -90,11 +78,7 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
// PATCH请求
|
||||
async patch<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<T> {
|
||||
async patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.patch<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
21
src/lib/api.ts
Normal file
21
src/lib/api.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { OpenAPI } from '@/api/core/OpenAPI';
|
||||
import * as Service from '@/api/services/Service';
|
||||
import * as ComfyService from '@/api/services/ComfyService';
|
||||
import * as DefaultService from '@/api/services/DefaultService';
|
||||
import * as WebhookService from '@/api/services/WebhookService';
|
||||
|
||||
const baseURL = (() => {
|
||||
const url = import.meta.env.VITE_API_BASE_URL || '';
|
||||
return url;
|
||||
})();
|
||||
|
||||
OpenAPI.BASE = baseURL;
|
||||
|
||||
// 统一导出 api 实例(同步导入,保证 api 是同步对象)
|
||||
export const api = {
|
||||
...Service,
|
||||
...ComfyService,
|
||||
...DefaultService,
|
||||
...WebhookService,
|
||||
OpenAPI,
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Service } from '@/api/services/Service';
|
||||
import type { Body_create_model_api_models_create_post } from '@/api/models/Body_create_model_api_models_create_post';
|
||||
import type { Body_update_model_api_models__model_id__put } from '@/api/models/Body_update_model_api_models__model_id__put';
|
||||
import ModelForm from '@/components/model/ModelForm';
|
||||
|
||||
interface ModelItem {
|
||||
id: number;
|
||||
tag: Record<string, string>;
|
||||
cover_url: string;
|
||||
status: number;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
const ModelPage: React.FC = () => {
|
||||
const [models, setModels] = useState<ModelItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selected, setSelected] = useState<ModelItem | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
|
||||
const fetchModels = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await Service.getModelsListApiModelsListGet({});
|
||||
setModels(res.data || []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels();
|
||||
}, []);
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditMode(false);
|
||||
setSelected(null);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (item: ModelItem) => {
|
||||
setEditMode(true);
|
||||
setSelected(item);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await Service.deleteModelApiModelsModelIdDelete({ modelId: id });
|
||||
fetchModels();
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: Body_create_model_api_models_create_post | Body_update_model_api_models__model_id__put, id?: number) => {
|
||||
if (editMode && id) {
|
||||
await Service.updateModelApiModelsModelIdPut({ modelId: id, formData: data as Body_update_model_api_models__model_id__put });
|
||||
} else {
|
||||
await Service.createModelApiModelsCreatePost({ formData: data as Body_create_model_api_models_create_post });
|
||||
}
|
||||
setOpen(false);
|
||||
fetchModels();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h1 className="text-2xl font-bold">模特维护</h1>
|
||||
<Button onClick={handleAdd}>新建模特</Button>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>标签</TableHead>
|
||||
<TableHead>封面</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>创建时间</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{models.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.id}</TableCell>
|
||||
<TableCell>{Object.values(item.tag).join('_')}</TableCell>
|
||||
<TableCell>
|
||||
<img src={item.cover_url} alt="cover" className="w-16 h-16 object-cover rounded" />
|
||||
</TableCell>
|
||||
<TableCell>{item.status === 1 ? '启用' : '禁用'}</TableCell>
|
||||
<TableCell>{item.create_time}</TableCell>
|
||||
<TableCell>
|
||||
<Button size="sm" variant="outline" onClick={() => handleEdit(item)} className="mr-2">编辑</Button>
|
||||
<Button size="sm" variant="destructive" onClick={() => handleDelete(item.id)}>删除</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogTitle>{editMode ? '编辑模特' : '新建模特'}</DialogTitle>
|
||||
<ModelForm
|
||||
initialData={selected}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={() => setOpen(false)}
|
||||
editMode={editMode}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelPage;
|
||||
326
src/pages/ModelPage/components/ModelForm.tsx
Normal file
326
src/pages/ModelPage/components/ModelForm.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
|
||||
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from '@/components/ui/form';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
interface ModelFormProps {
|
||||
initialData?: any;
|
||||
onSubmit: (data: any, id?: number) => void;
|
||||
onCancel: () => void;
|
||||
editMode?: boolean;
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
sex: z.string().min(1, '请选择性别'),
|
||||
category: z.string().min(1, '请选择类别'),
|
||||
size: z.string().min(1, '请选择尺寸'),
|
||||
material: z.string().min(1, '请选择材质'),
|
||||
color: z.string().min(1, '请选择颜色'),
|
||||
cover: z.any().optional(),
|
||||
status: z.boolean(),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const defaultValues: FormValues = {
|
||||
sex: '',
|
||||
category: '',
|
||||
size: '',
|
||||
material: '',
|
||||
color: '',
|
||||
cover: undefined,
|
||||
status: true,
|
||||
};
|
||||
|
||||
const ModelForm: React.FC<ModelFormProps> = ({ initialData, onSubmit, onCancel, editMode }) => {
|
||||
const [tagOptions, setTagOptions] = React.useState<any[]>([]);
|
||||
const [coverPreview, setCoverPreview] = React.useState<string | null>(null);
|
||||
// 新增:用于触发文件选择的 ref
|
||||
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// 初始化表单
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: initialData
|
||||
? {
|
||||
sex: initialData.tag?.sex || '',
|
||||
category: initialData.tag?.category || '',
|
||||
size: initialData.tag?.size || '',
|
||||
material: initialData.tag?.material || '',
|
||||
color: initialData.tag?.color || '',
|
||||
cover: undefined,
|
||||
status: initialData.status === 1 ? true : false,
|
||||
}
|
||||
: defaultValues,
|
||||
});
|
||||
|
||||
// 支持初始图片预览
|
||||
React.useEffect(() => {
|
||||
if (initialData?.coverUrl) {
|
||||
setCoverPreview(initialData.coverUrl);
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
// 级联选择逻辑
|
||||
const sexOptions = tagOptions;
|
||||
const categoryOptions = useMemo(() => {
|
||||
return form.watch('sex') ? tagOptions.find((t: any) => t.title === form.watch('sex'))?.children || [] : [];
|
||||
}, [tagOptions, form.watch('sex')]);
|
||||
const sizeOptions = useMemo(() => {
|
||||
return form.watch('category') ? categoryOptions.find((t: any) => t.title === form.watch('category'))?.children || [] : [];
|
||||
}, [categoryOptions, form.watch('category')]);
|
||||
const materialOptions = useMemo(() => {
|
||||
if (!form.watch('size')) return [];
|
||||
const size = sizeOptions.find((t: any) => t.title === form.watch('size'));
|
||||
return size?.children?.find((c: any) => c.title === '材质')?.children || [];
|
||||
}, [sizeOptions, form.watch('size')]);
|
||||
const colorOptions = useMemo(() => {
|
||||
if (!form.watch('size')) return [];
|
||||
const size = sizeOptions.find((t: any) => t.title === form.watch('size'));
|
||||
return size?.children?.find((c: any) => c.title === '颜色')?.children || [];
|
||||
}, [sizeOptions, form.watch('size')]);
|
||||
|
||||
// 拉取标签数据
|
||||
useEffect(() => {
|
||||
api.Service.fetchModelTagsApiTagModelTagListGet().then(data => setTagOptions(data.data || []));
|
||||
}, []);
|
||||
|
||||
// 提交
|
||||
const handleFormSubmit = async (values: FormValues) => {
|
||||
const tagString = [values.sex, values.category, values.size, values.material, values.color].join('_');
|
||||
onSubmit({ tags: tagString, cover_image: values.cover, status: values.status ? 1 : 0 }, initialData?.id);
|
||||
};
|
||||
|
||||
// 级联联动清空下级
|
||||
const handleCascade = (level: keyof FormValues, value: string) => {
|
||||
if (level === 'sex') {
|
||||
form.setValue('sex', value);
|
||||
form.setValue('category', '');
|
||||
form.setValue('size', '');
|
||||
form.setValue('material', '');
|
||||
form.setValue('color', '');
|
||||
} else if (level === 'category') {
|
||||
form.setValue('category', value);
|
||||
form.setValue('size', '');
|
||||
form.setValue('material', '');
|
||||
form.setValue('color', '');
|
||||
} else if (level === 'size') {
|
||||
form.setValue('size', value);
|
||||
form.setValue('material', '');
|
||||
form.setValue('color', '');
|
||||
} else if (level === 'material') {
|
||||
form.setValue('material', value);
|
||||
} else if (level === 'color') {
|
||||
form.setValue('color', value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleFormSubmit)} className='flex flex-col md:flex-row gap-8 items-start'>
|
||||
{/* 左侧图片区,点击可上传 */}
|
||||
<div className='w-full md:w-[240px] flex-shrink-0 flex flex-col items-center'>
|
||||
<div
|
||||
className='w-full aspect-[9/16] bg-muted flex items-center justify-center rounded-lg border border-muted shadow-md overflow-hidden relative group cursor-pointer'
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
tabIndex={0}
|
||||
role='button'
|
||||
aria-label='上传封面图片'
|
||||
>
|
||||
{coverPreview ? (
|
||||
<img
|
||||
src={coverPreview}
|
||||
alt='预览'
|
||||
className='object-contain w-full h-full transition-transform duration-200 hover:scale-105 bg-white'
|
||||
style={{ background: '#f8f8f8' }}
|
||||
/>
|
||||
) : (
|
||||
<span className='text-muted-foreground text-sm'>暂无图片</span>
|
||||
)}
|
||||
{/* 遮罩/按钮 */}
|
||||
<div className='absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity pointer-events-none'>
|
||||
<span className='text-white text-base font-medium pointer-events-none'>点击上传封面</span>
|
||||
</div>
|
||||
{/* 隐藏的文件选择 */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type='file'
|
||||
accept='image/*'
|
||||
className='hidden'
|
||||
onChange={e => {
|
||||
const file = e.target.files?.[0];
|
||||
form.setValue('cover', file);
|
||||
if (file) {
|
||||
setCoverPreview(URL.createObjectURL(file));
|
||||
} else {
|
||||
setCoverPreview(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* 错误提示 */}
|
||||
<div className='mt-2 w-full'>
|
||||
<FormField control={form.control} name='cover' render={() => <FormMessage />} />
|
||||
</div>
|
||||
</div>
|
||||
{/* 右侧表单区(去除封面图片项) */}
|
||||
<div className='flex-1 w-full flex flex-col gap-4'>
|
||||
{/* 性别 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='sex'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>性别</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={v => handleCascade('sex', v)}>
|
||||
<SelectTrigger className='w-full'>
|
||||
<SelectValue placeholder='请选择性别' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sexOptions.map((item: any) => (
|
||||
<SelectItem key={item.title} value={item.title}>
|
||||
{item.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 类别 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='category'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>类别</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={v => handleCascade('category', v)} disabled={!form.watch('sex')}>
|
||||
<SelectTrigger className='w-full'>
|
||||
<SelectValue placeholder='请选择类别' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categoryOptions.map((item: any) => (
|
||||
<SelectItem key={item.title} value={item.title}>
|
||||
{item.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 尺寸 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='size'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>尺寸</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={v => handleCascade('size', v)} disabled={!form.watch('category')}>
|
||||
<SelectTrigger className='w-full'>
|
||||
<SelectValue placeholder='请选择尺寸' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sizeOptions.map((item: any) => (
|
||||
<SelectItem key={item.title} value={item.title}>
|
||||
{item.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 材质 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='material'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>材质</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={v => handleCascade('material', v)} disabled={!form.watch('size')}>
|
||||
<SelectTrigger className='w-full'>
|
||||
<SelectValue placeholder='请选择材质' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{materialOptions.map((item: any) => (
|
||||
<SelectItem key={item.title} value={item.title}>
|
||||
{item.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 颜色 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='color'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>颜色</FormLabel>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={v => handleCascade('color', v)} disabled={!form.watch('size')}>
|
||||
<SelectTrigger className='w-full'>
|
||||
<SelectValue placeholder='请选择颜色' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{colorOptions.map((item: any) => (
|
||||
<SelectItem key={item.title} value={item.title}>
|
||||
{item.title}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/* 启用 */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='status'
|
||||
render={({ field }) => (
|
||||
<FormItem className='flex items-center space-x-2'>
|
||||
<FormLabel htmlFor='status'>启用</FormLabel>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className='flex justify-end space-x-2 mt-6'>
|
||||
<Button type='button' variant='outline' onClick={onCancel} disabled={form.formState.isSubmitting}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type='submit' disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? '加载中...' : editMode ? '保存' : '创建'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelForm;
|
||||
111
src/pages/ModelPage/index.tsx
Normal file
111
src/pages/ModelPage/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
|
||||
import type { Body_create_model_api_models_create_post } from '@/api/models/Body_create_model_api_models_create_post';
|
||||
import type { Body_update_model_api_models__model_id__put } from '@/api/models/Body_update_model_api_models__model_id__put';
|
||||
import ModelForm from './components/ModelForm';
|
||||
import { api } from '@/lib/api';
|
||||
import { ConfirmDialog } from '@/components/block/confirm-dialog';
|
||||
|
||||
interface ModelItem {
|
||||
id: number;
|
||||
tag: Record<string, string>;
|
||||
cover_url: string;
|
||||
status: number;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
const ModelPage: React.FC = () => {
|
||||
const [models, setModels] = useState<ModelItem[]>([]);
|
||||
const [_, setLoading] = useState(false);
|
||||
const [selected, setSelected] = useState<ModelItem | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
|
||||
const fetchModels = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.Service.getModelsListApiModelsListGet({});
|
||||
setModels(res.data || []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchModels();
|
||||
}, []);
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditMode(false);
|
||||
setSelected(null);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (item: ModelItem) => {
|
||||
setEditMode(true);
|
||||
setSelected(item);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
ConfirmDialog.confirm({
|
||||
content: '确定要删除该模特吗?',
|
||||
onConfirm: async () => {
|
||||
await api.Service.deleteModelApiModelsModelIdDelete({ modelId: id });
|
||||
fetchModels();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: Body_create_model_api_models_create_post | Body_update_model_api_models__model_id__put, id?: number) => {
|
||||
console.log(data);
|
||||
if (editMode && id) {
|
||||
await api.Service.updateModelApiModelsModelIdPut({ modelId: id, formData: data as Body_update_model_api_models__model_id__put });
|
||||
} else {
|
||||
await api.Service.createModelApiModelsCreatePost({ formData: data as Body_create_model_api_models_create_post });
|
||||
}
|
||||
setOpen(false);
|
||||
fetchModels();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='p-6'>
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<h1 className='text-2xl font-bold'>模特维护</h1>
|
||||
<Button onClick={handleAdd}>新建模特</Button>
|
||||
</div>
|
||||
<div className='grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-4 gap-6'>
|
||||
{models.map(item => (
|
||||
<div key={item.id} className='bg-white dark:bg-zinc-900 rounded-lg shadow p-4 flex flex-col'>
|
||||
<img src={item.cover_url} alt='cover' className='w-full object-cover rounded mb-4 aspect-[9/16]' />
|
||||
<div className='flex-1 flex flex-col gap-2'>
|
||||
<div className='text-sm text-muted-foreground'>ID: {item.id}</div>
|
||||
<div className='text-sm truncate'>标签: {Object.values(item.tag).join('_')}</div>
|
||||
<div className='text-sm'>
|
||||
状态: <span className={item.status === 1 ? 'text-green-600' : 'text-red-500'}>{item.status === 1 ? '启用' : '禁用'}</span>
|
||||
</div>
|
||||
<div className='text-xs text-muted-foreground'>创建时间: {item.create_time}</div>
|
||||
</div>
|
||||
<div className='mt-4 flex gap-2'>
|
||||
<Button size='sm' variant='outline' onClick={() => handleEdit(item)} className='flex-1'>
|
||||
编辑
|
||||
</Button>
|
||||
<Button size='sm' variant='destructive' onClick={() => handleDelete(item.id)} className='flex-1'>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent style={{ width: '650px', maxWidth: '650px' }}>
|
||||
<DialogTitle>{editMode ? '编辑模特' : '新建模特'}</DialogTitle>
|
||||
<ModelForm initialData={selected} onSubmit={handleFormSubmit} onCancel={() => setOpen(false)} editMode={editMode} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelPage;
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
import animate from 'tailwindcss-animate';
|
||||
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: ['./index.html', './src/**/*.{ts,tsx,js,jsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
// 这里可以扩展自定义主题
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
animate,
|
||||
// 其它shadcn/ui推荐插件
|
||||
],
|
||||
} satisfies Config;
|
||||
Reference in New Issue
Block a user