From 8936b898e2876aa88c74ab801f56dfa82b96ab05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=BE=B7=E8=BE=89?= Date: Fri, 27 Jun 2025 14:46:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E4=BA=86=E6=A8=A1?= =?UTF-8?q?=E7=89=B9=E7=9A=84=E7=BB=B4=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + bun.lock | 28 + package.json | 8 +- postcss.config.js | 4 +- src/App.tsx | 8 +- src/api/index.ts | 4 - ...g_material_api_img_upload_material_post.ts | 9 - src/api/models/ScenarioItem.ts | 9 - src/api/models/index.ts | 2 - ...g_material_api_img_upload_material_post.ts | 17 - src/api/schemas/$ScenarioItem.ts | 16 - src/api/services/Service.ts | 804 ++++++------------ src/components/block/confirm-dialog.tsx | 145 ++++ src/components/model/ModelForm.tsx | 87 -- src/components/ui/button.tsx | 55 +- src/components/ui/form.tsx | 165 ++++ src/components/ui/select.tsx | 183 ++++ src/{api => }/hooks/useApi.ts | 2 +- src/{api => lib}/api-client.ts | 30 +- src/lib/api.ts | 21 + src/pages/ModelPage.tsx | 116 --- src/pages/ModelPage/components/ModelForm.tsx | 326 +++++++ src/pages/ModelPage/index.tsx | 111 +++ tailwind.config.ts | 16 - 24 files changed, 1289 insertions(+), 879 deletions(-) delete mode 100644 src/api/models/Body_upload_img_material_api_img_upload_material_post.ts delete mode 100644 src/api/models/ScenarioItem.ts delete mode 100644 src/api/schemas/$Body_upload_img_material_api_img_upload_material_post.ts delete mode 100644 src/api/schemas/$ScenarioItem.ts create mode 100644 src/components/block/confirm-dialog.tsx delete mode 100644 src/components/model/ModelForm.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/select.tsx rename src/{api => }/hooks/useApi.ts (94%) rename src/{api => lib}/api-client.ts (76%) create mode 100644 src/lib/api.ts delete mode 100644 src/pages/ModelPage.tsx create mode 100644 src/pages/ModelPage/components/ModelForm.tsx create mode 100644 src/pages/ModelPage/index.tsx delete mode 100644 tailwind.config.ts diff --git a/.gitignore b/.gitignore index a547bf3..3b0b403 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env \ No newline at end of file diff --git a/bun.lock b/bun.lock index 2eff08b..a2bcef7 100644 --- a/bun.lock +++ b/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=="], diff --git a/package.json b/package.json index c7e28f9..bb98200 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/postcss.config.js b/postcss.config.js index dc3819c..9df712d 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ export default { plugins: { - tailwindcss: {}, + '@tailwindcss/postcss': {}, autoprefixer: {}, }, -} \ No newline at end of file +}; diff --git a/src/App.tsx b/src/App.tsx index ca06103..71ec34a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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: }, @@ -36,6 +37,7 @@ function App() { } /> + ); diff --git a/src/api/index.ts b/src/api/index.ts index 3daae6f..9881aa2 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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'; diff --git a/src/api/models/Body_upload_img_material_api_img_upload_material_post.ts b/src/api/models/Body_upload_img_material_api_img_upload_material_post.ts deleted file mode 100644 index 9342d13..0000000 --- a/src/api/models/Body_upload_img_material_api_img_upload_material_post.ts +++ /dev/null @@ -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; -}; - diff --git a/src/api/models/ScenarioItem.ts b/src/api/models/ScenarioItem.ts deleted file mode 100644 index aad2c08..0000000 --- a/src/api/models/ScenarioItem.ts +++ /dev/null @@ -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; -}; - diff --git a/src/api/models/index.ts b/src/api/models/index.ts index 8966ce0..2ce3bff 100644 --- a/src/api/models/index.ts +++ b/src/api/models/index.ts @@ -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'; diff --git a/src/api/schemas/$Body_upload_img_material_api_img_upload_material_post.ts b/src/api/schemas/$Body_upload_img_material_api_img_upload_material_post.ts deleted file mode 100644 index df89ceb..0000000 --- a/src/api/schemas/$Body_upload_img_material_api_img_upload_material_post.ts +++ /dev/null @@ -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; diff --git a/src/api/schemas/$ScenarioItem.ts b/src/api/schemas/$ScenarioItem.ts deleted file mode 100644 index 348f7e9..0000000 --- a/src/api/schemas/$ScenarioItem.ts +++ /dev/null @@ -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; diff --git a/src/api/services/Service.ts b/src/api/services/Service.ts index a718d2d..6475901 100644 --- a/src/api/services/Service.ts +++ b/src/api/services/Service.ts @@ -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 { 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 { + public static getModelsListApiModelsListGet({ + status, + }: { + /** + * 状态筛选,1启用,0禁用 + */ + status?: (number | null), + }): CancelablePromise { 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 { + 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 { + 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 | null), - }): CancelablePromise { - 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, - }): CancelablePromise { - 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 { - 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, + }): CancelablePromise { + 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 { - 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 | null), - }): CancelablePromise { - 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, - }): CancelablePromise { - 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 { - 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 { - 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 { - 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 { - 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, - }): CancelablePromise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { + 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 { + return __request(OpenAPI, { + method: 'GET', + url: '/api/tag/style/tag_list', + }); + } + /** + * 获取模特标签列表 + * @returns any Successful Response + * @throws ApiError + */ + public static fetchModelTagsApiTagModelTagListGet(): CancelablePromise { + 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 { + 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 { + 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 { + return __request(OpenAPI, { + method: 'GET', + url: '/api/video/query/{task_id}', + path: { + 'task_id': taskId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + } diff --git a/src/components/block/confirm-dialog.tsx b/src/components/block/confirm-dialog.tsx new file mode 100644 index 0000000..e083a1a --- /dev/null +++ b/src/components/block/confirm-dialog.tsx @@ -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; + 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()( + 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 ( + !isSubmitting && setOpen(value)}> + +
{content}
+ + {operations ? ( + operations({ setOpen }) + ) : ( + <> + + {buttonText?.confirm && ( + + )} + + )} + +
+
+ ); +}; + +ConfirmDialog.displayName = 'ConfirmDialog'; + +function confirm(props: { + content: React.ReactNode | ((props: { form?: UseFormReturn }) => React.ReactNode); + className?: string; + buttonText?: { cancel?: string; confirm?: string; loading?: '处理中' }; + operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[]; + onConfirm?: (formData?: F) => R | Promise; + form?: UseFormReturn; +}) { + 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(props: { + content: React.ReactNode | ((props: { form?: UseFormReturn }) => React.ReactNode); + buttonText?: { cancel?: string; confirm?: string }; + operations?: (props: { setOpen: (value: boolean) => void }) => React.ReactNode[]; + onConfirm?: (formData?: F) => R | Promise; + form?: UseFormReturn; +}) { + const promise = new Promise(resolve => { + confirm({ + 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; diff --git a/src/components/model/ModelForm.tsx b/src/components/model/ModelForm.tsx deleted file mode 100644 index 469c4f6..0000000 --- a/src/components/model/ModelForm.tsx +++ /dev/null @@ -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 = ({ initialData, onSubmit, onCancel, editMode }) => { - const [tag, setTag] = useState(initialData?.tag || defaultTag); - const [cover, setCover] = useState(null); - const [status, setStatus] = useState(initialData?.status ?? 1); - const [loading, setLoading] = useState(false); - - const handleChange = (e: React.ChangeEvent) => { - setTag({ ...tag, [e.target.name]: e.target.value }); - }; - - const handleFileChange = (e: React.ChangeEvent) => { - 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 ( -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - setStatus(v ? 1 : 0)} /> -
-
-
- - -
-
- ); -}; - -export default ModelForm; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index a2df8dc..edf72f3 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -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 & { - asChild?: boolean + asChild?: boolean; }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button'; - return ( - - ) + return ; } -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..7d7474c --- /dev/null +++ b/src/components/ui/form.tsx @@ -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 = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +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 ") + } + + 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( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +