commit ba0458abe28bd320ff9c8162baadbedb6b21260d Author: 魔方熊成祥 <3615096616@qq.com> Date: Wed Apr 29 21:38:18 2026 +0800 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d106780 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules +dist +.DS_Store +.idea +.vscode +*.local +*.tsbuildinfo +vite.config.js +vite.config.d.ts diff --git a/index.html b/index.html new file mode 100644 index 0000000..dbd7e49 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + 云拓商城商家端 + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..460c438 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "mall-merchant", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/icons": "^6.0.0", + "antd": "^5.19.3", + "axios": "^1.9.0", + "dayjs": "^1.11.13", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "7.9.6", + "sonner": "^2.0.6", + "zustand": "^5.0.6" + }, + "devDependencies": { + "@types/node": "^24.0.13", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react-swc": "^3.10.2", + "typescript": "~5.8.3", + "vite": "^7.0.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d83ca64 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2151 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ant-design/icons': + specifier: ^6.0.0 + version: 6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + antd: + specifier: ^5.19.3 + version: 5.29.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + axios: + specifier: ^1.9.0 + version: 1.15.2 + dayjs: + specifier: ^1.11.13 + version: 1.11.20 + react: + specifier: 18.2.0 + version: 18.2.0 + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + react-router-dom: + specifier: 7.9.6 + version: 7.9.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + sonner: + specifier: ^2.0.6 + version: 2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + zustand: + specifier: ^5.0.6 + version: 5.0.12(@types/react@18.3.28)(react@18.2.0) + devDependencies: + '@types/node': + specifier: ^24.0.13 + version: 24.12.2 + '@types/react': + specifier: ^18.2.48 + version: 18.3.28 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react-swc': + specifier: ^3.10.2 + version: 3.11.0(vite@7.3.2(@types/node@24.12.2)) + typescript: + specifier: ~5.8.3 + version: 5.8.3 + vite: + specifier: ^7.0.4 + version: 7.3.2(@types/node@24.12.2) + +packages: + + '@ant-design/colors@7.2.1': + resolution: {integrity: sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==} + + '@ant-design/colors@8.0.1': + resolution: {integrity: sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==} + + '@ant-design/cssinjs-utils@1.1.3': + resolution: {integrity: sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@ant-design/cssinjs@1.24.0': + resolution: {integrity: sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/fast-color@2.0.6': + resolution: {integrity: sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==} + engines: {node: '>=8.x'} + + '@ant-design/fast-color@3.0.1': + resolution: {integrity: sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==} + engines: {node: '>=8.x'} + + '@ant-design/icons-svg@4.4.2': + resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==} + + '@ant-design/icons@5.6.1': + resolution: {integrity: sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/icons@6.1.1': + resolution: {integrity: sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/react-slick@1.1.2': + resolution: {integrity: sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==} + peerDependencies: + react: '>=16.9.0' + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@emotion/hash@0.8.0': + resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==} + + '@emotion/unitless@0.7.5': + resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@rc-component/async-validator@5.1.0': + resolution: {integrity: sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==} + engines: {node: '>=14.x'} + + '@rc-component/color-picker@2.0.1': + resolution: {integrity: sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/context@1.4.0': + resolution: {integrity: sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/mini-decimal@1.1.3': + resolution: {integrity: sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==} + engines: {node: '>=8.x'} + + '@rc-component/mutate-observer@1.1.0': + resolution: {integrity: sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/portal@1.1.2': + resolution: {integrity: sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/qrcode@1.1.1': + resolution: {integrity: sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/tour@1.15.1': + resolution: {integrity: sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/trigger@2.3.1': + resolution: {integrity: sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + '@rc-component/util@1.10.1': + resolution: {integrity: sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.60.2': + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.2': + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.2': + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.2': + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.2': + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.2': + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.2': + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.2': + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.2': + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.2': + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.2': + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.2': + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.2': + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.2': + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} + cpu: [x64] + os: [win32] + + '@swc/core-darwin-arm64@1.15.30': + resolution: {integrity: sha512-VvpP+vq08HmGYewMWvrdsxh9s2lthz/808zXm8Yu5kaqeR8Yia2b0eYXleHQ3VAjoStUDk6LzTheBW9KXYQdMA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.30': + resolution: {integrity: sha512-WiJA0hiZI3nwQAO6mu5RqigtWGDtth4Hiq6rbZxAaQyhIcqKIg5IoMRc1Y071lrNJn29eEDMC86Rq58xgUxlDg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.30': + resolution: {integrity: sha512-YANuFUo48kIT6plJgCD0keae9HFXfjxsbvsgevqc0hr/07X/p7sAWTFOGYEc2SXcASaK7UvuQqzlbW8pr7R79g==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.30': + resolution: {integrity: sha512-VndG8jaR4ugY6u+iVOT0Q+d2fZd7sLgjPgN8W/Le+3EbZKl+cRfFxV7Eoz4gfLqhmneZPdcIzf9T3LkgkmqNLg==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.30': + resolution: {integrity: sha512-1SYGs2l0Yyyi0pR/P/NKz/x0kqxkoiw+BXeJjLUdecSk/KasncWlJrc6hOvFSgKHOBrzgM5jwuluKtlT8dnrcA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.15.30': + resolution: {integrity: sha512-TXREtiXeRhbfDFbmhnkIsXpKfzbfT73YkV2ZF6w0sfxgjC5zI2ZAbaCOq25qxvegofj2K93DtOpm9RLaBgqR2g==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.30': + resolution: {integrity: sha512-DCR2YYeyd6DQE4OuDhImouuNcjXEiEdnn1Y0DyGteugPEDvVuvYk8Xddi+4o2SgWH6jiW8/I+3emZvbep1NC+g==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.30': + resolution: {integrity: sha512-5Pizw3NgfOJ5BJOBK8TIRa59xFW2avESTOBDPTAYwZYa1JNDs+KMF9lUfjJiJLM5HiMs/wPheA9eiT0q9m2AoA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.30': + resolution: {integrity: sha512-qyqydP/wyH8alcIP4a2hnGSjHLJjm9H7yDFup+CPy9oTahFgLLwnNcv5UHXqO2Qs3AIND+cls5f/Bb6hqpxdgA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.30': + resolution: {integrity: sha512-CaQENgDHVGOg1mSF5sQVgvfFHG9kjMor2rkLMLeLOkfZYNj13ppnJ9+lfaBZLZUMMbnlGQnavCJb8PVBUOso7Q==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.30': + resolution: {integrity: sha512-30VdLeGk6fugiUs/kUdJ/pAg7z/zpvVbR11RH60jZ0Z42WIeIniYx0rLEWN7h/pKJ3CopqsQ3RsogCAkRKiA2g==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.30': + resolution: {integrity: sha512-4iObHPR+Q4oDY110EF5SF5eIaaVJNpMdG9C0q3Q92BsJ5y467uHz7sYQhP60WYlLFsLQ1el2YrIPUItUAQGOKg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.30': + resolution: {integrity: sha512-R8VQbQY1BZcbIF2p3gjlTCwAQzx1A194ugWfwld5y+WgVVWqVKm7eURGGOVbQVubgKWzidP2agomBbg96rZilQ==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.28': + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + + '@vitejs/plugin-react-swc@3.11.0': + resolution: {integrity: sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==} + peerDependencies: + vite: ^4 || ^5 || ^6 || ^7 + + antd@5.29.3: + resolution: {integrity: sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.15.2: + resolution: {integrity: sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + compute-scroll-into-view@3.1.1: + resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + is-mobile@5.0.0: + resolution: {integrity: sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + json2mq@0.2.0: + resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.10: + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} + engines: {node: ^10 || ^12 || >=14} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + rc-cascader@3.34.0: + resolution: {integrity: sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-checkbox@3.5.0: + resolution: {integrity: sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-collapse@3.9.0: + resolution: {integrity: sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-dialog@9.6.0: + resolution: {integrity: sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-drawer@7.3.0: + resolution: {integrity: sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-dropdown@4.2.1: + resolution: {integrity: sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==} + peerDependencies: + react: '>=16.11.0' + react-dom: '>=16.11.0' + + rc-field-form@2.7.1: + resolution: {integrity: sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-image@7.12.0: + resolution: {integrity: sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-input-number@9.5.0: + resolution: {integrity: sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-input@1.8.0: + resolution: {integrity: sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + rc-mentions@2.20.0: + resolution: {integrity: sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-menu@9.16.1: + resolution: {integrity: sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-motion@2.9.5: + resolution: {integrity: sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-notification@5.6.4: + resolution: {integrity: sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-overflow@1.5.0: + resolution: {integrity: sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-pagination@5.1.0: + resolution: {integrity: sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-picker@4.11.3: + resolution: {integrity: sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==} + engines: {node: '>=8.x'} + peerDependencies: + date-fns: '>= 2.x' + dayjs: '>= 1.x' + luxon: '>= 3.x' + moment: '>= 2.x' + react: '>=16.9.0' + react-dom: '>=16.9.0' + peerDependenciesMeta: + date-fns: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + + rc-progress@4.0.0: + resolution: {integrity: sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-rate@2.13.1: + resolution: {integrity: sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-resize-observer@1.4.3: + resolution: {integrity: sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-segmented@2.7.1: + resolution: {integrity: sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + rc-select@14.16.8: + resolution: {integrity: sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==} + engines: {node: '>=8.x'} + peerDependencies: + react: '*' + react-dom: '*' + + rc-slider@11.1.9: + resolution: {integrity: sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-steps@6.0.1: + resolution: {integrity: sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-switch@4.1.0: + resolution: {integrity: sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-table@7.54.0: + resolution: {integrity: sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tabs@15.7.0: + resolution: {integrity: sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-textarea@1.10.2: + resolution: {integrity: sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tooltip@6.4.0: + resolution: {integrity: sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-tree-select@5.27.0: + resolution: {integrity: sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==} + peerDependencies: + react: '*' + react-dom: '*' + + rc-tree@5.13.1: + resolution: {integrity: sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==} + engines: {node: '>=10.x'} + peerDependencies: + react: '*' + react-dom: '*' + + rc-upload@4.11.0: + resolution: {integrity: sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-util@5.44.4: + resolution: {integrity: sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + rc-virtual-list@3.19.2: + resolution: {integrity: sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==} + engines: {node: '>=8.x'} + peerDependencies: + react: '>=16.9.0' + react-dom: '>=16.9.0' + + react-dom@18.2.0: + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-router-dom@7.9.6: + resolution: {integrity: sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.9.6: + resolution: {integrity: sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + rollup@4.60.2: + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-convert@0.2.1: + resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + throttle-debounce@5.0.2: + resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} + engines: {node: '>=12.22'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + vite@7.3.2: + resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + +snapshots: + + '@ant-design/colors@7.2.1': + dependencies: + '@ant-design/fast-color': 2.0.6 + + '@ant-design/colors@8.0.1': + dependencies: + '@ant-design/fast-color': 3.0.1 + + '@ant-design/cssinjs-utils@1.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@ant-design/cssinjs': 1.24.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@babel/runtime': 7.29.2 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@ant-design/cssinjs@1.24.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/hash': 0.8.0 + '@emotion/unitless': 0.7.5 + classnames: 2.5.1 + csstype: 3.2.3 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + stylis: 4.4.0 + + '@ant-design/fast-color@2.0.6': + dependencies: + '@babel/runtime': 7.29.2 + + '@ant-design/fast-color@3.0.1': {} + + '@ant-design/icons-svg@4.4.2': {} + + '@ant-design/icons@5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@ant-design/colors': 7.2.1 + '@ant-design/icons-svg': 4.4.2 + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@ant-design/icons@6.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@ant-design/colors': 8.0.1 + '@ant-design/icons-svg': 4.4.2 + '@rc-component/util': 1.10.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + clsx: 2.1.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@ant-design/react-slick@1.1.2(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + json2mq: 0.2.0 + react: 18.2.0 + resize-observer-polyfill: 1.5.1 + throttle-debounce: 5.0.2 + + '@babel/runtime@7.29.2': {} + + '@emotion/hash@0.8.0': {} + + '@emotion/unitless@0.7.5': {} + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@rc-component/async-validator@5.1.0': + dependencies: + '@babel/runtime': 7.29.2 + + '@rc-component/color-picker@2.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@ant-design/fast-color': 2.0.6 + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@rc-component/context@1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@rc-component/mini-decimal@1.1.3': + dependencies: + '@babel/runtime': 7.29.2 + + '@rc-component/mutate-observer@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@rc-component/portal@1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@rc-component/qrcode@1.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@rc-component/tour@1.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/trigger': 2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@rc-component/trigger@2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + '@rc-component/util@1.10.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + is-mobile: 5.0.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.3.1 + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.60.2': + optional: true + + '@rollup/rollup-android-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-x64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.2': + optional: true + + '@swc/core-darwin-arm64@1.15.30': + optional: true + + '@swc/core-darwin-x64@1.15.30': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.30': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.30': + optional: true + + '@swc/core-linux-arm64-musl@1.15.30': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.30': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.30': + optional: true + + '@swc/core-linux-x64-gnu@1.15.30': + optional: true + + '@swc/core-linux-x64-musl@1.15.30': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.30': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.30': + optional: true + + '@swc/core-win32-x64-msvc@1.15.30': + optional: true + + '@swc/core@1.15.30': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.26 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.30 + '@swc/core-darwin-x64': 1.15.30 + '@swc/core-linux-arm-gnueabihf': 1.15.30 + '@swc/core-linux-arm64-gnu': 1.15.30 + '@swc/core-linux-arm64-musl': 1.15.30 + '@swc/core-linux-ppc64-gnu': 1.15.30 + '@swc/core-linux-s390x-gnu': 1.15.30 + '@swc/core-linux-x64-gnu': 1.15.30 + '@swc/core-linux-x64-musl': 1.15.30 + '@swc/core-win32-arm64-msvc': 1.15.30 + '@swc/core-win32-ia32-msvc': 1.15.30 + '@swc/core-win32-x64-msvc': 1.15.30 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.26': + dependencies: + '@swc/counter': 0.1.3 + + '@types/estree@1.0.8': {} + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.28)': + dependencies: + '@types/react': 18.3.28 + + '@types/react@18.3.28': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@vitejs/plugin-react-swc@3.11.0(vite@7.3.2(@types/node@24.12.2))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.27 + '@swc/core': 1.15.30 + vite: 7.3.2(@types/node@24.12.2) + transitivePeerDependencies: + - '@swc/helpers' + + antd@5.29.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@ant-design/colors': 7.2.1 + '@ant-design/cssinjs': 1.24.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@ant-design/cssinjs-utils': 1.1.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@ant-design/fast-color': 2.0.6 + '@ant-design/icons': 5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@ant-design/react-slick': 1.1.2(react@18.2.0) + '@babel/runtime': 7.29.2 + '@rc-component/color-picker': 2.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/mutate-observer': 1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/qrcode': 1.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/tour': 1.15.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@rc-component/trigger': 2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + copy-to-clipboard: 3.3.3 + dayjs: 1.11.20 + rc-cascader: 3.34.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-checkbox: 3.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-collapse: 3.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-dialog: 9.6.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-drawer: 7.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-dropdown: 4.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-field-form: 2.7.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-image: 7.12.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-input: 1.8.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-input-number: 9.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-mentions: 2.20.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.16.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-notification: 5.6.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-pagination: 5.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-picker: 4.11.3(dayjs@1.11.20)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-progress: 4.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-rate: 2.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-segmented: 2.7.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-select: 14.16.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-slider: 11.1.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-steps: 6.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-switch: 4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-table: 7.54.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tabs: 15.7.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-textarea: 1.10.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tooltip: 6.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree-select: 5.27.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-upload: 4.11.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + scroll-into-view-if-needed: 3.1.0 + throttle-debounce: 5.0.2 + transitivePeerDependencies: + - date-fns + - luxon + - moment + + asynckit@0.4.0: {} + + axios@1.15.2: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + classnames@2.5.1: {} + + clsx@2.1.1: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + compute-scroll-into-view@3.1.1: {} + + cookie@1.1.1: {} + + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + csstype@3.2.3: {} + + dayjs@1.11.20: {} + + delayed-stream@1.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + is-mobile@5.0.0: {} + + js-tokens@4.0.0: {} + + json2mq@0.2.0: + dependencies: + string-convert: 0.2.1 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + nanoid@3.3.11: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.10: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-from-env@2.1.0: {} + + rc-cascader@3.34.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-select: 14.16.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-checkbox@3.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-collapse@3.9.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-dialog@9.6.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-drawer@7.3.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-dropdown@4.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/trigger': 2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-field-form@2.7.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/async-validator': 5.1.0 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-image@7.12.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/portal': 1.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-dialog: 9.6.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-input-number@9.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/mini-decimal': 1.1.3 + classnames: 2.5.1 + rc-input: 1.8.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-input@1.8.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-mentions@2.20.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/trigger': 2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-input: 1.8.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.16.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-textarea: 1.10.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-menu@9.16.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/trigger': 2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-overflow: 1.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-motion@2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-notification@5.6.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-overflow@1.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-pagination@5.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-picker@4.11.3(dayjs@1.11.20)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/trigger': 2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-overflow: 1.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + dayjs: 1.11.20 + + rc-progress@4.0.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-rate@2.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-resize-observer@1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + resize-observer-polyfill: 1.5.1 + + rc-segmented@2.7.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-select@14.16.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/trigger': 2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-overflow: 1.5.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.19.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-slider@11.1.9(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-steps@6.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-switch@4.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-table@7.54.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/context': 1.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.19.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-tabs@15.7.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-dropdown: 4.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-menu: 9.16.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-textarea@1.10.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-input: 1.8.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-resize-observer: 1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-tooltip@6.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + '@rc-component/trigger': 2.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-tree-select@5.27.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-select: 14.16.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-tree: 5.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-tree@5.13.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-motion: 2.9.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-virtual-list: 3.19.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-upload@4.11.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + rc-util@5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.3.1 + + rc-virtual-list@3.19.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.29.2 + classnames: 2.5.1 + rc-resize-observer: 1.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + rc-util: 5.44.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + react-dom@18.2.0(react@18.2.0): + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.2 + + react-is@18.3.1: {} + + react-router-dom@7.9.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 7.9.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + + react-router@7.9.6(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + cookie: 1.1.1 + react: 18.2.0 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 18.2.0(react@18.2.0) + + react@18.2.0: + dependencies: + loose-envify: 1.4.0 + + resize-observer-polyfill@1.5.1: {} + + rollup@4.60.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.2 + '@rollup/rollup-android-arm64': 4.60.2 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-freebsd-arm64': 4.60.2 + '@rollup/rollup-freebsd-x64': 4.60.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-arm64-musl': 4.60.2 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 + '@rollup/rollup-linux-loong64-musl': 4.60.2 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-musl': 4.60.2 + '@rollup/rollup-openbsd-x64': 4.60.2 + '@rollup/rollup-openharmony-arm64': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 + '@rollup/rollup-win32-x64-gnu': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 + fsevents: 2.3.3 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + scroll-into-view-if-needed@3.1.0: + dependencies: + compute-scroll-into-view: 3.1.1 + + set-cookie-parser@2.7.2: {} + + sonner@2.0.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + + source-map-js@1.2.1: {} + + string-convert@0.2.1: {} + + stylis@4.4.0: {} + + throttle-debounce@5.0.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + toggle-selection@1.0.6: {} + + typescript@5.8.3: {} + + undici-types@7.16.0: {} + + vite@7.3.2(@types/node@24.12.2): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.10 + rollup: 4.60.2 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.2 + fsevents: 2.3.3 + + zustand@5.0.12(@types/react@18.3.28)(react@18.2.0): + optionalDependencies: + '@types/react': 18.3.28 + react: 18.2.0 diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..d248a4b --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,12 @@ +import { Toaster } from 'sonner' +import AppRoutes from '@/routes' + +export default function App() { + return ( + <> + + + + ) +} + diff --git a/src/apis/apisCommon.ts b/src/apis/apisCommon.ts new file mode 100644 index 0000000..516fc7d --- /dev/null +++ b/src/apis/apisCommon.ts @@ -0,0 +1,113 @@ +import axiosInstance from './base/axiosInstance' +import type { BaseResponse } from './base/types' + +const unwrapResponse = (payload: BaseResponse) => { + if (payload.code !== 200) { + throw new Error(payload.message || payload.msg || '请求失败') + } + + return payload +} + +const unwrapResponseData = (payload: BaseResponse) => { + return unwrapResponse(payload).data +} + +export interface OssSignToken { + fileName: string + accessId: string + callback: string + directory: string + expire: number + host: string + policy: string + signature: string + 'x:callbackkey': string +} + +export interface OssSignData { + storeType: string + token: OssSignToken +} + +export interface OssUploadData { + size: number + mime: string + width?: number + height?: number + url: string +} + +export const apiGetSmsCode = async ( + props: { + account: string + type: '0' | '1' | '2' | '8' + }, + signal?: AbortSignal +) => { + const path = '/api/v1/user/captcha' + const response = await axiosInstance.post>( + path, + { + ...props, + loginType: '0', + }, + { + signal, + } + ) + return unwrapResponse(response.data) +} + +export const apiGetOssSign = async ( + props: { + suffix: string + }, + signal?: AbortSignal +) => { + const path = '/api/v1/file/tst' + const response = await axiosInstance.post>(path, props, { + signal, + }) + + return unwrapResponseData(response.data) +} + +export const uploadFileToOss = async ( + props: { + file: File + sign: OssSignData + onProgress?: (percent: number) => void + }, + signal?: AbortSignal +) => { + const { file, sign, onProgress } = props + const token = sign.token + const key = token.directory + token.fileName + const formData = new FormData() + + formData.append('key', key) + formData.append('policy', token.policy) + formData.append('OSSAccessKeyId', token.accessId) + formData.append('success_action_status', '200') + formData.append('signature', token.signature) + formData.append('callback', token.callback) + formData.append('x:callbackkey', token['x:callbackkey']) + formData.append('file', file) + + const response = await axiosInstance.post>(token.host, formData, { + signal, + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress: event => { + if (!event.total) { + return + } + + onProgress?.(Math.round((event.loaded / event.total) * 100)) + }, + }) + + return unwrapResponseData(response.data) +} diff --git a/src/apis/apisUser.ts b/src/apis/apisUser.ts new file mode 100644 index 0000000..f0cc51e --- /dev/null +++ b/src/apis/apisUser.ts @@ -0,0 +1,27 @@ +import axiosInstance from './base/axiosInstance' +import type { BaseResponse } from './base/types' +import type { ITokenData } from '@/types/ITokenData' + +const unwrapResponse = (payload: BaseResponse) => { + if (payload.code !== 200) { + throw new Error(payload.message || payload.msg || '请求失败') + } + + return payload +} + +export const apiMallMerchantLogin = async ( + props: { + account: string + loginIp?: string + captcha?: string + password?: string + loginType: '0' | '1' + type: '0' | '1' + }, + signal?: AbortSignal +) => { + const path = '/api/v1/user/mall_merchant/login' + const response = await axiosInstance.post>(path, props, { signal }) + return unwrapResponse(response.data) +} diff --git a/src/apis/base/axiosInstance.ts b/src/apis/base/axiosInstance.ts new file mode 100644 index 0000000..8a039d9 --- /dev/null +++ b/src/apis/base/axiosInstance.ts @@ -0,0 +1,46 @@ +import axios, { AxiosError } from 'axios' +import { getBaseUrl as getConfigBaseUrl, getVersion } from '@/config/source/config' +import { ApiError, API_ERROR_CODES, type BaseResponse } from './types' +import { useSessionStore } from '@/store/session' + +const resolveBaseUrl = () => getConfigBaseUrl() + +const axiosInstance = axios.create({ + timeout: 30 * 1000, +}) + +axiosInstance.interceptors.request.use(config => { + const { token } = useSessionStore.getState() + + config.baseURL = resolveBaseUrl() + config.headers = { + 'Content-Type': 'application/json;charset=utf-8', + version: getVersion(), + ...(token?.accessToken ? { authorization: token.accessToken } : {}), + ...config.headers, + } as any + + return config +}) + +axiosInstance.interceptors.response.use( + response => response, + (error: AxiosError>) => { + if (error.response?.data) { + const data = error.response.data + throw new ApiError( + data.code ?? error.response.status, + data.message || data.msg || error.message || '请求失败', + data.data + ) + } + + if (error.code === 'ECONNABORTED') { + throw new ApiError(API_ERROR_CODES.SERVER_ERROR, '请求超时,请稍后重试') + } + + throw new ApiError(API_ERROR_CODES.SERVER_ERROR, error.message || '网络异常') + } +) + +export default axiosInstance diff --git a/src/apis/base/types.ts b/src/apis/base/types.ts new file mode 100644 index 0000000..7d0c3c5 --- /dev/null +++ b/src/apis/base/types.ts @@ -0,0 +1,33 @@ +export const API_ERROR_CODES = { + SUCCESS: 200, + UNAUTHORIZED: 600, + PARAMS_ERROR: 965, + SERVER_ERROR: 999, + DB_ERROR: 1001, + UPDATE_EMPTY: 1002, +} as const + +export class ApiError extends Error { + code: number + data?: unknown + + constructor(code: number, message: string, data?: unknown) { + super(message) + this.name = 'ApiError' + this.code = code + this.data = data + } +} + +export interface BaseResponse { + code: number + message?: string + msg?: string + data: T +} + +export interface PageRequest { + page: number + size: number +} + diff --git a/src/apis/hqb.ts b/src/apis/hqb.ts new file mode 100644 index 0000000..a1f5d6a --- /dev/null +++ b/src/apis/hqb.ts @@ -0,0 +1,96 @@ +export interface HqbImageItem { + url?: string + thumbUrl?: string + imgSrc?: string +} + +export interface HqbSpecItem { + valueId?: string | number + value?: string + imgSrc?: string | null + thumbUrl?: string | null +} + +export interface HqbSpecGroup { + specId?: string | number + specName?: string + isUploadImage?: number + items?: HqbSpecItem[] +} + +export interface HqbSkuInfo { + id?: string | number + goodsId?: string | number + storeCount?: number + keyName?: string + price?: string | number | null + skuCode?: string + key?: string + imgUrl?: string + skuImg?: string + thumbUrl?: string + originalPrice?: string | number | null + weight?: string | number | null +} + +export interface HqbShopInfo { + id?: string | number + shopName?: string +} + +export interface HqbGoodsInfo { + id: string | number + title?: string + remark?: string + goodsSn?: string + imageUrl?: string + videoUrl?: string + price?: string | number | null + maxPrice?: string | number | null + yprice?: string | number | null + catePath?: string + cateIdPath?: string + siteCode?: string + textDesc?: string | null + descImageList?: unknown + specsList?: Record + goodsImageList?: HqbImageItem[] + skuInfoList?: HqbSkuInfo[] + shopInfo?: HqbShopInfo +} + +interface HqbBaseResponse { + status: number + code: number + msg?: string + data: T +} + +const HQB_API_BASE = 'https://api.52dsy.com' + +export const fetchHqbGoodsInfo = async (goodsId: string | number, signal?: AbortSignal) => { + const response = await fetch(`${HQB_API_BASE}/newapi/goods/goods/getGoodsInfo`, { + method: 'POST', + mode: 'cors', + signal, + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + site: 'hqb', + 'app-type': 'dsy-home', + 'app-version': '0.1', + t: String(Date.now()), + }, + body: JSON.stringify({ goodsId: Number(goodsId) }), + }) + + if (!response.ok) { + throw new Error(`52hqb 接口请求失败:${response.status}`) + } + + const payload = (await response.json()) as HqbBaseResponse + if (payload.status !== 1 || payload.code !== 0 || !payload.data) { + throw new Error(payload.msg || '52hqb 未返回商品数据') + } + + return payload.data +} diff --git a/src/apis/mallCategory.ts b/src/apis/mallCategory.ts new file mode 100644 index 0000000..ed9ed96 --- /dev/null +++ b/src/apis/mallCategory.ts @@ -0,0 +1,26 @@ +import type { BaseResponse } from '@/apis/base/types' +import axiosInstance from '@/apis/base/axiosInstance' +import type { MallCategoryTreeData, MallCategoryTreeParams } from '@/types/mall' +import { compactObject } from '@/utils/mall' + +export const mallCategoryTree = async ( + payload: MallCategoryTreeParams = {}, + signal?: AbortSignal +) => { + const response = await axiosInstance.post>( + '/api/v1/mall/admin/category_tree', + compactObject(payload), + { signal } + ) + + const businessPayload = response.data + if (!businessPayload) { + throw new Error('服务端未返回业务数据') + } + + if (businessPayload.code !== 200) { + throw new Error(businessPayload.message || businessPayload.msg || '请求失败') + } + + return businessPayload.data +} diff --git a/src/apis/mallMerchant/afterSale.ts b/src/apis/mallMerchant/afterSale.ts new file mode 100644 index 0000000..3af0440 --- /dev/null +++ b/src/apis/mallMerchant/afterSale.ts @@ -0,0 +1,24 @@ +import type { + MerchantAfterSaleAuditData, + MerchantAfterSaleAuditParams, + MerchantAfterSalePageData, + MerchantAfterSalePageParams, +} from '@/types/mall' +import { merchantPost } from './shared' + +export const merchantAfterSalePage = (payload: MerchantAfterSalePageParams, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/after_sale_page', + payload, + signal + ) + +export const merchantAfterSaleAudit = ( + payload: MerchantAfterSaleAuditParams, + signal?: AbortSignal +) => + merchantPost( + '/api/v1/mall/merchant/after_sale_audit', + payload, + signal + ) diff --git a/src/apis/mallMerchant/category.ts b/src/apis/mallMerchant/category.ts new file mode 100644 index 0000000..638a8d3 --- /dev/null +++ b/src/apis/mallMerchant/category.ts @@ -0,0 +1,27 @@ +import type { + MallCategoryTreeData, + MallCategoryTreeParams, + MerchantCategoryAttributeListData, + MerchantCategoryAttributeListParams, +} from '@/types/mall' +import { merchantPost } from './shared' + +export const merchantCategoryTree = ( + payload: MallCategoryTreeParams = {}, + signal?: AbortSignal +) => + merchantPost( + '/api/v1/mall/merchant/category_tree', + payload, + signal + ) + +export const merchantCategoryAttributeList = ( + payload: MerchantCategoryAttributeListParams, + signal?: AbortSignal +) => + merchantPost( + '/api/v1/mall/merchant/category_attribute_list', + payload, + signal + ) diff --git a/src/apis/mallMerchant/featuredGoods.ts b/src/apis/mallMerchant/featuredGoods.ts new file mode 100644 index 0000000..6f3347e --- /dev/null +++ b/src/apis/mallMerchant/featuredGoods.ts @@ -0,0 +1,39 @@ +import type { + MerchantFeaturedGoodsDeleteData, + MerchantFeaturedGoodsDeleteParams, + MerchantFeaturedGoodsPageData, + MerchantFeaturedGoodsPageParams, + MerchantFeaturedGoodsSaveData, + MerchantFeaturedGoodsSaveParams, +} from '@/types/mall' +import { merchantPost } from './shared' + +export const merchantFeaturedGoodsPage = ( + payload: MerchantFeaturedGoodsPageParams, + signal?: AbortSignal +) => + merchantPost( + '/api/v1/mall/merchant/featured_goods_page', + payload, + signal + ) + +export const merchantFeaturedGoodsSave = ( + payload: MerchantFeaturedGoodsSaveParams, + signal?: AbortSignal +) => + merchantPost( + '/api/v1/mall/merchant/featured_goods_save', + payload, + signal + ) + +export const merchantFeaturedGoodsDelete = ( + payload: MerchantFeaturedGoodsDeleteParams, + signal?: AbortSignal +) => + merchantPost( + '/api/v1/mall/merchant/featured_goods_delete', + payload, + signal + ) diff --git a/src/apis/mallMerchant/goods.ts b/src/apis/mallMerchant/goods.ts new file mode 100644 index 0000000..04236e2 --- /dev/null +++ b/src/apis/mallMerchant/goods.ts @@ -0,0 +1,39 @@ +import type { + MerchantGoodsDetailData, + MerchantGoodsDetailParams, + MerchantGoodsPageData, + MerchantGoodsPageParams, + MerchantGoodsPublishData, + MerchantGoodsPublishParams, + MerchantGoodsSaveData, + MerchantGoodsSaveParams, +} from '@/types/mall' +import { merchantPost } from './shared' + +export const merchantGoodsPage = (payload: MerchantGoodsPageParams, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/goods_page', + payload, + signal + ) + +export const merchantGoodsDetail = (payload: MerchantGoodsDetailParams, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/goods_detail', + payload, + signal + ) + +export const merchantGoodsSave = (payload: MerchantGoodsSaveParams, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/goods_save', + payload, + signal + ) + +export const merchantGoodsPublish = (payload: MerchantGoodsPublishParams, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/goods_publish', + payload, + signal + ) diff --git a/src/apis/mallMerchant/index.ts b/src/apis/mallMerchant/index.ts new file mode 100644 index 0000000..363d9a1 --- /dev/null +++ b/src/apis/mallMerchant/index.ts @@ -0,0 +1,6 @@ +export * from './profile' +export * from './category' +export * from './goods' +export * from './featuredGoods' +export * from './orders' +export * from './afterSale' diff --git a/src/apis/mallMerchant/orders.ts b/src/apis/mallMerchant/orders.ts new file mode 100644 index 0000000..f13cc05 --- /dev/null +++ b/src/apis/mallMerchant/orders.ts @@ -0,0 +1,30 @@ +import type { + MerchantOrderDetailData, + MerchantOrderDetailParams, + MerchantOrderPageData, + MerchantOrderPageParams, + MerchantOrderShipData, + MerchantOrderShipParams, +} from '@/types/mall' +import { merchantPost } from './shared' + +export const merchantOrderPage = (payload: MerchantOrderPageParams, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/order_page', + payload, + signal + ) + +export const merchantOrderDetail = (payload: MerchantOrderDetailParams, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/order_detail', + payload, + signal + ) + +export const merchantOrderShip = (payload: MerchantOrderShipParams, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/order_ship', + payload, + signal + ) diff --git a/src/apis/mallMerchant/profile.ts b/src/apis/mallMerchant/profile.ts new file mode 100644 index 0000000..ef0f8a8 --- /dev/null +++ b/src/apis/mallMerchant/profile.ts @@ -0,0 +1,24 @@ +import type { + MerchantProfileGetData, + MerchantProfileGetParams, + MerchantProfileUpdateData, + MerchantProfileUpdateParams, +} from '@/types/mall' +import { merchantPost } from './shared' + +export const merchantProfileGet = (payload: MerchantProfileGetParams = {}, signal?: AbortSignal) => + merchantPost( + '/api/v1/mall/merchant/profile_get', + payload, + signal + ) + +export const merchantProfileUpdate = ( + payload: MerchantProfileUpdateParams, + signal?: AbortSignal +) => + merchantPost( + '/api/v1/mall/merchant/profile_update', + payload, + signal + ) diff --git a/src/apis/mallMerchant/shared.ts b/src/apis/mallMerchant/shared.ts new file mode 100644 index 0000000..027376f --- /dev/null +++ b/src/apis/mallMerchant/shared.ts @@ -0,0 +1,24 @@ +import type { BaseResponse } from '@/apis/base/types' +import axiosInstance from '@/apis/base/axiosInstance' +import { compactObject } from '@/utils/mall' + +export const merchantPost = async ( + path: string, + payload: TPayload, + signal?: AbortSignal +) => { + const response = await axiosInstance.post>(path, compactObject(payload), { + signal, + }) + + const businessPayload = response.data + if (!businessPayload) { + throw new Error('服务端未返回业务数据') + } + + if (businessPayload.code !== 200) { + throw new Error(businessPayload.message || businessPayload.msg || '请求失败') + } + + return businessPayload.data +} diff --git a/src/components/ImageUploadField.tsx b/src/components/ImageUploadField.tsx new file mode 100644 index 0000000..71998c0 --- /dev/null +++ b/src/components/ImageUploadField.tsx @@ -0,0 +1,219 @@ +import { CloudUploadOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons' +import { Button, Flex, Image, Progress, Typography, message } from 'antd' +import { useRef, useState } from 'react' +import type { ChangeEvent } from 'react' +import { apiGetOssSign, uploadFileToOss } from '@/apis/apisCommon' + +const { Text } = Typography + +interface ImageUploadFieldProps { + value?: string + onChange?: (value: string) => void + title?: string + hint?: string + buttonText?: string + disabled?: boolean + previewHeight?: number + compact?: boolean +} + +const normalizeUrl = (value: string) => { + const trimmed = value.trim() + if (!trimmed) { + return '' + } + + if (typeof window === 'undefined') { + return trimmed + } + + try { + const parsedUrl = new URL(trimmed, window.location.origin) + if (window.location.protocol === 'https:' && parsedUrl.protocol === 'http:') { + parsedUrl.protocol = 'https:' + } + + return parsedUrl.toString() + } catch { + return trimmed + } +} + +export default function ImageUploadField({ + value, + onChange, + title = '上传图片', + hint = '支持 JPG / PNG / WEBP,上传成功后自动回填。', + buttonText = '上传图片', + disabled = false, + previewHeight = 160, + compact = false, +}: ImageUploadFieldProps) { + const inputRef = useRef(null) + const [uploading, setUploading] = useState(false) + const [progress, setProgress] = useState(0) + + const handleUpload = async (file: File) => { + if (!file.type.startsWith('image/')) { + message.error('请选择图片文件') + return + } + + try { + setUploading(true) + setProgress(0) + + const suffix = file.name.split('.').pop() || file.type.split('/').pop() || 'png' + const sign = await apiGetOssSign({ suffix }) + const result = await uploadFileToOss( + { + file, + sign, + onProgress: percent => setProgress(percent), + } + ) + + onChange?.(normalizeUrl(result.url)) + message.success('图片上传成功') + } catch (error) { + const messageText = error instanceof Error ? error.message : '图片上传失败' + message.error(messageText) + } finally { + setUploading(false) + } + } + + const handleFileChange = async (event: ChangeEvent) => { + const file = event.target.files?.[0] + event.target.value = '' + + if (!file) { + return + } + + await handleUpload(file) + } + + if (compact) { + return ( +
+ + + {value ? ( +
+ ) + } + + return ( +
+ +
+ {title} +
+ {hint} +
+
+ + + {value ? ( + + ) : null} + + +
+ +
+ {value ? ( + uploaded + ) : ( + 暂未上传图片 + )} +
+ + {uploading ? ( + + ) : null} + + void handleFileChange(event)} + /> +
+ ) +} diff --git a/src/components/ImageWallUploadField.tsx b/src/components/ImageWallUploadField.tsx new file mode 100644 index 0000000..eec001c --- /dev/null +++ b/src/components/ImageWallUploadField.tsx @@ -0,0 +1,190 @@ +import { + CloudUploadOutlined, + DeleteOutlined, + LoadingOutlined, + ReloadOutlined, +} from '@ant-design/icons' +import { Button, Image, Typography, message } from 'antd' +import { useRef, useState } from 'react' +import type { ChangeEvent } from 'react' +import { apiGetOssSign, uploadFileToOss } from '@/apis/apisCommon' + +const { Text } = Typography + +interface ImageWallUploadFieldProps { + value?: string[] + onChange?: (value: string[]) => void + disabled?: boolean + maxCount?: number +} + +const normalizeUrl = (value: string) => { + const trimmed = value.trim() + if (!trimmed) { + return '' + } + + if (typeof window === 'undefined') { + return trimmed + } + + try { + const parsedUrl = new URL(trimmed, window.location.origin) + if (window.location.protocol === 'https:' && parsedUrl.protocol === 'http:') { + parsedUrl.protocol = 'https:' + } + + return parsedUrl.toString() + } catch { + return trimmed + } +} + +const normalizeImageList = (value?: string[]) => { + if (!Array.isArray(value)) { + return [] + } + + return value.map(item => item.trim()).filter(Boolean) +} + +export default function ImageWallUploadField({ + value, + onChange, + disabled = false, + maxCount = 10, +}: ImageWallUploadFieldProps) { + const inputRef = useRef(null) + const [uploading, setUploading] = useState(false) + const [uploadTargetIndex, setUploadTargetIndex] = useState(null) + const images = normalizeImageList(value) + + const triggerUpload = (targetIndex: number | null) => { + if (disabled || uploading) { + return + } + + if (targetIndex === null && images.length >= maxCount) { + message.warning(`轮播图最多上传 ${maxCount} 张`) + return + } + + setUploadTargetIndex(targetIndex) + inputRef.current?.click() + } + + const handleDelete = (index: number) => { + onChange?.(images.filter((_, currentIndex) => currentIndex !== index)) + } + + const handleUpload = async (file: File, targetIndex: number | null) => { + if (!file.type.startsWith('image/')) { + message.error('请选择图片文件') + return + } + + try { + setUploading(true) + const suffix = file.name.split('.').pop() || file.type.split('/').pop() || 'png' + const sign = await apiGetOssSign({ suffix }) + const result = await uploadFileToOss({ file, sign }) + const uploadedUrl = normalizeUrl(result.url) + const nextImages = [...images] + + if (targetIndex === null) { + nextImages.push(uploadedUrl) + } else { + nextImages[targetIndex] = uploadedUrl + } + + onChange?.(nextImages) + message.success('图片上传成功') + } catch (error) { + const messageText = error instanceof Error ? error.message : '图片上传失败' + message.error(messageText) + } finally { + setUploading(false) + setUploadTargetIndex(null) + } + } + + const handleFileChange = async (event: ChangeEvent) => { + const file = event.target.files?.[0] + const currentTargetIndex = uploadTargetIndex + event.target.value = '' + + if (!file) { + return + } + + await handleUpload(file, currentTargetIndex) + } + + return ( +
+
+ {images.length < maxCount ? ( + + ) : null} + + {images.map((item, index) => ( +
+ {index === 0 ? 主图 : null} + {`carousel-${index +
+ + +
+
+ ))} +
+ + void handleFileChange(event)} + /> + + 共{images.length}张 +
+ ) +} diff --git a/src/config/source/config.ts b/src/config/source/config.ts new file mode 100644 index 0000000..50e6a58 --- /dev/null +++ b/src/config/source/config.ts @@ -0,0 +1,10 @@ +import pkg from '../../../package.json' + +const DEFAULT_VERSION = pkg.version ?? '0.1.0' +export const FIXED_API_BASE_URL = 'https://test-gateway.yuntuo.hk' + +export const getBaseUrl = () => { + return FIXED_API_BASE_URL +} + +export const getVersion = () => DEFAULT_VERSION diff --git a/src/config/themes/index.ts b/src/config/themes/index.ts new file mode 100644 index 0000000..fff51ce --- /dev/null +++ b/src/config/themes/index.ts @@ -0,0 +1,2 @@ +export { lightTheme } from './light' + diff --git a/src/config/themes/light.ts b/src/config/themes/light.ts new file mode 100644 index 0000000..409b704 --- /dev/null +++ b/src/config/themes/light.ts @@ -0,0 +1,98 @@ +import type { ThemeConfig } from 'antd' + +export const lightTheme: ThemeConfig = { + token: { + colorPrimaryBg: '#e5f2ee', + colorPrimaryBgHover: '#cde7de', + colorPrimaryBorder: '#86c2ae', + colorPrimaryBorderHover: '#61ae94', + colorPrimaryActive: '#054e38', + colorPrimaryTextHover: '#349172', + colorPrimaryTextActive: '#054e38', + colorPrimaryHover: '#349172', + colorPrimary: '#156f4e', + colorInfo: '#156f4e', + colorTextBase: '#0e1513', + colorError: '#ef485e', + colorWarning: '#eda617', + colorSuccess: '#05aa81', + colorSuccessBg: '#dff4eb', + colorSuccessBgHover: '#a9e9d0', + colorTextTertiary: 'rgba(14,21,19,0.45)', + colorTextQuaternary: 'rgba(14,21,19,0.25)', + colorBgLayout: '#f5f6f8', + colorBgContainer: '#ffffff', + fontSize: 14, + fontSizeLG: 14, + fontSizeHeading3: 18, + fontSizeHeading4: 16, + fontSizeHeading5: 15, + lineHeight: 1.25, + controlHeight: 32, + controlHeightLG: 36, + controlHeightSM: 28, + sizeStep: 4, + sizeUnit: 4, + borderRadiusXS: 4, + borderRadiusSM: 6, + borderRadius: 8, + borderRadiusLG: 12, + wireframe: false, + }, + components: { + Button: { + defaultColor: 'rgb(21,111,78)', + defaultActiveColor: 'rgb(21,111,78)', + defaultHoverColor: 'rgb(52,145,114)', + defaultHoverBorderColor: 'rgb(52,145,114)', + defaultActiveBorderColor: 'rgb(21,111,78)', + groupBorderColor: 'rgb(52,145,114)', + primaryShadow: '0 0px 0 rgba(5,145,255,0.1)', + defaultShadow: '0 0px 0 rgba(0,0,0,0.02)', + dangerShadow: '0 0px 0 rgba(255,38,5,0.06)', + }, + Pagination: { + colorPrimary: 'rgb(21,111,78)', + }, + DatePicker: { + activeBorderColor: 'rgb(21,111,78)', + }, + Input: { + activeBorderColor: 'rgb(21,111,78)', + hoverBorderColor: 'rgb(52,145,114)', + }, + InputNumber: { + activeBorderColor: 'rgb(21,111,78)', + hoverBorderColor: 'rgb(52,145,114)', + }, + Select: { + activeBorderColor: 'rgb(21,111,78)', + hoverBorderColor: 'rgb(52,145,114)', + }, + Tabs: { + itemSelectedColor: 'rgb(21,111,78)', + itemHoverColor: 'rgb(52,145,114)', + inkBarColor: 'rgb(21,111,78)', + }, + Menu: { + itemBorderRadius: 6, + itemSelectedBg: '#e5f2ee', + itemSelectedColor: '#156f4e', + itemHoverBg: '#f3f8f6', + itemHoverColor: '#156f4e', + }, + Alert: { + colorInfoBg: 'rgb(240,244,242)', + colorInfoBorder: 'rgb(200,215,208)', + }, + Progress: { + circleTextColor: 'rgb(14,21,19)', + }, + Card: { + headerHeight: 46, + bodyPadding: 16, + bodyPaddingSM: 14, + }, + }, +} + diff --git a/src/hooks/useMallCategoryTree.ts b/src/hooks/useMallCategoryTree.ts new file mode 100644 index 0000000..2ba9a6d --- /dev/null +++ b/src/hooks/useMallCategoryTree.ts @@ -0,0 +1,115 @@ +import { useEffect, useMemo, useState } from 'react' +import { toast } from 'sonner' +import { mallCategoryTree } from '@/apis/mallCategory' +import type { MallCategoryItem } from '@/types/mall' + +interface MallCategoryTreeNode { + key: number + value: number + title: string + selectable?: boolean + children?: MallCategoryTreeNode[] +} + +let cachedCategoryTree: MallCategoryItem[] | null = null +let pendingCategoryTreeRequest: Promise | null = null + +const loadMallCategoryTree = async () => { + if (cachedCategoryTree) { + return cachedCategoryTree + } + + if (!pendingCategoryTreeRequest) { + pendingCategoryTreeRequest = mallCategoryTree({ status: 'enabled' }) + .then(data => { + cachedCategoryTree = data.list ?? [] + return cachedCategoryTree + }) + .finally(() => { + pendingCategoryTreeRequest = null + }) + } + + return pendingCategoryTreeRequest +} + +const buildTreeData = ( + list: MallCategoryItem[], + options?: { + leafOnly?: boolean + } +): MallCategoryTreeNode[] => { + return list.map(item => ({ + key: item.id, + value: item.id, + title: item.categoryName, + selectable: options?.leafOnly ? item.isLeaf : true, + children: buildTreeData(item.children ?? [], options), + })) +} + +const buildCategoryPathMap = ( + list: MallCategoryItem[], + parentLabels: string[] = [], + pathMap = new Map() +) => { + list.forEach(item => { + const currentLabels = [...parentLabels, item.categoryName] + pathMap.set(item.id, currentLabels.join(' / ')) + + if (item.children?.length) { + buildCategoryPathMap(item.children, currentLabels, pathMap) + } + }) + + return pathMap +} + +export const useMallCategoryTree = () => { + const [categories, setCategories] = useState(cachedCategoryTree ?? []) + const [loading, setLoading] = useState(!cachedCategoryTree) + + useEffect(() => { + let cancelled = false + + const run = async () => { + try { + const list = await loadMallCategoryTree() + if (cancelled) { + return + } + + setCategories(list) + } catch (error) { + if (cancelled) { + return + } + + const message = error instanceof Error ? error.message : '加载商品分类失败' + toast.error(message) + } finally { + if (!cancelled) { + setLoading(false) + } + } + } + + void run() + + return () => { + cancelled = true + } + }, []) + + const filterTreeData = useMemo(() => buildTreeData(categories), [categories]) + const leafTreeData = useMemo(() => buildTreeData(categories, { leafOnly: true }), [categories]) + const categoryPathMap = useMemo(() => buildCategoryPathMap(categories), [categories]) + + return { + categories, + loading, + filterTreeData, + leafTreeData, + categoryPathMap, + } +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..3148f35 --- /dev/null +++ b/src/index.css @@ -0,0 +1,953 @@ +html, +body, +#root { + min-height: 100%; + height: 100%; +} + +body { + margin: 0; + color: #141414; + background: #f5f6f8; + font-family: + "PingFang SC", + "Hiragino Sans GB", + "Microsoft YaHei", + "Helvetica Neue", + Arial, + sans-serif; +} + +* { + box-sizing: border-box; +} + +a { + color: inherit; +} + +.app-page-title { + margin-bottom: 0; + font-size: 18px; + font-weight: 600; + line-height: 1.3; +} + +.page-card .ant-card-body { + padding: 16px; +} + +.page-card .ant-card-head { + min-height: 46px; + padding: 0 16px; +} + +.page-card .ant-card-head-title { + padding: 12px 0; + font-size: 14px; + font-weight: 600; +} + +.merchant-shell__topbar { + height: 64px; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.merchant-shell__brand, +.merchant-shell__topnav-btn, +.merchant-shell__user-trigger { + border: 0; + background: transparent; + padding: 0; + cursor: pointer; +} + +.merchant-shell__brand { + display: inline-flex; + align-items: center; + gap: 10px; + color: inherit; +} + +.merchant-shell__brand-mark { + width: 36px; + height: 36px; + border-radius: 12px; + background: linear-gradient(135deg, #156f4e 0%, #2c9a73 100%); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 700; + box-shadow: 0 8px 18px rgba(21, 111, 78, 0.16); +} + +.merchant-shell__brand-copy { + display: inline-flex; + flex-direction: column; + align-items: flex-start; + gap: 1px; +} + +.merchant-shell__collapse-btn { + width: 40px; + height: 40px; + border-radius: 12px; +} + +.merchant-shell__topnav { + display: flex; + align-items: center; + gap: 6px; +} + +.merchant-shell__topnav-btn { + height: 40px; + padding: 0 16px; + border-radius: 12px; + color: #2e3b37; + font-size: 14px; + transition: all 0.2s ease; +} + +.merchant-shell__topnav-btn:hover { + color: var(--ant-color-primary); + background: #f3f8f6; +} + +.merchant-shell__topnav-btn.is-active { + color: var(--ant-color-primary); + background: var(--ant-color-primary-bg); + font-weight: 600; +} + +.merchant-shell__user-trigger { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.merchant-shell__sider { + height: 100%; + padding: 14px 12px 12px; + display: flex; + flex-direction: column; + gap: 10px; + background: var(--ant-color-bg-container); +} + +.merchant-shell__sider-title { + padding: 0 4px; + font-size: 12px; +} + +.ant-typography { + margin-bottom: 0; +} + +.ant-btn { + font-size: 13px; + box-shadow: none; +} + +.ant-btn.ant-btn-default:not(.ant-btn-dangerous), +.ant-btn.ant-btn-dashed:not(.ant-btn-dangerous) { + color: var(--ant-color-primary); + border-color: var(--ant-color-primary-border); +} + +.ant-btn.ant-btn-default:not(.ant-btn-dangerous):hover, +.ant-btn.ant-btn-dashed:not(.ant-btn-dangerous):hover { + color: var(--ant-color-primary-hover); + border-color: var(--ant-color-primary-hover); + background: #fff; +} + +.ant-btn.ant-btn-link:not(.ant-btn-dangerous), +.ant-btn.ant-btn-text:not(.ant-btn-dangerous) { + color: var(--ant-color-primary); +} + +.ant-btn.ant-btn-link:not(.ant-btn-dangerous):hover, +.ant-btn.ant-btn-text:not(.ant-btn-dangerous):hover { + color: var(--ant-color-primary-hover); +} + +.ant-input, +.ant-input-number, +.ant-select-selector, +.ant-btn, +.ant-tabs-tab, +.ant-form-item-label > label, +.ant-descriptions, +.ant-menu, +.ant-table, +.ant-tag { + font-size: 13px; +} + +.json-textarea textarea { + font-family: + "SFMono-Regular", + Consolas, + "Liberation Mono", + Menlo, + monospace; + font-size: 12px; + line-height: 1.6; +} + +.goods-category-card { + display: flex; + flex-direction: column; + gap: 14px; +} + +.goods-category-card__row { + display: flex; + align-items: flex-start; + gap: 20px; +} + +.goods-category-card__label { + width: 112px; + padding-top: 12px; + display: flex; + justify-content: flex-end; + white-space: nowrap; + font-size: 13px; + font-weight: 600; + color: #222; +} + +.goods-category-card__required { + margin-right: 4px; + color: #ff4d4f; +} + +.goods-category-card__control { + flex: 1; + min-width: 0; +} + +.goods-category-card__picker { + display: flex; + align-items: center; + gap: 10px; +} + +.goods-category-card__picker .ant-select { + width: 100%; +} + +.goods-category-card__picker .ant-cascader { + width: 100%; +} + +.goods-category-card__picker .ant-select-selector, +.goods-category-card__picker .ant-cascader .ant-select-selector, +.goods-category-card__picker .ant-input, +.goods-category-card__picker .ant-btn { + height: 38px !important; +} + +.goods-category-card__picker .ant-select-selection-placeholder, +.goods-category-card__picker .ant-select-selection-item, +.goods-category-card__picker .ant-cascader .ant-select-selection-placeholder, +.goods-category-card__picker .ant-cascader .ant-select-selection-item, +.goods-category-card__picker .ant-input { + line-height: 36px !important; + font-size: 13px !important; +} + +.goods-category-card__picker .ant-btn { + min-width: 104px; + border-radius: 10px; + font-size: 13px; + font-weight: 600; +} + +.goods-category-card__path { + margin-top: 8px; + padding-left: 4px; + font-size: 11px; + color: var(--ant-color-primary); +} + +.goods-product-card { + display: flex; + flex-direction: column; + gap: 12px; +} + +.goods-product-card__row { + display: flex; + align-items: center; + gap: 12px; +} + +.goods-product-card__row--image { + align-items: flex-start; +} + +.goods-product-card__label { + width: 104px; + display: flex; + justify-content: flex-end; + white-space: nowrap; + font-size: 12px; + font-weight: 600; + color: #222; +} + +.goods-product-card__row--image .goods-product-card__label { + padding-top: 4px; +} + +.goods-product-card__required { + margin-right: 4px; + color: #ff4d4f; +} + +.goods-product-card__control { + flex: 1; + min-width: 0; +} + +.goods-product-card__control .ant-input, +.goods-product-card__control .ant-input-number, +.goods-product-card__control .ant-select-selector { + height: 30px !important; + border-radius: 7px !important; +} + +.goods-product-card__control .ant-input { + padding-top: 0; + padding-bottom: 0; + line-height: 28px; + font-size: 12px; + padding-left: 10px; + padding-right: 10px; +} + +.goods-product-card__control .ant-input-number { + display: flex; + align-items: center; +} + +.goods-product-card__control .ant-select-selection-placeholder, +.goods-product-card__control .ant-select-selection-item { + line-height: 28px !important; + font-size: 12px !important; +} + +.goods-product-card__control .ant-input-show-count-suffix { + font-size: 10px; + color: #9ca3af; +} + +.goods-product-card__control .ant-radio-group { + display: flex; + align-items: center; + gap: 18px; + min-height: 38px; +} + +.goods-product-card__control .ant-radio-wrapper { + margin-inline-end: 0; + font-size: 12px; +} + +.goods-product-card__control--medium { + max-width: 660px; +} + +.goods-attribute-card { + display: flex; + flex-direction: column; + gap: 12px; +} + +.goods-attribute-card__row { + display: flex; + align-items: center; + gap: 12px; +} + +.goods-attribute-card__label { + width: 104px; + display: flex; + justify-content: flex-end; + white-space: nowrap; + font-size: 12px; + font-weight: 600; + color: #222; +} + +.goods-attribute-card__control { + width: min(520px, 100%); +} + +.goods-attribute-card__control .ant-select-selector { + min-height: 30px !important; + border-radius: 7px !important; + font-size: 12px; +} + +.goods-attribute-card__control .ant-select-selection-placeholder, +.goods-attribute-card__control .ant-select-selection-item { + font-size: 12px !important; +} + +.goods-attribute-card__empty { + min-height: 42px; + border: 1px dashed #d8dce3; + border-radius: 10px; + display: flex; + align-items: center; + padding: 0 14px; + color: #8c8c8c; + font-size: 12px; + background: #fafafa; +} + +.image-wall-upload { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; +} + +.image-wall-upload__list { + width: 100%; + display: flex; + align-items: center; + gap: 10px; + overflow-x: auto; +} + +.image-wall-upload__add { + width: 112px; + height: 112px; + flex: 0 0 112px; + border: 1px dashed #d2d7de; + border-radius: 14px; + background: #fff; + color: #6b7280; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.image-wall-upload__add:hover:not(:disabled) { + border-color: var(--ant-color-primary); + color: var(--ant-color-primary); +} + +.image-wall-upload__add:disabled { + cursor: not-allowed; + opacity: 0.72; +} + +.image-wall-upload__add-icon { + font-size: 24px; + line-height: 1; +} + +.image-wall-upload__add-text { + font-size: 13px; + font-weight: 500; +} + +.image-wall-upload__item { + position: relative; + width: 112px; + height: 112px; + flex: 0 0 112px; + overflow: hidden; + border: 1px solid #d9dee6; + border-radius: 14px; + background: #fff; + box-shadow: 0 4px 10px rgba(15, 23, 42, 0.04); +} + +.image-wall-upload__item .ant-image, +.image-wall-upload__item .ant-image-img { + width: 100%; + height: 100%; +} + +.image-wall-upload__badge { + position: absolute; + top: 6px; + left: 6px; + z-index: 2; + padding: 1px 6px; + border-radius: 999px; + background: #2f76ff; + color: #fff; + font-size: 10px; + font-weight: 600; + line-height: 16px; +} + +.image-wall-upload__toolbar { + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + padding: 14px 4px 4px; + background: linear-gradient(180deg, rgba(15, 23, 42, 0) 0%, rgba(15, 23, 42, 0.68) 100%); +} + +.image-wall-upload__toolbar .ant-btn { + color: #fff; + font-size: 11px; + padding-inline: 6px; +} + +.image-wall-upload__toolbar .ant-btn.ant-btn-text:hover { + color: #fff; + background: rgba(255, 255, 255, 0.12); +} + +.image-wall-upload__toolbar .ant-btn.ant-btn-text.ant-btn-dangerous { + color: #fff; +} + +.image-wall-upload__counter { + font-size: 11px; + color: #b7bcc4 !important; +} + +.hqb-import-modal__input { + display: flex; + flex-direction: column; + gap: 8px; +} + +.hqb-import-modal__section { + padding: 12px; + border: 1px solid var(--ant-color-border-secondary); + border-radius: 12px; + background: #fff; +} + +.hqb-import-modal__section-title { + margin-bottom: 10px; + font-size: 13px; + font-weight: 600; + color: #202624; +} + +.hqb-import-modal__images { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.hqb-import-modal__image, +.hqb-import-modal__sku-image { + border-radius: 8px; + object-fit: cover; + border: 1px solid var(--ant-color-border-secondary); +} + +.goods-list-product { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 0; +} + +.goods-list-product__image { + flex: 0 0 56px; + border-radius: 9px; + object-fit: cover; + border: 1px solid var(--ant-color-border-secondary); + background: #f7f8fa; +} + +.goods-list-product__placeholder { + width: 56px; + height: 56px; + flex: 0 0 56px; + border-radius: 9px; + border: 1px dashed var(--ant-color-border); + background: #fafafa; + color: #a0a6ad; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; +} + +.goods-list-product__content { + min-width: 0; + flex: 1; +} + +.goods-list-product__content .ant-typography { + max-width: 100%; + font-size: 12px; + line-height: 1.35; +} + +.goods-list-product__title { + display: -webkit-box; + max-height: 34px; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + font-size: 13px !important; + line-height: 17px !important; + white-space: normal; +} + +.goods-detail-editor { + display: flex; + align-items: flex-start; + gap: 18px; +} + +.goods-detail-editor__canvas { + flex: 1; + min-width: 0; + min-height: 320px; + padding: 14px; + border-radius: 20px; + background: #f7f7f8; + border: 1px solid #efefef; +} + +.goods-detail-editor__block { + display: flex; + align-items: flex-start; + gap: 10px; +} + +.goods-detail-editor__block + .goods-detail-editor__block { + margin-top: 12px; +} + +.goods-detail-editor__content { + flex: 1; + min-width: 0; +} + +.goods-detail-editor__content .ant-input, +.goods-detail-editor__content .ant-input-affix-wrapper, +.goods-detail-editor__content .ant-input-textarea textarea { + border-radius: 12px !important; +} + +.goods-detail-editor__content .ant-input-textarea textarea { + min-height: 72px !important; + padding: 10px 12px; + font-size: 12px; +} + +.goods-detail-editor__tools { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding-top: 6px; +} + +.goods-detail-editor__tools .ant-btn { + width: 28px; + height: 28px; + padding: 0; + border-radius: 8px; +} + +.goods-detail-editor__sidebar { + width: 150px; + flex: 0 0 150px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.goods-detail-editor__action.ant-btn { + height: 44px; + border-radius: 14px; + font-size: 13px; + font-weight: 600; +} + +.goods-detail-editor__empty { + min-height: 280px; + border: 1px dashed #d8dce3; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + color: #9ca3af; + font-size: 12px; + background: #fff; +} + +.sku-variant-editor { + display: flex; + flex-direction: column; + gap: 18px; +} + +.sku-variant-editor__specs { + padding: 16px; + border: 1px solid #eef0f3; + border-radius: 8px; + background: #fafafa; +} + +.sku-variant-editor__spec-row { + position: relative; + display: flex; + flex-direction: column; + gap: 10px; + padding-right: 34px; +} + +.sku-variant-editor__spec-row + .sku-variant-editor__spec-row { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid #eceff3; +} + +.sku-variant-editor__inputs { + display: grid; + grid-template-columns: 180px minmax(180px, 1fr) 76px; + gap: 10px; + max-width: 620px; +} + +.sku-variant-editor__name { + height: 34px; + padding: 0 12px; + border: 1px solid #d9d9d9; + border-radius: 8px; + display: flex; + align-items: center; + background: #fff; + font-size: 13px; + font-weight: 600; + color: #222; +} + +.sku-variant-editor__inputs .ant-input, +.sku-variant-editor__inputs .ant-btn { + height: 34px; + border-radius: 8px; + font-size: 13px; +} + +.sku-variant-editor__values { + min-height: 24px; + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding-left: 190px; +} + +.sku-variant-editor__values .ant-tag { + margin-inline-end: 0; + padding: 3px 8px; + border-radius: 7px; + font-size: 12px; +} + +.sku-variant-editor__remove { + position: absolute; + top: 1px; + right: 0; +} + +.sku-variant-editor__title { + font-size: 14px; +} + +.sku-variant-editor__empty { + height: 42px; + display: flex; + align-items: center; + color: #8c8c8c; + font-size: 12px; +} + +.sku-table-wrap { + overflow-x: auto; + border: 1px solid #eceff3; + border-radius: 8px; +} + +.sku-table { + width: max-content; + min-width: 100%; + border-collapse: collapse; + background: #fff; +} + +.sku-table th, +.sku-table td { + min-width: 128px; + padding: 10px 12px; + border-right: 1px solid #eef0f3; + border-bottom: 1px solid #eef0f3; + text-align: left; + vertical-align: middle; + font-size: 13px; +} + +.sku-table th { + background: #fafafa; + font-weight: 600; +} + +.sku-table th span { + margin-left: 2px; + color: #ff4d4f; +} + +.sku-table__image-col, +.sku-table td:first-child { + min-width: 112px; + width: 112px; + text-align: center; +} + +.sku-table .ant-input, +.sku-table .ant-input-number, +.sku-table .ant-select-selector { + height: 34px !important; + border-radius: 8px !important; + font-size: 13px; +} + +.sku-table .ant-input-number { + width: 100%; +} + +.sku-table .ant-select-selection-item, +.sku-table .ant-select-selection-placeholder { + line-height: 32px !important; +} + +.image-upload-compact { + position: relative; + width: 76px; + height: 76px; + margin: 0 auto; +} + +.image-upload-compact__trigger { + width: 76px; + height: 76px; + padding: 0; + border: 1px dashed #d8dce3; + border-radius: 12px; + background: #fff; + color: #6b7280; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + cursor: pointer; + overflow: hidden; +} + +.image-upload-compact__trigger:hover:not(:disabled) { + border-color: var(--ant-color-primary); + color: var(--ant-color-primary); +} + +.image-upload-compact__trigger:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +.image-upload-compact__icon { + font-size: 22px; +} + +.image-upload-compact__trigger span { + font-size: 12px; +} + +.image-upload-compact__trigger img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.image-upload-compact__remove { + position: absolute; + top: -8px; + right: -8px; + width: 22px; + height: 22px; + min-width: 22px; + padding: 0; + border-radius: 999px; + background: #fff; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.12); +} + +@media (max-width: 1280px) { + .goods-category-card__row, + .goods-product-card__row { + flex-direction: column; + gap: 12px; + } + + .goods-category-card__label, + .goods-product-card__label { + width: auto; + padding-top: 0; + justify-content: flex-start; + } + + .goods-category-card__picker { + flex-direction: column; + align-items: stretch; + } + + .image-wall-upload { + align-items: stretch; + } + + .goods-detail-editor { + flex-direction: column; + } + + .goods-detail-editor__sidebar { + width: 100%; + flex: none; + } + + .sku-variant-editor__inputs { + grid-template-columns: 1fr; + max-width: none; + } + + .sku-variant-editor__values { + padding-left: 0; + } +} diff --git a/src/layouts/RootLayout.tsx b/src/layouts/RootLayout.tsx new file mode 100644 index 0000000..028e3ba --- /dev/null +++ b/src/layouts/RootLayout.tsx @@ -0,0 +1,17 @@ +import { useEffect } from 'react' +import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { useSessionStore } from '@/store/session' + +export default function RootLayout() { + const token = useSessionStore(state => state.token) + const location = useLocation() + const navigate = useNavigate() + + useEffect(() => { + if (location.pathname === '/') { + navigate(token ? '/profile' : '/login', { replace: true }) + } + }, [location.pathname, navigate, token]) + + return +} diff --git a/src/layouts/auth/AuthLayout.tsx b/src/layouts/auth/AuthLayout.tsx new file mode 100644 index 0000000..49e28f8 --- /dev/null +++ b/src/layouts/auth/AuthLayout.tsx @@ -0,0 +1,33 @@ +import { Layout, Space, Typography, theme } from 'antd' +import { Outlet } from 'react-router-dom' + +export default function AuthLayout() { + const { token } = theme.useToken() + + return ( + + +
+ + + 云拓商城商家端 + + + +
+
+
+ ) +} diff --git a/src/layouts/main/MainLayout.tsx b/src/layouts/main/MainLayout.tsx new file mode 100644 index 0000000..d1b9d5a --- /dev/null +++ b/src/layouts/main/MainLayout.tsx @@ -0,0 +1,293 @@ +import { useEffect, useMemo, useState } from 'react' +import type { ReactNode } from 'react' +import { + AppstoreOutlined, + AuditOutlined, + LogoutOutlined, + MenuFoldOutlined, + MenuUnfoldOutlined, + ShopOutlined, + ShoppingCartOutlined, + StarOutlined, + UserOutlined, +} from '@ant-design/icons' +import { Avatar, Button, Dropdown, Layout, Menu, Space, Typography, theme } from 'antd' +import type { MenuProps } from 'antd' +import { toast } from 'sonner' +import { Outlet, useLocation, useNavigate } from 'react-router-dom' +import { merchantProfileGet } from '@/apis/mallMerchant' +import { useSessionStore } from '@/store/session' +import { resolveMerchantSessionIdentity } from '@/utils/mall' + +const { Header, Sider, Content } = Layout + +interface SectionItem { + key: string + label: string + icon: ReactNode +} + +interface NavSection { + key: string + label: string + items: SectionItem[] +} + +const NAV_SECTIONS: NavSection[] = [ + { + key: 'shop', + label: '店铺', + items: [ + { + key: '/profile', + label: '商家资料', + icon: , + }, + { + key: '/featured-goods', + label: '首页精选商品', + icon: , + }, + ], + }, + { + key: 'goods', + label: '商品', + items: [ + { + key: '/goods', + label: '商品管理', + icon: , + }, + ], + }, + { + key: 'trade', + label: '交易', + items: [ + { + key: '/orders', + label: '订单管理', + icon: , + }, + { + key: '/after-sales', + label: '售后管理', + icon: , + }, + ], + }, +] + +const resolveCurrentSection = (pathname: string) => { + return ( + NAV_SECTIONS.find(section => section.items.some(item => pathname.startsWith(item.key))) ?? NAV_SECTIONS[0] + ) +} + +const getAccountTypeLabel = (value?: string) => { + if (value === '0') { + return '主账号' + } + + if (value === '1') { + return '子账号' + } + + return '商家账号' +} + +export default function MainLayout() { + const navigate = useNavigate() + const location = useLocation() + const { token } = theme.useToken() + const [collapsed, setCollapsed] = useState(false) + const authToken = useSessionStore(state => state.token) + const userInfo = useSessionStore(state => state.userInfo) + const loginAccount = useSessionStore(state => state.loginAccount) + const merchantName = useSessionStore(state => state.merchantName) + const merchantId = useSessionStore(state => state.merchantId) + const updateIdentity = useSessionStore(state => state.updateIdentity) + const clearSession = useSessionStore(state => state.clearSession) + + const currentSection = useMemo(() => resolveCurrentSection(location.pathname), [location.pathname]) + + const sideMenuItems: MenuProps['items'] = useMemo( + () => + currentSection.items.map(item => ({ + key: item.key, + icon: item.icon, + label: item.label, + })), + [currentSection] + ) + + useEffect(() => { + if ( + !authToken?.accessToken || + location.pathname.startsWith('/profile') || + (merchantId && merchantName) + ) { + return + } + + let cancelled = false + const syncIdentity = async () => { + try { + const data = await merchantProfileGet(merchantId ? { merchantId } : {}) + if (cancelled) { + return + } + + updateIdentity(resolveMerchantSessionIdentity(data.merchant)) + } catch (error) { + if (cancelled) { + return + } + + const message = error instanceof Error ? error.message : '同步商家资料失败' + toast.error(message) + } + } + + void syncIdentity() + + return () => { + cancelled = true + } + }, [authToken?.accessToken, location.pathname, merchantId, merchantName, updateIdentity]) + + const dropdownItems: MenuProps['items'] = [ + { + key: 'logout', + icon: , + label: '退出登录', + onClick: () => { + clearSession() + navigate('/login', { replace: true }) + }, + }, + ] + + return ( + +
+
+ + + + ) + })} +
+ + + + + {userInfo?.account || + loginAccount || + [merchantName, getAccountTypeLabel(authToken?.uType)].filter(Boolean).join(' | ') || + '已登录用户'} + + + + + + +
+ + + +
+ {!collapsed ? ( + + {currentSection.label} + + ) : null} + navigate(key)} + style={{ + borderInlineEnd: 'none', + background: token.colorBgContainer, + }} + /> +
+
+ + + + +
+
+ ) +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..d2a934a --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,21 @@ +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { App as AntdApp, ConfigProvider } from 'antd' +import zhCN from 'antd/locale/zh_CN' +import dayjs from 'dayjs' +import 'dayjs/locale/zh-cn' +import App from './App' +import { lightTheme } from '@/config/themes' +import './index.css' + +dayjs.locale('zh-cn') + +createRoot(document.getElementById('root')!).render( + + + + + + + +) diff --git a/src/pages/afterSale/AuditModal.tsx b/src/pages/afterSale/AuditModal.tsx new file mode 100644 index 0000000..8252973 --- /dev/null +++ b/src/pages/afterSale/AuditModal.tsx @@ -0,0 +1,108 @@ +import { Form, Input, Modal, Radio } from 'antd' +import { toast } from 'sonner' +import { merchantAfterSaleAudit } from '@/apis/mallMerchant' +import type { AfterSaleSummary } from '@/types/mall' + +interface AuditModalProps { + open: boolean + merchantId: string + record: AfterSaleSummary | null + onClose: () => void + onSuccess: () => void +} + +interface AuditFormValues { + auditStatus: 'approved' | 'rejected' + refundAmount?: string + rejectReason?: string + handleRemark?: string +} + +export default function AuditModal({ + open, + merchantId, + record, + onClose, + onSuccess, +}: AuditModalProps) { + const [form] = Form.useForm() + const auditStatus = Form.useWatch('auditStatus', form) + + const handleOk = async () => { + if (!record) { + return + } + + try { + const values = await form.validateFields() + await merchantAfterSaleAudit({ + merchantId, + afterSaleId: record.id, + auditStatus: values.auditStatus, + refundAmount: values.refundAmount?.trim(), + rejectReason: values.rejectReason?.trim(), + handleRemark: values.handleRemark?.trim(), + }) + toast.success('售后审核已提交') + form.resetFields() + onSuccess() + onClose() + } catch (error) { + const message = error instanceof Error ? error.message : '提交审核失败' + toast.error(message) + } + } + + return ( + { + form.resetFields() + onClose() + }} + onOk={() => void handleOk()} + okText="提交审核" + > +
+ + + + {auditStatus === 'approved' ? ( + + + + ) : ( + + + + )} + + + +
+
+ ) +} diff --git a/src/pages/afterSale/index.tsx b/src/pages/afterSale/index.tsx new file mode 100644 index 0000000..24af9e1 --- /dev/null +++ b/src/pages/afterSale/index.tsx @@ -0,0 +1,230 @@ +import { useEffect, useState } from 'react' +import { Button, Card, Form, Input, Select, Space, Table, Tag, Typography } from 'antd' +import { toast } from 'sonner' +import { merchantAfterSalePage } from '@/apis/mallMerchant' +import AuditModal from './AuditModal' +import { useSessionStore } from '@/store/session' +import type { AfterSaleStatus, AfterSaleSummary } from '@/types/mall' +import { + AFTER_SALE_STATUS_OPTIONS, + AFTER_SALE_TYPE_OPTIONS, + formatUnixTime, + getStatusColor, + getStatusLabel, + prettyJsonText, +} from '@/utils/mall' + +interface AfterSaleSearchValues { + afterSaleType?: 0 | 1 | 2 + afterSaleStatus?: AfterSaleStatus | '' + keyword?: string +} + +export default function AfterSalePage() { + const merchantId = useSessionStore(state => state.merchantId) + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [size, setSize] = useState(20) + const [total, setTotal] = useState(0) + const [list, setList] = useState([]) + const [auditRecord, setAuditRecord] = useState(null) + + const loadList = async (nextPage = page, nextSize = size) => { + if (!merchantId) { + return + } + + try { + setLoading(true) + const values = form.getFieldsValue() + const result = await merchantAfterSalePage({ + merchantId, + afterSaleType: values.afterSaleType ?? 0, + afterSaleStatus: values.afterSaleStatus || undefined, + keyword: values.keyword?.trim(), + page: nextPage, + size: nextSize, + }) + setList(result.list ?? []) + setTotal(result.total ?? 0) + } catch (error) { + const message = error instanceof Error ? error.message : '加载售后列表失败' + toast.error(message) + } finally { + setLoading(false) + } + } + + useEffect(() => { + form.setFieldsValue({ + afterSaleType: 0, + afterSaleStatus: '', + keyword: '', + }) + setPage(1) + void loadList(1, size) + }, [merchantId]) // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + + +
+ + ({ ...item }))} + /> + + + + + + + + + + +
+
+
+ + + ( + + 售后单 ID:{record.id} + 会员 ID:{record.uid} + 原因:{record.reason || '-'} + 说明:{record.description || '-'} + 驳回原因:{record.rejectReason || '-'} + 处理备注:{record.handleRemark || '-'} + 审核时间:{formatUnixTime(record.auditTime)} + 完成时间:{formatUnixTime(record.successTime)} + 关闭时间:{formatUnixTime(record.closeTime)} +
+ 凭证 JSON: + + {prettyJsonText(record.evidenceJson) || '-'} + +
+
+ ), + }} + pagination={{ + current: page, + pageSize: size, + total, + showSizeChanger: true, + onChange: (nextPage, nextSize) => { + setPage(nextPage) + setSize(nextSize) + void loadList(nextPage, nextSize) + }, + }} + columns={[ + { + title: '售后单号', + dataIndex: 'afterSaleNo', + width: 220, + render: (_, record) => ( + + {record.afterSaleNo} + {record.orderNo} + + ), + }, + { + title: '商品', + dataIndex: 'spuName', + width: 240, + render: (_, record) => ( + + {record.spuName} + {record.skuName} + + ), + }, + { + title: '类型', + dataIndex: 'afterSaleType', + width: 120, + render: value => (value === 1 ? '仅退款' : '退货退款'), + }, + { + title: '金额', + dataIndex: 'applyAmount', + width: 160, + render: (_, record) => ( + + 申请:{record.applyAmount} + + 已退:{record.refundAmount || '-'} + + + ), + }, + { + title: '状态', + dataIndex: 'afterSaleStatus', + width: 160, + render: value => {getStatusLabel(value)}, + }, + { + title: '申请时间', + dataIndex: 'createdAt', + width: 180, + render: value => formatUnixTime(value), + }, + { + title: '操作', + key: 'actions', + width: 140, + render: (_, record) => ( + + ), + }, + ]} + /> + + + setAuditRecord(null)} + onSuccess={() => void loadList()} + /> + + ) +} diff --git a/src/pages/featuredGoods/GoodsPickerModal.tsx b/src/pages/featuredGoods/GoodsPickerModal.tsx new file mode 100644 index 0000000..b5970b7 --- /dev/null +++ b/src/pages/featuredGoods/GoodsPickerModal.tsx @@ -0,0 +1,226 @@ +import { useEffect, useMemo, useState } from 'react' +import { Button, Form, Input, Modal, Select, Space, Table, Tag, Typography } from 'antd' +import { toast } from 'sonner' +import { merchantGoodsPage } from '@/apis/mallMerchant' +import type { AuditStatus, GoodsSummary, SaleStatus } from '@/types/mall' +import { + AUDIT_STATUS_OPTIONS, + SALE_STATUS_OPTIONS, + getStatusColor, + getStatusLabel, +} from '@/utils/mall' + +interface GoodsPickerSearchValues { + keyword?: string + auditStatus?: AuditStatus | '' + saleStatus?: SaleStatus | '' +} + +interface GoodsPickerModalProps { + open: boolean + merchantId: string + configuredSpuIds: number[] + currentSpuId?: number + onCancel: () => void + onSelect: (goods: GoodsSummary) => void +} + +const PAGE_SIZE = 10 + +export default function GoodsPickerModal({ + open, + merchantId, + configuredSpuIds, + currentSpuId, + onCancel, + onSelect, +}: GoodsPickerModalProps) { + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [total, setTotal] = useState(0) + const [list, setList] = useState([]) + + const disabledSpuIds = useMemo( + () => new Set(configuredSpuIds.filter(spuId => spuId !== currentSpuId)), + [configuredSpuIds, currentSpuId] + ) + + const loadList = async (nextPage = page) => { + if (!open) { + return + } + + try { + setLoading(true) + const values = form.getFieldsValue() + const result = await merchantGoodsPage({ + merchantId, + keyword: values.keyword?.trim(), + auditStatus: values.auditStatus || undefined, + saleStatus: values.saleStatus || undefined, + page: nextPage, + size: PAGE_SIZE, + }) + setList(result.list ?? []) + setTotal(result.total ?? 0) + } catch (error) { + const message = error instanceof Error ? error.message : '加载商品列表失败' + toast.error(message) + } finally { + setLoading(false) + } + } + + useEffect(() => { + if (!open) { + return + } + + form.setFieldsValue({ + keyword: '', + auditStatus: '', + saleStatus: '', + }) + setPage(1) + void loadList(1) + }, [form, merchantId, open]) // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + 关闭 + , + ]} + > + + + 当前只展示该商家主体下的商品,merchantId 为 {merchantId};已在精选列表中的商品不能重复选择。 + + +
+ + + + + ({ ...item }))} /> + + + + + + + + + +
{ + setPage(nextPage) + void loadList(nextPage) + }, + }} + columns={[ + { + title: '商品编码', + dataIndex: 'spuNo', + width: 160, + render: value => value || '-', + }, + { + title: '商品名称', + dataIndex: 'spuName', + width: 220, + render: (_, record) => ( + + {record.spuName} + {record.subTitle || '-'} + + ), + }, + { + title: '类目 ID', + dataIndex: 'categoryId', + width: 110, + }, + { + title: '商品状态', + dataIndex: 'status', + width: 110, + render: value => {getStatusLabel(value)}, + }, + { + title: '审核状态', + dataIndex: 'auditStatus', + width: 110, + render: value => {getStatusLabel(value)}, + }, + { + title: '上下架', + dataIndex: 'saleStatus', + width: 110, + render: value => {getStatusLabel(value)}, + }, + { + title: '价格区间', + dataIndex: 'minPrice', + width: 160, + render: (_, record) => `${record.minPrice} - ${record.maxPrice}`, + }, + { + title: '库存', + dataIndex: 'totalStock', + width: 100, + }, + { + title: '操作', + key: 'actions', + width: 120, + fixed: 'right', + render: (_, record) => ( + + ), + }, + ]} + /> + + + ) +} diff --git a/src/pages/featuredGoods/index.tsx b/src/pages/featuredGoods/index.tsx new file mode 100644 index 0000000..801f9d1 --- /dev/null +++ b/src/pages/featuredGoods/index.tsx @@ -0,0 +1,465 @@ +import { useEffect, useMemo, useState } from 'react' +import { + Button, + Card, + Form, + Image, + Input, + InputNumber, + Modal, + Popconfirm, + Radio, + Space, + Table, + Tag, + Typography, +} from 'antd' +import { PlusOutlined } from '@ant-design/icons' +import { toast } from 'sonner' +import { + merchantFeaturedGoodsDelete, + merchantFeaturedGoodsPage, + merchantFeaturedGoodsSave, +} from '@/apis/mallMerchant' +import { useSessionStore } from '@/store/session' +import type { EnableStatus, GoodsSummary, MerchantFeaturedGoodsItem } from '@/types/mall' +import { ENABLE_STATUS_OPTIONS, formatUnixTime, getStatusColor, getStatusLabel } from '@/utils/mall' +import GoodsPickerModal from './GoodsPickerModal' + +interface SearchValues { + keyword?: string + status?: EnableStatus | '' +} + +interface EditorValues { + sort?: number + status: EnableStatus +} + +interface SelectedGoodsInfo { + id: number + spuNo: string + spuName: string + subTitle: string +} + +const ENABLED_DISABLED_OPTIONS = [ + { label: '启用', value: 'enabled' }, + { label: '停用', value: 'disabled' }, +] as const + +const getGoodsStatusColor = (value?: EnableStatus) => { + if (value === 'enabled') { + return 'green' + } + + if (value === 'disabled') { + return 'red' + } + + return getStatusColor(value) +} + +const getAuditRiskColor = (value?: string) => { + if (value === 'approved') { + return 'green' + } + + if (value === 'pending' || value === 'rejected') { + return 'red' + } + + return getStatusColor(value) +} + +const getSaleRiskColor = (value?: string) => { + if (value === 'on') { + return 'processing' + } + + if (value === 'off') { + return 'red' + } + + return getStatusColor(value) +} + +export default function FeaturedGoodsPage() { + const merchantId = useSessionStore(state => state.merchantId) + const [searchForm] = Form.useForm() + const [editorForm] = Form.useForm() + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [page, setPage] = useState(1) + const [size, setSize] = useState(20) + const [total, setTotal] = useState(0) + const [list, setList] = useState([]) + const [editorOpen, setEditorOpen] = useState(false) + const [pickerOpen, setPickerOpen] = useState(false) + const [editingRecord, setEditingRecord] = useState(null) + const [selectedGoods, setSelectedGoods] = useState(null) + + const configuredSpuIds = useMemo(() => list.map(item => item.spuId), [list]) + + const loadList = async (nextPage = page, nextSize = size) => { + if (!merchantId) { + return + } + + try { + setLoading(true) + const values = searchForm.getFieldsValue() + const result = await merchantFeaturedGoodsPage({ + merchantId, + keyword: values.keyword?.trim(), + status: values.status || undefined, + page: nextPage, + size: nextSize, + }) + setList(result.list ?? []) + setTotal(result.total ?? 0) + } catch (error) { + const message = error instanceof Error ? error.message : '加载首页精选商品失败' + toast.error(message) + } finally { + setLoading(false) + } + } + + useEffect(() => { + searchForm.setFieldsValue({ + keyword: '', + status: '', + }) + setPage(1) + void loadList(1, size) + }, [merchantId]) // eslint-disable-line react-hooks/exhaustive-deps + + const handleOpenCreate = () => { + setEditingRecord(null) + setSelectedGoods(null) + editorForm.setFieldsValue({ + sort: 0, + status: 'enabled', + }) + setEditorOpen(true) + } + + const handleOpenEdit = (record: MerchantFeaturedGoodsItem) => { + setEditingRecord(record) + setSelectedGoods({ + id: record.spuId, + spuNo: record.spuNo, + spuName: record.spuName, + subTitle: record.subTitle, + }) + editorForm.setFieldsValue({ + sort: record.sort, + status: record.configStatus, + }) + setEditorOpen(true) + } + + const handleDelete = async (record: MerchantFeaturedGoodsItem) => { + if (!merchantId) { + return + } + + try { + await merchantFeaturedGoodsDelete({ + merchantId, + id: record.id, + }) + toast.success('精选商品已删除') + await loadList() + } catch (error) { + const message = error instanceof Error ? error.message : '删除精选商品失败' + toast.error(message) + } + } + + const handleSave = async () => { + if (!merchantId) { + return + } + + if (!selectedGoods) { + toast.error('请选择商家商品') + return + } + + try { + const values = await editorForm.validateFields() + setSaving(true) + await merchantFeaturedGoodsSave({ + merchantId, + id: editingRecord?.id, + spuId: selectedGoods.id, + sort: values.sort ?? 0, + status: values.status, + }) + toast.success(editingRecord ? '精选商品已更新' : '精选商品已新增') + setEditorOpen(false) + await loadList() + } catch (error) { + const message = error instanceof Error ? error.message : '保存精选商品失败' + toast.error(message) + } finally { + setSaving(false) + } + } + + return ( + + } disabled={!merchantId} onClick={handleOpenCreate}> + 新增精选商品 + + } + > + +
+ + + + + ({ ...item }))} + optionType="button" + buttonStyle="solid" + /> + + + + + + + + +
+
+ + +
{ + setPage(nextPage) + setSize(nextSize) + void loadList(nextPage, nextSize) + }, + }} + columns={[ + { + title: '配置 ID', + dataIndex: 'id', + width: 100, + }, + { + title: '商品 ID', + dataIndex: 'spuId', + width: 100, + }, + { + title: '商品编码', + dataIndex: 'spuNo', + width: 160, + render: value => value || '-', + }, + { + title: '商品名称', + dataIndex: 'spuName', + width: 180, + }, + { + title: '副标题', + dataIndex: 'subTitle', + width: 180, + render: value => value || '-', + }, + { + title: '封面图', + dataIndex: 'coverUrl', + width: 120, + render: value => + value ? ( + + ) : ( + '-' + ), + }, + { + title: '商品状态', + dataIndex: 'goodsStatus', + width: 110, + render: value => {getStatusLabel(value)}, + }, + { + title: '审核状态', + dataIndex: 'auditStatus', + width: 110, + render: value => {getStatusLabel(value)}, + }, + { + title: '上下架状态', + dataIndex: 'saleStatus', + width: 120, + render: value => {getStatusLabel(value)}, + }, + { + title: '配置状态', + dataIndex: 'configStatus', + width: 110, + render: value => {getStatusLabel(value)}, + }, + { + title: '最低价', + dataIndex: 'minPrice', + width: 100, + render: value => value || '-', + }, + { + title: '最高价', + dataIndex: 'maxPrice', + width: 100, + render: value => value || '-', + }, + { + title: '总库存', + dataIndex: 'totalStock', + width: 100, + }, + { + title: '排序', + dataIndex: 'sort', + width: 90, + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + width: 180, + render: value => formatUnixTime(value), + }, + { + title: '操作', + key: 'actions', + width: 140, + fixed: 'right', + render: (_, record) => ( + + + void handleDelete(record)} + > + + + + ), + }, + ]} + /> + + + void handleSave()} + onCancel={() => setEditorOpen(false)} + > + + + 当前页按商家主体维护首页精选商品。新增时提交 merchantId、spuId、sort、status;编辑时会额外带上当前配置 ID。 + + +
+ + + + {selectedGoods ? ( + + spuId: {selectedGoods.id} {selectedGoods.subTitle ? `| ${selectedGoods.subTitle}` : ''} + + ) : null} + + + + + + + + + + ({ ...item }))} /> + + +
+
+ + {merchantId ? ( + setPickerOpen(false)} + onSelect={(goods: GoodsSummary) => { + setSelectedGoods({ + id: goods.id, + spuNo: goods.spuNo, + spuName: goods.spuName, + subTitle: goods.subTitle, + }) + setPickerOpen(false) + }} + /> + ) : null} + + ) +} diff --git a/src/pages/goods/GoodsEditorDrawer.tsx b/src/pages/goods/GoodsEditorDrawer.tsx new file mode 100644 index 0000000..eca8149 --- /dev/null +++ b/src/pages/goods/GoodsEditorDrawer.tsx @@ -0,0 +1,1602 @@ +import { useEffect, useState } from 'react' +import { + Button, + Card, + Cascader, + Drawer, + Form, + Input, + InputNumber, + Select, + Space, + Spin, + Tag, + Typography, +} from 'antd' +import { + ArrowDownOutlined, + ArrowUpOutlined, + DeleteOutlined, + FileTextOutlined, + PictureOutlined, + PlusOutlined, +} from '@ant-design/icons' +import { toast } from 'sonner' +import { + merchantCategoryAttributeList, + merchantCategoryTree, + merchantGoodsDetail, + merchantGoodsSave, +} from '@/apis/mallMerchant' +import ImageUploadField from '@/components/ImageUploadField' +import ImageWallUploadField from '@/components/ImageWallUploadField' +import type { + EnableStatus, + MallCategoryItem, + MerchantCategoryAttributeItem, + MerchantCategoryAttributeValue, + SaleStatus, +} from '@/types/mall' + +interface GoodsEditorDrawerProps { + open: boolean + merchantId: string + spuId?: number | null + onClose: () => void + onSuccess: () => void +} + +interface CategoryCascaderOption { + value: number + label: string + isLeaf?: boolean + loading?: boolean + children?: CategoryCascaderOption[] + category: MallCategoryItem +} + +interface GoodsNameValueFormValue { + name: string + value: string +} + +interface GoodsProductAttributeFormValue { + [attributeId: string]: string[] +} + +interface GoodsSkuSpecFormValue { + attributeId: string + attributeName: string + valueName: string + valueId?: string +} + +interface GoodsServiceFormValue { + title: string + desc?: string +} + +interface GoodsDetailBlockFormValue { + type: 'image' | 'text' + value: string +} + +interface GoodsExtraFieldFormValue { + key: string + value: string +} + +interface GoodsVariantSpecFormValue { + attributeId: string + attributeName: string + draftValue?: string + values: string[] +} + +interface GoodsSkuFormValue { + skuId?: number + skuNo: string + skuName: string + barcode?: string + imageUrl?: string + specs: GoodsSkuSpecFormValue[] + salePrice: string + originPrice?: string + costPrice?: string + weight?: string + status: EnableStatus + stock: number + warnStock: number +} + +interface GoodsFormValues { + categoryId?: number + spuName: string + englishTitle: string + origin: string + goodsNo: string + sensitiveFlag: '0' | '1' + customizeFlag: '0' | '1' + subTitle?: string + coverUrl: string + mediaImages: string[] + mediaVideos: string[] + detailBlocks: GoodsDetailBlockFormValue[] + variantSpecs: GoodsVariantSpecFormValue[] + productAttributes: GoodsProductAttributeFormValue + services: GoodsServiceFormValue[] + extraSource?: string + extraTags: string[] + extraFields: GoodsExtraFieldFormValue[] + status: EnableStatus + saleStatus: SaleStatus + sort?: number + skus: GoodsSkuFormValue[] +} + +const emptyNameValue = (): GoodsNameValueFormValue => ({ + name: '', + value: '', +}) + +const emptyDetailBlock = (type: GoodsDetailBlockFormValue['type'] = 'image'): GoodsDetailBlockFormValue => ({ + type, + value: '', +}) + +const emptyVariantSpec = (attribute?: MerchantCategoryAttributeItem): GoodsVariantSpecFormValue => ({ + attributeId: attribute ? String(attribute.attributeId) : '', + attributeName: attribute?.attributeName ?? '', + draftValue: '', + values: [], +}) + +const defaultSku = (): GoodsSkuFormValue => ({ + skuId: 0, + skuNo: '', + skuName: '', + barcode: '', + imageUrl: '', + specs: [], + salePrice: '', + originPrice: '', + costPrice: '', + weight: '', + status: 'enabled', + stock: 0, + warnStock: 0, +}) + +const defaultValues = (): GoodsFormValues => ({ + categoryId: undefined, + spuName: '', + englishTitle: '', + origin: '', + goodsNo: '', + sensitiveFlag: '0', + customizeFlag: '0', + subTitle: '', + coverUrl: '', + mediaImages: [''], + mediaVideos: [], + detailBlocks: [], + variantSpecs: [], + productAttributes: {}, + services: [], + extraSource: 'merchant_manual', + extraTags: [], + extraFields: [], + status: 'enabled', + saleStatus: 'off', + sort: 0, + skus: [defaultSku()], +}) + +const normalizeString = (value: unknown) => { + if (typeof value === 'string') { + return value + } + + if (value === undefined || value === null) { + return '' + } + + return String(value) +} + +const safeJsonParse = (value?: string) => { + if (!value?.trim()) { + return null + } + + try { + return JSON.parse(value) + } catch { + return null + } +} + +const normalizeStringList = (values: unknown) => { + if (!Array.isArray(values)) { + return [] + } + + return values.map(item => normalizeString(item).trim()).filter(Boolean) +} + +const normalizeUploadedImageList = (values?: string[]) => { + if (!Array.isArray(values)) { + return [] + } + + return values.map(item => item.trim()).filter(Boolean) +} + +const normalizeBoolean = (value: unknown) => value === true || value === 1 || value === '1' + +const parseNameValueList = (values: unknown) => { + if (!Array.isArray(values)) { + return [] + } + + return values + .map(item => ({ + name: normalizeString((item as Record)?.name), + value: normalizeString((item as Record)?.value), + })) + .filter(item => item.name || item.value) +} + +const parseProductAttributeValues = (values: unknown): GoodsProductAttributeFormValue => { + if (!Array.isArray(values)) { + return {} + } + + return values.reduce((result, item) => { + const record = item as Record + const attributeId = normalizeString(record.attributeId).trim() + const selectedValues = Array.isArray(record.values) ? record.values : [] + + if (!attributeId) { + return result + } + + result[attributeId] = selectedValues + .map(valueItem => { + const valueRecord = valueItem as Record + return ( + normalizeString(valueRecord.valueId).trim() || + normalizeString(valueRecord.valueName).trim() + ) + }) + .filter(Boolean) + + return result + }, {}) +} + +const parseSkuSpecList = (values: unknown): GoodsSkuSpecFormValue[] => { + if (!Array.isArray(values)) { + return [] + } + + return values + .map(item => { + const record = item as Record + const attributeName = + normalizeString(record.attributeName).trim() || normalizeString(record.name).trim() + const valueName = + normalizeString(record.valueName).trim() || normalizeString(record.value).trim() + + return { + attributeId: normalizeString(record.attributeId).trim(), + attributeName, + valueName, + valueId: normalizeString(record.valueId).trim(), + } + }) + .filter(item => item.attributeName || item.valueName) +} + +const parseServiceList = (values: unknown) => { + if (!Array.isArray(values)) { + return [] + } + + return values + .map(item => ({ + title: normalizeString((item as Record)?.title), + desc: normalizeString((item as Record)?.desc), + })) + .filter(item => item.title || item.desc) +} + +const parseDetailBlocks = (value?: string) => { + const parsed = safeJsonParse(value) + const blocks = Array.isArray((parsed as Record | null)?.blocks) + ? ((parsed as Record).blocks as unknown[]) + : [] + + return blocks + .map(item => ({ + type: (item as Record)?.type === 'text' ? 'text' : 'image', + value: normalizeString((item as Record)?.value), + })) + .filter(item => item.value) +} + +const parseExtraInfo = (value?: string) => { + const parsed = safeJsonParse(value) + const source = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? normalizeString((parsed as Record).source) + : '' + const tags = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? normalizeStringList((parsed as Record).tags) + : [] + + const englishTitle = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? normalizeString((parsed as Record).englishTitle) + : '' + const origin = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? normalizeString((parsed as Record).origin) + : '' + const goodsNo = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? normalizeString((parsed as Record).goodsNo) + : '' + const sensitiveFlag: '0' | '1' = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? normalizeString((parsed as Record).sensitiveFlag) === '1' + ? '1' + : '0' + : '0' + const customizeFlag: '0' | '1' = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? normalizeString((parsed as Record).customizeFlag) === '1' + ? '1' + : '0' + : '0' + + const extraFields = + parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? Object.entries(parsed as Record) + .filter( + ([key]) => + key !== 'source' && + key !== 'tags' && + key !== 'englishTitle' && + key !== 'origin' && + key !== 'goodsNo' && + key !== 'sensitiveFlag' && + key !== 'customizeFlag' + ) + .map(([key, fieldValue]) => ({ + key, + value: + typeof fieldValue === 'string' + ? fieldValue + : fieldValue === undefined || fieldValue === null + ? '' + : JSON.stringify(fieldValue), + })) + : [] + + return { + source, + tags, + englishTitle, + origin, + goodsNo, + sensitiveFlag, + customizeFlag, + extraFields, + } +} + +const ensureStringRows = (values: string[]) => (values.length ? values : ['']) + +const toCategoryOptions = (list: MallCategoryItem[]): CategoryCascaderOption[] => + list.map(item => { + const hasChildren = normalizeBoolean(item.hasChildren) || !normalizeBoolean(item.isLeaf) + + return { + value: Number(item.id), + label: item.categoryName, + isLeaf: normalizeBoolean(item.isLeaf), + children: hasChildren && !normalizeBoolean(item.isLeaf) ? [] : undefined, + category: item, + } + }) + +const getAttributeId = (attribute: Pick) => + String(attribute.attributeId) + +const getAttributeValueKey = (value: MerchantCategoryAttributeValue) => + normalizeString(value.valueId).trim() || normalizeString(value.valueName).trim() + +const getAttributeValueName = (value: MerchantCategoryAttributeValue) => + normalizeString(value.valueName).trim() + +const HIDDEN_CATEGORY_ATTRIBUTE_IDS = new Set(['100701']) +const HIDDEN_CATEGORY_ATTRIBUTE_NAMES = new Set([ + '是否含有化学物质', + '化学物质', + '是否含有致癌物质', + '致癌物质', +]) + +const shouldDisplayCategoryAttribute = (attribute: MerchantCategoryAttributeItem) => { + const attributeId = getAttributeId(attribute) + return ( + !HIDDEN_CATEGORY_ATTRIBUTE_IDS.has(attributeId) && + !HIDDEN_CATEGORY_ATTRIBUTE_NAMES.has(attribute.attributeName) + ) +} + +const isProductAttribute = (attribute: MerchantCategoryAttributeItem) => + shouldDisplayCategoryAttribute(attribute) && attribute.attributeType === 'PRODUCT_PROPERTY' + +const isSalesAttribute = (attribute: MerchantCategoryAttributeItem) => + shouldDisplayCategoryAttribute(attribute) && attribute.attributeType === 'SALES_PROPERTY' + +const normalizeVariantSpecs = (values?: GoodsVariantSpecFormValue[]) => { + if (!Array.isArray(values)) { + return [] + } + + return values + .map(item => { + const seen = new Set() + const normalizedValues = (item.values ?? []) + .map(value => value.trim()) + .filter(value => { + if (!value || seen.has(value)) { + return false + } + seen.add(value) + return true + }) + + return { + attributeId: normalizeString(item.attributeId).trim(), + attributeName: normalizeString(item.attributeName).trim(), + draftValue: item.draftValue ?? '', + values: normalizedValues, + } + }) + .filter(item => item.attributeName || item.values.length) +} + +const buildVariantSpecsFromSkus = (skus: GoodsSkuFormValue[]) => { + const specsMap = new Map() + + skus.forEach(sku => { + (sku.specs ?? []).forEach(spec => { + const attributeName = spec.attributeName.trim() + const valueName = spec.valueName.trim() + if (!attributeName || !valueName) { + return + } + + const key = spec.attributeId || attributeName + if (!specsMap.has(key)) { + specsMap.set(key, { + attributeId: spec.attributeId, + attributeName, + draftValue: '', + values: [], + }) + } + + const targetSpec = specsMap.get(key) + if (targetSpec && !targetSpec.values.includes(valueName)) { + targetSpec.values.push(valueName) + } + }) + }) + + const specs = Array.from(specsMap.values()) + + return specs.length ? specs : [] +} + +const buildVariantSpecsFromAttributes = ( + attributes: MerchantCategoryAttributeItem[], + previousSkus: GoodsSkuFormValue[] = [], + previousSpecs: GoodsVariantSpecFormValue[] = [] +) => { + const previousSpecMap = new Map( + normalizeVariantSpecs(previousSpecs).map(item => [item.attributeId || item.attributeName, item]) + ) + const skuSpecMap = new Map( + buildVariantSpecsFromSkus(previousSkus).map(item => [item.attributeId || item.attributeName, item]) + ) + + return attributes.filter(isSalesAttribute).map(attribute => { + const attributeId = getAttributeId(attribute) + const previousSpec = + previousSpecMap.get(attributeId) || + previousSpecMap.get(attribute.attributeName) || + skuSpecMap.get(attributeId) || + skuSpecMap.get(attribute.attributeName) + + return { + attributeId, + attributeName: attribute.attributeName, + draftValue: '', + values: previousSpec?.values ?? [], + } + }) +} + +const getSkuSpecKey = (specs: GoodsSkuSpecFormValue[] = []) => { + return specs + .map(item => `${item.attributeId || item.attributeName.trim()}:${item.valueName.trim()}`) + .join('|') +} + +const buildSpecCombinations = (variantSpecs: GoodsVariantSpecFormValue[]) => { + const activeSpecs = normalizeVariantSpecs(variantSpecs).filter( + item => item.attributeName && item.values.length + ) + + if (!activeSpecs.length) { + return [[]] + } + + return activeSpecs.reduce( + (result, spec) => + result.flatMap(existing => + spec.values.map(valueName => [ + ...existing, + { + attributeId: spec.attributeId, + attributeName: spec.attributeName, + valueName, + }, + ]) + ), + [[]] + ) +} + +const buildSkusFromVariantSpecs = ( + variantSpecs: GoodsVariantSpecFormValue[], + previousSkus: GoodsSkuFormValue[] +) => { + const previousMap = new Map() + previousSkus.forEach(sku => { + previousMap.set(getSkuSpecKey(sku.specs), sku) + }) + + return buildSpecCombinations(variantSpecs).map((specs, index) => { + const matchedSku = previousMap.get(getSkuSpecKey(specs)) + const previousSku = matchedSku ?? previousSkus[index] + const specName = specs.map(item => item.valueName).join('-') + + return { + ...defaultSku(), + ...previousSku, + skuId: matchedSku?.skuId ?? 0, + skuName: previousSku?.skuName?.trim() || specName || '默认规格', + specs, + } + }) +} + +const normalizePairRows = (values: GoodsNameValueFormValue[], label: string) => { + return values.reduce((result, item, index) => { + const name = item.name.trim() + const value = item.value.trim() + + if (!name && !value) { + return result + } + + if (!name || !value) { + throw new Error(`${label} 第 ${index + 1} 行请填写完整`) + } + + result.push({ name, value }) + return result + }, []) +} + +const normalizeServiceRows = (values: GoodsServiceFormValue[] = []) => { + return values.reduce((result, item, index) => { + const title = item.title.trim() + const desc = item.desc?.trim() ?? '' + + if (!title && !desc) { + return result + } + + if (!title) { + throw new Error(`服务承诺第 ${index + 1} 行请填写标题`) + } + + result.push({ title, desc }) + return result + }, []) +} + +const normalizeDetailRows = (values: GoodsDetailBlockFormValue[] = []) => { + return values.reduce((result, item) => { + const value = item.value.trim() + + if (!value) { + return result + } + + result.push({ + type: item.type === 'text' ? 'text' : 'image', + value, + }) + return result + }, []) +} + +const normalizeExtraFields = (values: GoodsExtraFieldFormValue[] = []) => { + return values.reduce>((result, item, index) => { + const key = item.key.trim() + const value = item.value.trim() + + if (!key && !value) { + return result + } + + if (!key) { + throw new Error(`扩展信息第 ${index + 1} 行请填写字段名`) + } + + if (key === 'source' || key === 'tags') { + throw new Error(`扩展字段名 ${key} 为保留字段,请换一个名字`) + } + + result[key] = value + return result + }, {}) +} + +const buildMediaJson = (images: string[] = [], videos: string[] = []) => { + return JSON.stringify({ + images: normalizeUploadedImageList(images), + videos: videos.map(item => item.trim()).filter(Boolean), + }) +} + +const buildDetailJson = (blocks: GoodsDetailBlockFormValue[] = []) => { + return JSON.stringify({ + blocks: normalizeDetailRows(blocks), + }) +} + +const normalizeProductAttributeValue = (value?: string[]) => { + if (!Array.isArray(value)) { + return [] + } + + return value.map(item => item.trim()).filter(Boolean) +} + +const buildProductAttributeJson = ( + productAttributes: GoodsProductAttributeFormValue = {}, + attributeTemplates: MerchantCategoryAttributeItem[] = [] +) => { + const params = attributeTemplates.filter(isProductAttribute).reduce< + Array<{ + attributeId: string + attributeName: string + values: Array<{ valueId: string; valueName: string }> + }> + >((result, attribute) => { + const attributeId = getAttributeId(attribute) + const selectedValues = normalizeProductAttributeValue(productAttributes[attributeId]) + + if (!selectedValues.length) { + return result + } + + result.push({ + attributeId, + attributeName: attribute.attributeName, + values: selectedValues.map(selectedValue => { + const matchedValue = attribute.values?.find(value => getAttributeValueKey(value) === selectedValue) + return { + valueId: matchedValue ? normalizeString(matchedValue.valueId).trim() : '', + valueName: matchedValue ? getAttributeValueName(matchedValue) : selectedValue, + } + }), + }) + + return result + }, []) + + return JSON.stringify(params) +} + +const buildServiceJson = (services: GoodsServiceFormValue[] = []) => { + return JSON.stringify(normalizeServiceRows(services)) +} + +const buildExtraInfoJson = ( + englishTitle = '', + origin = '', + goodsNo = '', + sensitiveFlag: '0' | '1' = '0', + customizeFlag: '0' | '1' = '0', + source: string | undefined, + tags: string[] = [], + extraFields: GoodsExtraFieldFormValue[] = [] +) => { + const result: Record = { + ...normalizeExtraFields(extraFields), + } + + const normalizedEnglishTitle = englishTitle.trim() + const normalizedOrigin = origin.trim() + const normalizedGoodsNo = goodsNo.trim() + const normalizedSource = source?.trim() + const normalizedTags = tags.map(item => item.trim()).filter(Boolean) + + if (normalizedEnglishTitle) { + result.englishTitle = normalizedEnglishTitle + } + + if (normalizedOrigin) { + result.origin = normalizedOrigin + } + + if (normalizedGoodsNo) { + result.goodsNo = normalizedGoodsNo + } + + result.sensitiveFlag = sensitiveFlag + result.customizeFlag = customizeFlag + + if (normalizedSource) { + result.source = normalizedSource + } + + if (normalizedTags.length) { + result.tags = normalizedTags + } + + return JSON.stringify(result) +} + +const buildSpecJson = (specs: GoodsSkuSpecFormValue[] = []) => { + return JSON.stringify( + (specs ?? []) + .map(spec => ({ + attributeId: spec.attributeId.trim(), + attributeName: spec.attributeName.trim(), + valueId: spec.valueId?.trim() || undefined, + valueName: spec.valueName.trim(), + })) + .filter(spec => spec.attributeName && spec.valueName) + ) +} + +export default function GoodsEditorDrawer({ + open, + merchantId, + spuId, + onClose, + onSuccess, +}: GoodsEditorDrawerProps) { + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [categoryLoading, setCategoryLoading] = useState(false) + const [attributeLoading, setAttributeLoading] = useState(false) + const [categoryOptions, setCategoryOptions] = useState([]) + const [categoryPathValue, setCategoryPathValue] = useState([]) + const [selectedCategoryPath, setSelectedCategoryPath] = useState('') + const [categoryAttributes, setCategoryAttributes] = useState([]) + const variantSpecs = Form.useWatch('variantSpecs', form) ?? [] + const watchedSkus = Form.useWatch('skus', form) ?? [] + const skuSpecColumns = normalizeVariantSpecs(variantSpecs).filter(item => item.attributeName) + const productAttributeTemplates = categoryAttributes.filter(isProductAttribute) + + const syncSkusByVariantSpecs = (nextVariantSpecs: GoodsVariantSpecFormValue[]) => { + const currentSkus = (form.getFieldValue('skus') ?? []) as GoodsSkuFormValue[] + form.setFieldsValue({ + variantSpecs: nextVariantSpecs, + skus: buildSkusFromVariantSpecs(nextVariantSpecs, currentSkus), + }) + } + + const loadRootCategories = async () => { + try { + setCategoryLoading(true) + const result = await merchantCategoryTree({ parentId: 0 }) + setCategoryOptions(toCategoryOptions(result.list ?? [])) + } catch (error) { + const message = error instanceof Error ? error.message : '加载商品分类失败' + toast.error(message) + } finally { + setCategoryLoading(false) + } + } + + const loadCategoryAttributes = async ( + categoryId: number, + options?: { + preserveSkus?: boolean + resetProductAttributes?: boolean + fallbackPath?: string + } + ) => { + try { + setAttributeLoading(true) + const result = await merchantCategoryAttributeList({ + categoryId, + sourcePlatform: 'tiktok', + sourceKey: 'local_v2', + status: 'enabled', + }) + const attributes = result.list ?? [] + const currentSkus = options?.preserveSkus + ? ((form.getFieldValue('skus') ?? []) as GoodsSkuFormValue[]) + : [] + const currentVariantSpecs = options?.preserveSkus + ? ((form.getFieldValue('variantSpecs') ?? []) as GoodsVariantSpecFormValue[]) + : [] + const nextVariantSpecs = buildVariantSpecsFromAttributes( + attributes, + currentSkus, + currentVariantSpecs + ) + + setCategoryAttributes(attributes) + setSelectedCategoryPath(options?.fallbackPath || result.categoryName || String(categoryId)) + form.setFieldsValue({ + ...(options?.resetProductAttributes ? { productAttributes: {} } : {}), + variantSpecs: nextVariantSpecs, + skus: buildSkusFromVariantSpecs(nextVariantSpecs, currentSkus), + }) + } catch (error) { + setCategoryAttributes([]) + const message = error instanceof Error ? error.message : '加载分类属性失败' + toast.error(message) + } finally { + setAttributeLoading(false) + } + } + + const handleCategoryLoadData = (selectedOptions: CategoryCascaderOption[]) => { + const targetOption = selectedOptions[selectedOptions.length - 1] + if (!targetOption || targetOption.isLeaf) { + return + } + + targetOption.loading = true + setCategoryOptions([...categoryOptions]) + + merchantCategoryTree({ parentId: Number(targetOption.value) }) + .then(result => { + targetOption.loading = false + targetOption.children = toCategoryOptions(result.list ?? []) + setCategoryOptions([...categoryOptions]) + }) + .catch(error => { + targetOption.loading = false + setCategoryOptions([...categoryOptions]) + const message = error instanceof Error ? error.message : '加载下级分类失败' + toast.error(message) + }) + } + + const handleCategoryChange = ( + value: Array, + selectedOptions?: CategoryCascaderOption[] + ) => { + const leafOption = selectedOptions?.[selectedOptions.length - 1] + const categoryPath = selectedOptions?.map(item => item.label).join(' / ') ?? '' + + if (!leafOption?.isLeaf) { + form.setFieldValue('categoryId', undefined) + setCategoryPathValue([]) + setSelectedCategoryPath('') + setCategoryAttributes([]) + return + } + + const categoryId = Number(leafOption.value) + form.setFieldsValue({ + categoryId, + productAttributes: {}, + variantSpecs: [], + skus: [defaultSku()], + }) + setCategoryPathValue(value.map(item => Number(item))) + setSelectedCategoryPath(categoryPath) + void loadCategoryAttributes(categoryId, { + resetProductAttributes: true, + fallbackPath: categoryPath, + }) + } + + const handleAddSpecValue = (groupIndex: number) => { + const currentSpecs = (form.getFieldValue('variantSpecs') ?? []) as GoodsVariantSpecFormValue[] + const targetSpec = currentSpecs[groupIndex] ?? emptyVariantSpec() + const attributeName = targetSpec.attributeName?.trim() + const draftValue = targetSpec.draftValue?.trim() + + if (!attributeName) { + toast.error('请先选择分类并加载销售属性') + return + } + + if (!draftValue) { + toast.error('请输入规格值') + return + } + + const values = Array.from(new Set([...(targetSpec.values ?? []), draftValue])) + const nextSpecs = currentSpecs.map((item, index) => + index === groupIndex + ? { + ...item, + attributeName, + draftValue: '', + values, + } + : item + ) + + syncSkusByVariantSpecs(nextSpecs) + } + + const handleRemoveSpecValue = (groupIndex: number, value: string) => { + const currentSpecs = (form.getFieldValue('variantSpecs') ?? []) as GoodsVariantSpecFormValue[] + const nextSpecs = currentSpecs.map((item, index) => + index === groupIndex + ? { + ...item, + values: (item.values ?? []).filter(currentValue => currentValue !== value), + } + : item + ) + + syncSkusByVariantSpecs(nextSpecs) + } + + useEffect(() => { + if (!open) { + return + } + + form.resetFields() + setCategoryPathValue([]) + setSelectedCategoryPath('') + setCategoryAttributes([]) + void loadRootCategories() + + if (!spuId) { + form.setFieldsValue(defaultValues()) + return + } + + let cancelled = false + const run = async () => { + try { + setLoading(true) + const result = await merchantGoodsDetail({ + merchantId, + spuId, + }) + + if (cancelled) { + return + } + + const data = result.data + const media = safeJsonParse(data.mediaJson) as Record | null + const mediaImages = normalizeStringList(media?.images) + const detailBlocks = parseDetailBlocks(data.detailJson) + const productAttributes = parseProductAttributeValues(safeJsonParse(data.paramJson)) + const services = parseServiceList(safeJsonParse(data.serviceJson)) + const extraInfo = parseExtraInfo(data.extraInfoJson) + const skus = data.skus.length + ? data.skus.map(sku => ({ + skuId: sku.id, + skuNo: sku.skuNo, + skuName: sku.skuName, + barcode: sku.barcode, + imageUrl: sku.imageUrl, + specs: parseSkuSpecList(safeJsonParse(sku.specJson)), + salePrice: sku.salePrice, + originPrice: sku.originPrice, + costPrice: sku.costPrice, + weight: sku.weight, + status: sku.status, + stock: sku.stock, + warnStock: sku.warnStock, + })) + : [defaultSku()] + + form.setFieldsValue({ + categoryId: data.categoryId, + spuName: data.spuName, + englishTitle: extraInfo.englishTitle, + origin: extraInfo.origin, + goodsNo: extraInfo.goodsNo, + sensitiveFlag: extraInfo.sensitiveFlag, + customizeFlag: extraInfo.customizeFlag, + subTitle: data.subTitle, + coverUrl: data.coverUrl, + mediaImages: ensureStringRows(mediaImages.length ? mediaImages : data.coverUrl ? [data.coverUrl] : []), + mediaVideos: normalizeStringList(media?.videos), + detailBlocks, + variantSpecs: buildVariantSpecsFromSkus(skus), + productAttributes, + services, + extraSource: extraInfo.source, + extraTags: extraInfo.tags, + extraFields: extraInfo.extraFields, + status: data.status, + saleStatus: data.saleStatus, + sort: data.sort, + skus, + }) + setSelectedCategoryPath(String(data.categoryId)) + void loadCategoryAttributes(data.categoryId, { + preserveSkus: true, + fallbackPath: String(data.categoryId), + }) + } catch (error) { + const message = error instanceof Error ? error.message : '加载商品详情失败' + toast.error(message) + } finally { + if (!cancelled) { + setLoading(false) + } + } + } + + void run() + return () => { + cancelled = true + } + }, [form, merchantId, open, spuId]) + + const handleSave = async () => { + try { + const values = await form.validateFields() + setSaving(true) + const mediaImages = normalizeUploadedImageList(values.mediaImages) + const skuValues = Array.isArray(values.skus) && values.skus.length ? values.skus : [defaultSku()] + + if (!mediaImages.length) { + throw new Error('请至少上传 1 张产品轮播图') + } + + if (!values.categoryId) { + throw new Error('请选择叶子商品分类') + } + + await merchantGoodsSave({ + merchantId, + spuId: spuId ?? 0, + categoryId: Number(values.categoryId), + spuName: values.spuName.trim(), + subTitle: values.subTitle?.trim() || undefined, + coverUrl: mediaImages[0], + mediaJson: buildMediaJson(mediaImages, values.mediaVideos ?? []), + detailJson: buildDetailJson(values.detailBlocks ?? []), + paramJson: buildProductAttributeJson(values.productAttributes ?? {}, categoryAttributes), + serviceJson: buildServiceJson(values.services ?? []), + extraInfoJson: buildExtraInfoJson( + values.englishTitle ?? '', + values.origin ?? '', + values.goodsNo ?? '', + values.sensitiveFlag ?? '0', + values.customizeFlag ?? '0', + values.extraSource, + values.extraTags ?? [], + values.extraFields ?? [] + ), + status: values.status, + saleStatus: values.saleStatus, + sort: Number(values.sort ?? 0), + skus: skuValues.map((sku, index) => { + const imageUrl = sku.imageUrl?.trim() ?? '' + const specs = sku.specs ?? [] + + if (!imageUrl) { + throw new Error(`请上传 SKU ${index + 1} 的主图`) + } + + return { + skuId: Number(sku.skuId ?? 0), + skuNo: sku.skuNo?.trim() ?? '', + skuName: sku.skuName?.trim() || specs.map(spec => spec.valueName).join('-') || '默认规格', + barcode: sku.barcode?.trim() ?? '', + imageUrl, + specJson: buildSpecJson(specs), + salePrice: sku.salePrice?.trim() ?? '', + originPrice: sku.originPrice?.trim() ?? '', + costPrice: sku.costPrice?.trim() ?? '', + weight: sku.weight?.trim() ?? '', + status: sku.status, + stock: Number(sku.stock ?? 0), + warnStock: Number(sku.warnStock ?? 0), + } + }), + }) + + toast.success(spuId ? '商品已更新' : '商品已创建') + onSuccess() + onClose() + } catch (error) { + const message = error instanceof Error ? error.message : '保存商品失败' + toast.error(message) + } finally { + setSaving(false) + } + } + + return ( + + + + + } + > + +
+ + +
+
+
+ * + 产品分类: +
+
+
+ + +
+
+ {selectedCategoryPath || '请选择叶子类目'} +
+
+
+
+ +
+ + +
+
+
+ * + 产品标题: +
+
+ + + +
+
+ +
+
+ * + 产品轮播图: +
+
+ + + +
+
+
+ + + + + + + + + + + +
+ + + + {productAttributeTemplates.length ? ( +
+ {productAttributeTemplates.map(attribute => { + const attributeId = getAttributeId(attribute) + const isMultiple = normalizeBoolean(attribute.isMultipleSelection) + + return ( +
+
+ {normalizeBoolean(attribute.isRequired) ? ( + * + ) : null} + {attribute.attributeName}: +
+
+ { + const normalizedValue = normalizeProductAttributeValue(value) + return { + value: isMultiple ? normalizedValue : normalizedValue[0], + } + }} + getValueFromEvent={(value: string | string[] | undefined) => + Array.isArray(value) ? value : value ? [value] : [] + } + rules={[ + { + validator: async (_, value?: string[]) => { + if ( + !normalizeBoolean(attribute.isRequired) || + normalizeProductAttributeValue(value).length + ) { + return + } + + throw new Error(`请选择${attribute.attributeName}`) + }, + }, + ]} + > + + + +
+
{currentSpec.attributeName}
+ + { + event.preventDefault() + handleAddSpecValue(index) + }} + /> + + +
+
+ {(currentSpec.values ?? []).map(value => ( + { + event.preventDefault() + handleRemoveSpecValue(index, value) + }} + > + {value} + + ))} +
+
+ ) + }) + ) : ( +
+ {selectedCategoryPath ? '当前分类暂无销售属性,将生成默认 SKU' : '请先选择叶子商品分类'} +
+ )} +
+ )} + + + + SKU 信息 + + +
+
+ + + + {skuSpecColumns.map(spec => ( + + ))} + + + + + + + + + + + + {fields => ( + + {fields.map(field => { + const currentSku = watchedSkus?.[field.name] ?? defaultSku() + return ( + + + {skuSpecColumns.map(spec => ( + + ))} + + + + + + + + + + ) + })} + + )} + +
预览图*{spec.attributeName}SKU 编号销售价*原价成本价库存*预警库存*重量状态*
+ + + + + + + + {currentSku.specs?.find( + item => + (spec.attributeId && item.attributeId === spec.attributeId) || + item.attributeName === spec.attributeName + )?.valueName || '-'} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + {(fields, { add, remove, move }) => ( +
+
+ {fields.length ? ( + fields.map((field, index) => ( +
+
+ + {() => + form.getFieldValue(['detailBlocks', field.name, 'type']) === 'text' ? ( + + + + ) : ( + + + + ) + } + +
+
+
+
+ )) + ) : ( +
点击右侧按钮,开始添加产品详情
+ )} +
+ +
+ + +
+
+ )} +
+
+
+ + + + ) +} diff --git a/src/pages/goods/HqbImportModal.tsx b/src/pages/goods/HqbImportModal.tsx new file mode 100644 index 0000000..6cb72e4 --- /dev/null +++ b/src/pages/goods/HqbImportModal.tsx @@ -0,0 +1,285 @@ +import { useEffect, useState } from 'react' +import { + Alert, + Button, + Descriptions, + Empty, + Image, + Input, + Modal, + Space, + Table, + Tag, + Typography, +} from 'antd' +import { CloudDownloadOutlined, LinkOutlined } from '@ant-design/icons' +import { toast } from 'sonner' +import { fetchHqbGoodsInfo } from '@/apis/hqb' +import { merchantGoodsSave } from '@/apis/mallMerchant' +import { + buildMallGoodsFromHqb, + extractHqbGoodsId, + type HqbImportPreview, +} from '@/utils/hqbImport' +import type { MerchantGoodsSkuInput } from '@/types/mall' + +interface HqbImportModalProps { + open: boolean + merchantId: string + onClose: () => void + onSuccess: () => void +} + +const parseSpecNames = (specJson: string) => { + try { + const specs = JSON.parse(specJson) as Array<{ attributeName?: string; valueName?: string }> + return specs.map(item => item.valueName).filter(Boolean).join(' / ') || '-' + } catch { + return '-' + } +} + +export default function HqbImportModal({ open, merchantId, onClose, onSuccess }: HqbImportModalProps) { + const [sourceText, setSourceText] = useState('') + const [preview, setPreview] = useState(null) + const [parsing, setParsing] = useState(false) + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (!open) { + setPreview(null) + setParsing(false) + setSaving(false) + } + }, [open]) + + const handleParse = async () => { + const goodsId = extractHqbGoodsId(sourceText) + if (!goodsId) { + toast.error('请输入 52hqb 商品详情链接,例如 https://www.52hqb.com/good/12432863') + return + } + + try { + setParsing(true) + const goods = await fetchHqbGoodsInfo(goodsId) + setPreview(buildMallGoodsFromHqb(goods, merchantId, `https://www.52hqb.com/good/${goodsId}`)) + toast.success('商品解析完成') + } catch (error) { + const message = error instanceof Error ? error.message : '商品解析失败' + toast.error(message) + } finally { + setParsing(false) + } + } + + const handleSave = async () => { + if (!merchantId) { + toast.error('请先登录商家账号') + return + } + + if (!preview) { + await handleParse() + return + } + + try { + setSaving(true) + await merchantGoodsSave({ + ...preview.payload, + merchantId, + }) + toast.success('商品已创建,默认进入待审核/下架状态') + onSuccess() + onClose() + } catch (error) { + const message = error instanceof Error ? error.message : '创建商品失败' + toast.error(message) + } finally { + setSaving(false) + } + } + + return ( + + + 52hqb 链接上品 + + } + open={open} + width={920} + onCancel={onClose} + footer={ + + + + + + } + destroyOnClose + > + + + +
+ 商品详情链接 + { + setSourceText(event.target.value) + setPreview(null) + }} + /> +
+ + {preview ? ( + + + {preview.categoryName} + 1126672 + + ), + }, + { + key: 'sourceCategory', + label: '源站分类', + children: preview.sourceCategoryPath || '-', + }, + { + key: 'shopName', + label: '源店铺', + children: preview.shopName || '-', + }, + { + key: 'imageCount', + label: '轮播图', + children: `${preview.imageUrls.length} 张`, + }, + { + key: 'skuCount', + label: 'SKU', + children: `${preview.skuCount} 个`, + }, + ]} + /> + +
+
图片预览
+
+ {preview.imageUrls.slice(0, 8).map(url => ( + + ))} +
+
+ +
+
SKU 预览
+ + rowKey={(record, index) => `${record.skuNo}-${index}`} + size="small" + dataSource={preview.payload.skus} + pagination={false} + scroll={{ y: 240 }} + columns={[ + { + title: '图片', + dataIndex: 'imageUrl', + width: 74, + render: value => + value ? ( + + ) : ( + '-' + ), + }, + { + title: '规格', + dataIndex: 'specJson', + render: value => parseSpecNames(value), + }, + { + title: 'SKU 货号', + dataIndex: 'skuNo', + ellipsis: true, + }, + { + title: '售价', + dataIndex: 'salePrice', + width: 90, + }, + { + title: '库存', + dataIndex: 'stock', + width: 80, + }, + ]} + /> +
+
+ ) : ( + + )} + +
+ ) +} diff --git a/src/pages/goods/index.tsx b/src/pages/goods/index.tsx new file mode 100644 index 0000000..d7c9db6 --- /dev/null +++ b/src/pages/goods/index.tsx @@ -0,0 +1,329 @@ +import { useEffect, useState } from 'react' +import { + Button, + Card, + Form, + Image, + Input, + Select, + Space, + Table, + TreeSelect, + Tag, + Typography, +} from 'antd' +import { LinkOutlined, PlusOutlined } from '@ant-design/icons' +import { toast } from 'sonner' +import { merchantGoodsPage, merchantGoodsPublish } from '@/apis/mallMerchant' +import GoodsEditorDrawer from './GoodsEditorDrawer' +import HqbImportModal from './HqbImportModal' +import { useMallCategoryTree } from '@/hooks/useMallCategoryTree' +import { useSessionStore } from '@/store/session' +import type { AuditStatus, GoodsSummary, SaleStatus } from '@/types/mall' +import { + AUDIT_STATUS_OPTIONS, + SALE_STATUS_OPTIONS, + formatUnixTime, + getStatusColor, + getStatusLabel, +} from '@/utils/mall' + +interface GoodsSearchValues { + categoryId?: number + keyword?: string + auditStatus?: AuditStatus | '' + saleStatus?: SaleStatus | '' +} + +export default function GoodsPage() { + const merchantId = useSessionStore(state => state.merchantId) + const { filterTreeData, loading: categoryLoading } = useMallCategoryTree() + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [size, setSize] = useState(20) + const [total, setTotal] = useState(0) + const [list, setList] = useState([]) + const [drawerOpen, setDrawerOpen] = useState(false) + const [importOpen, setImportOpen] = useState(false) + const [editingRecord, setEditingRecord] = useState(null) + + const loadList = async (nextPage = page, nextSize = size) => { + if (!merchantId) { + return + } + + try { + setLoading(true) + const values = form.getFieldsValue() + const result = await merchantGoodsPage({ + merchantId, + categoryId: values.categoryId ? Number(values.categoryId) : undefined, + keyword: values.keyword?.trim(), + auditStatus: values.auditStatus || undefined, + saleStatus: values.saleStatus || undefined, + page: nextPage, + size: nextSize, + }) + setList(result.list ?? []) + setTotal(result.total ?? 0) + } catch (error) { + const message = error instanceof Error ? error.message : '加载商品列表失败' + toast.error(message) + } finally { + setLoading(false) + } + } + + useEffect(() => { + form.setFieldsValue({ + categoryId: undefined, + keyword: '', + auditStatus: '', + saleStatus: '', + }) + void loadList(1, size) + setPage(1) + }, [merchantId]) // eslint-disable-line react-hooks/exhaustive-deps + + const handlePublish = async (record: GoodsSummary) => { + try { + await merchantGoodsPublish({ + merchantId: merchantId ?? '', + spuId: record.id, + saleStatus: record.saleStatus === 'on' ? 'off' : 'on', + }) + toast.success(`商品已${record.saleStatus === 'on' ? '下架' : '上架'}`) + await loadList() + } catch (error) { + const message = error instanceof Error ? error.message : '更新上下架状态失败' + toast.error(message) + } + } + + return ( + + + + + + } + > + +
+ + + + + ({ ...item }))} + /> + + + + + + + + + + +
+
+ + + + { + setPage(nextPage) + setSize(nextSize) + void loadList(nextPage, nextSize) + }, + }} + columns={[ + { + title: '商品信息', + dataIndex: 'spuName', + width: 300, + render: (_, record) => ( +
+ {record.coverUrl ? ( + + ) : ( +
暂无主图
+ )} + + + {record.spuName} + + + {record.spuNo || `spuId: ${record.id}`} + + + {record.subTitle || '-'} + + +
+ ), + }, + { + title: '商品 ID / 类目', + dataIndex: 'id', + width: 170, + render: (_, record) => ( + + spuId: {record.id} + + {record.categoryName || `categoryId: ${record.categoryId}`} + + + ), + }, + { + title: '价格区间', + dataIndex: 'minPrice', + width: 96, + render: (_, record) => `${record.minPrice} - ${record.maxPrice}`, + }, + { + title: '库存', + dataIndex: 'totalStock', + width: 96, + render: (_, record) => ( + + 总库存:{record.totalStock} + 锁定:{record.lockedStock} + + ), + }, + { + title: '状态', + dataIndex: 'status', + width: 118, + render: (_, record) => ( + + {getStatusLabel(record.status)} + {getStatusLabel(record.auditStatus)} + {getStatusLabel(record.saleStatus)} + + ), + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + width: 138, + render: value => formatUnixTime(value), + }, + { + title: '操作', + key: 'actions', + width: 118, + render: (_, record) => ( + + + + + ), + }, + ]} + /> + + + setDrawerOpen(false)} + onSuccess={() => void loadList()} + /> + + setImportOpen(false)} + onSuccess={() => void loadList()} + /> + + ) +} diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..9fbcb77 --- /dev/null +++ b/src/pages/login/index.tsx @@ -0,0 +1,276 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { + Button, + Card, + Form, + Input, + Space, + Tabs, +} from 'antd' +import { LockOutlined, MessageOutlined, MobileOutlined } from '@ant-design/icons' +import { theme } from 'antd' +import { toast } from 'sonner' +import { useNavigate } from 'react-router-dom' +import { apiGetSmsCode } from '@/apis/apisCommon' +import { merchantProfileGet } from '@/apis/mallMerchant' +import { apiMallMerchantLogin } from '@/apis/apisUser' +import { useSessionStore } from '@/store/session' +import { resolveMerchantSessionIdentity } from '@/utils/mall' + +interface LoginFormValues { + account: string + password?: string + captcha?: string +} + +type LoginMode = 'password' | 'sms' + +const PHONE_REGEX = /^1\d{10}$/ +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + +const resolveLoginType = (account: string): '0' | '1' => { + return EMAIL_REGEX.test(account) ? '1' : '0' +} + +export default function LoginPage() { + const { token } = theme.useToken() + const navigate = useNavigate() + const setSession = useSessionStore(state => state.setSession) + const setToken = useSessionStore(state => state.setToken) + const setUserInfo = useSessionStore(state => state.setUserInfo) + const existingToken = useSessionStore(state => state.token) + const loginAccount = useSessionStore(state => state.loginAccount) + const userInfo = useSessionStore(state => state.userInfo) + const [mode, setMode] = useState('password') + const [loading, setLoading] = useState(false) + const [smsSending, setSmsSending] = useState(false) + const [smsLeft, setSmsLeft] = useState(0) + const smsTimerRef = useRef(null) + const [form] = Form.useForm() + + useEffect(() => { + if (!loading && existingToken?.accessToken) { + navigate('/profile', { replace: true }) + } + }, [existingToken, loading, navigate]) + + useEffect(() => { + return () => { + if (smsTimerRef.current) { + window.clearInterval(smsTimerRef.current) + smsTimerRef.current = null + } + } + }, []) + + const tabs = useMemo( + () => [ + { key: 'password', label: '密码登录' }, + { key: 'sms', label: '短信登录' }, + ], + [] + ) + + const startSmsCountdown = (seconds: number) => { + setSmsLeft(seconds) + if (smsTimerRef.current) { + window.clearInterval(smsTimerRef.current) + smsTimerRef.current = null + } + smsTimerRef.current = window.setInterval(() => { + setSmsLeft(prev => { + if (prev <= 1) { + if (smsTimerRef.current) { + window.clearInterval(smsTimerRef.current) + smsTimerRef.current = null + } + return 0 + } + return prev - 1 + }) + }, 1000) + } + + const onSendSms = async () => { + try { + const account = (form.getFieldValue('account') ?? '').toString().trim() + if (!PHONE_REGEX.test(account)) { + toast.error('请输入有效的手机号') + return + } + setSmsSending(true) + await apiGetSmsCode({ account, type: '1' }) + toast.success('验证码已发送') + startSmsCountdown(60) + } catch (error) { + const message = error instanceof Error ? error.message : '验证码发送失败' + toast.error(message) + } finally { + setSmsSending(false) + } + } + + return ( + + +
{ + try { + const account = values.account.trim() + const isPhone = PHONE_REGEX.test(account) + const isEmail = EMAIL_REGEX.test(account) + + if (!isPhone && !isEmail) { + toast.error('请输入有效的手机号或邮箱') + return + } + + if (mode === 'sms' && !isPhone) { + toast.error('验证码登录仅支持手机号') + return + } + + setLoading(true) + const payload = + mode === 'password' + ? { + account, + password: values.password?.trim(), + loginType: resolveLoginType(account), + type: '0' as const, + loginIp: '', + } + : { + account, + captcha: values.captcha?.trim(), + loginType: '0' as const, + type: '1' as const, + loginIp: '', + } + + const loginResult = await apiMallMerchantLogin(payload) + setToken(loginResult.data) + setUserInfo(null) + setSession({ + loginAccount: account, + merchantId: null, + merchantName: '', + }) + + try { + const profileData = await merchantProfileGet() + setSession({ + loginAccount: account, + ...resolveMerchantSessionIdentity(profileData.merchant), + }) + } catch (profileError) { + const message = + profileError instanceof Error ? profileError.message : '同步商家资料失败' + toast.error(`登录成功,但同步商家资料失败:${message}`) + } + + toast.success('登录成功') + navigate('/profile', { replace: true }) + } catch (error) { + const message = error instanceof Error ? error.message : '登录失败' + toast.error(message) + } finally { + setLoading(false) + } + }} + > + { + setMode(key as LoginMode) + form.resetFields(['password', 'captcha']) + }} + /> + { + const account = (value ?? '').toString().trim() + + if (!account) { + return Promise.reject( + new Error(mode === 'sms' ? '请输入手机号' : '请输入手机号或邮箱') + ) + } + + if (mode === 'sms') { + return PHONE_REGEX.test(account) + ? Promise.resolve() + : Promise.reject(new Error('请输入有效的手机号')) + } + + return PHONE_REGEX.test(account) || EMAIL_REGEX.test(account) + ? Promise.resolve() + : Promise.reject(new Error('请输入有效的手机号或邮箱')) + }, + }, + ]} + > + } + /> + + {mode === 'password' ? ( + + } + /> + + ) : ( + + + } /> + + + + )} + + + + +
+
+ ) +} diff --git a/src/pages/orders/OrderDetailDrawer.tsx b/src/pages/orders/OrderDetailDrawer.tsx new file mode 100644 index 0000000..6c8aca6 --- /dev/null +++ b/src/pages/orders/OrderDetailDrawer.tsx @@ -0,0 +1,196 @@ +import { useEffect, useState } from 'react' +import { Card, Descriptions, Drawer, Space, Spin, Table, Tag, Typography } from 'antd' +import { toast } from 'sonner' +import { merchantOrderDetail } from '@/apis/mallMerchant' +import type { OrderDetailInfo } from '@/types/mall' +import { formatUnixTime, getStatusColor, getStatusLabel, prettyJsonText } from '@/utils/mall' + +interface OrderDetailDrawerProps { + open: boolean + merchantId: string + orderId?: number + refreshToken?: number + onClose: () => void +} + +export default function OrderDetailDrawer({ + open, + merchantId, + orderId, + refreshToken, + onClose, +}: OrderDetailDrawerProps) { + const [loading, setLoading] = useState(false) + const [data, setData] = useState(null) + + useEffect(() => { + if (!open || !orderId) { + return + } + + let cancelled = false + const run = async () => { + try { + setLoading(true) + const result = await merchantOrderDetail({ + merchantId, + orderId, + }) + + if (!cancelled) { + setData(result.data) + } + } catch (error) { + const message = error instanceof Error ? error.message : '加载订单详情失败' + toast.error(message) + } finally { + if (!cancelled) { + setLoading(false) + } + } + } + + void run() + return () => { + cancelled = true + } + }, [merchantId, open, orderId, refreshToken]) + + return ( + + + {!data ? null : ( + + + + {data.orderNo} + {data.tradeNo} + {data.id} + {data.uid} + {data.merchantId} + {data.merchantName || '-'} + + {getStatusLabel(data.orderStatus)} + + + + {getStatusLabel(data.afterSaleStatus)} + + + {data.goodsAmount} + {data.payAmount} + {data.freightAmount} + {data.discountAmount} + {data.currency || '-'} + {formatUnixTime(data.payTime)} + {formatUnixTime(data.shipTime)} + {formatUnixTime(data.finishTime)} + {formatUnixTime(data.cancelTime)} + {formatUnixTime(data.autoConfirmAt)} + {data.payChannel || '-'} + {formatUnixTime(data.createdAt)} + {formatUnixTime(data.updatedAt)} + + + {prettyJsonText(data.extraInfoJson) || '-'} + + + + + + + + {data.receiverName} + {data.receiverMobile} + {data.receiverCountryCode || '-'} + {data.receiverProvince || '-'} + {data.receiverCity || '-'} + {data.receiverDistrict || '-'} + + {[data.receiverProvince, data.receiverCity, data.receiverDistrict, data.receiverAddress] + .filter(Boolean) + .join(' ')} + + {data.receiverZipCode || '-'} + {data.deliveryCompany || '-'} + {data.deliveryNo || '-'} + + {data.sellerRemark || '-'} + + + {data.buyerRemark || '-'} + + + + + +
( + + {record.spuName} + {record.skuName} + + ), + }, + { + title: '规格 JSON', + dataIndex: 'skuSpecJson', + width: 260, + render: value => ( + + {prettyJsonText(value) || '-'} + + ), + }, + { + title: '数量', + dataIndex: 'buyNum', + width: 90, + }, + { + title: '单价', + dataIndex: 'salePrice', + width: 100, + }, + { + title: '原价', + dataIndex: 'originPrice', + width: 100, + }, + { + title: '金额', + dataIndex: 'itemAmount', + width: 120, + }, + { + title: '优惠', + dataIndex: 'discountAmount', + width: 100, + }, + { + title: '退款', + dataIndex: 'refundAmount', + width: 100, + }, + { + title: '售后状态', + dataIndex: 'afterSaleStatus', + width: 120, + render: value => {getStatusLabel(value)}, + }, + ]} + /> + + + )} + + + ) +} diff --git a/src/pages/orders/ShipModal.tsx b/src/pages/orders/ShipModal.tsx new file mode 100644 index 0000000..9527f20 --- /dev/null +++ b/src/pages/orders/ShipModal.tsx @@ -0,0 +1,86 @@ +import { Form, Input, Modal } from 'antd' +import { toast } from 'sonner' +import { merchantOrderShip } from '@/apis/mallMerchant' +import type { OrderSummary } from '@/types/mall' + +interface ShipModalProps { + open: boolean + merchantId: string + record: OrderSummary | null + onClose: () => void + onSuccess: () => void +} + +interface ShipFormValues { + deliveryCompany: string + deliveryNo: string + sellerRemark?: string +} + +export default function ShipModal({ + open, + merchantId, + record, + onClose, + onSuccess, +}: ShipModalProps) { + const [form] = Form.useForm() + + const handleOk = async () => { + if (!record) { + return + } + + try { + const values = await form.validateFields() + await merchantOrderShip({ + merchantId, + orderId: record.id, + deliveryCompany: values.deliveryCompany.trim(), + deliveryNo: values.deliveryNo.trim(), + sellerRemark: values.sellerRemark?.trim(), + }) + toast.success('发货信息已提交') + form.resetFields() + onSuccess() + onClose() + } catch (error) { + const message = error instanceof Error ? error.message : '发货失败' + toast.error(message) + } + } + + return ( + { + form.resetFields() + onClose() + }} + onOk={() => void handleOk()} + okText="确认发货" + destroyOnClose + > +
+ + + + + + + + + + +
+ ) +} diff --git a/src/pages/orders/index.tsx b/src/pages/orders/index.tsx new file mode 100644 index 0000000..64c1794 --- /dev/null +++ b/src/pages/orders/index.tsx @@ -0,0 +1,234 @@ +import { useEffect, useState } from 'react' +import { Button, Card, Form, Input, Select, Space, Table, Tag, Typography } from 'antd' +import { toast } from 'sonner' +import { merchantOrderPage } from '@/apis/mallMerchant' +import OrderDetailDrawer from './OrderDetailDrawer' +import ShipModal from './ShipModal' +import { useSessionStore } from '@/store/session' +import type { AfterSaleStatus, OrderStatus, OrderSummary } from '@/types/mall' +import { + AFTER_SALE_STATUS_OPTIONS, + ORDER_STATUS_OPTIONS, + formatUnixTime, + getStatusColor, + getStatusLabel, +} from '@/utils/mall' + +interface OrderSearchValues { + orderStatus?: OrderStatus | '' + afterSaleStatus?: AfterSaleStatus | '' + keyword?: string +} + +export default function OrdersPage() { + const merchantId = useSessionStore(state => state.merchantId) + const [form] = Form.useForm() + const [loading, setLoading] = useState(false) + const [page, setPage] = useState(1) + const [size, setSize] = useState(20) + const [total, setTotal] = useState(0) + const [list, setList] = useState([]) + const [detailRecord, setDetailRecord] = useState(null) + const [shipRecord, setShipRecord] = useState(null) + const [detailRefreshToken, setDetailRefreshToken] = useState(0) + + const renderOrderAction = (record: OrderSummary) => { + switch (record.orderStatus) { + case 'pending_pay': + return 待支付 + case 'paid_wait_ship': + return ( + + ) + case 'shipped': + return 已发货 + case 'finished': + return 已完成 + default: + return - + } + } + + const loadList = async (nextPage = page, nextSize = size) => { + if (!merchantId) { + return + } + + try { + setLoading(true) + const values = form.getFieldsValue() + const result = await merchantOrderPage({ + merchantId, + orderStatus: values.orderStatus || undefined, + afterSaleStatus: values.afterSaleStatus || undefined, + keyword: values.keyword?.trim(), + page: nextPage, + size: nextSize, + }) + setList(result.list ?? []) + setTotal(result.total ?? 0) + } catch (error) { + const message = error instanceof Error ? error.message : '加载订单列表失败' + toast.error(message) + } finally { + setLoading(false) + } + } + + useEffect(() => { + form.setFieldsValue({ + orderStatus: '', + afterSaleStatus: '', + keyword: '', + }) + setPage(1) + void loadList(1, size) + }, [merchantId]) // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + + +
+ + ({ ...item }))} + /> + + + + + + + + + + + +
+
+ + +
{ + setPage(nextPage) + setSize(nextSize) + void loadList(nextPage, nextSize) + }, + }} + columns={[ + { + title: '订单号', + dataIndex: 'orderNo', + width: 220, + render: (_, record) => ( + + {record.orderNo} + {record.tradeNo} + + ), + }, + { + title: '状态', + dataIndex: 'orderStatus', + width: 180, + render: (_, record) => ( + + {getStatusLabel(record.orderStatus)} + + {getStatusLabel(record.afterSaleStatus)} + + + ), + }, + { + title: '金额', + dataIndex: 'payAmount', + width: 160, + render: (_, record) => ( + + 支付:{record.payAmount} + 商品:{record.goodsAmount} + + ), + }, + { + title: '支付时间', + dataIndex: 'payTime', + width: 180, + render: value => formatUnixTime(value), + }, + { + title: '发货时间', + dataIndex: 'shipTime', + width: 180, + render: value => formatUnixTime(value), + }, + { + title: '操作', + key: 'actions', + width: 180, + render: (_, record) => ( + + + {renderOrderAction(record)} + + ), + }, + ]} + /> + + + setDetailRecord(null)} + /> + + setShipRecord(null)} + onSuccess={() => { + void loadList() + setDetailRefreshToken(value => value + 1) + }} + /> + + ) +} diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx new file mode 100644 index 0000000..5b856cf --- /dev/null +++ b/src/pages/profile/index.tsx @@ -0,0 +1,230 @@ +import { useEffect, useState } from 'react' +import { + Button, + Card, + Col, + Form, + Input, + Row, + Space, + Spin, +} from 'antd' +import { toast } from 'sonner' +import { merchantProfileGet, merchantProfileUpdate } from '@/apis/mallMerchant' +import { useSessionStore } from '@/store/session' +import type { MerchantInfo } from '@/types/mall' +import { resolveMerchantSessionIdentity } from '@/utils/mall' + +interface MerchantFormValues { + merchantName: string + logo?: string + banner?: string + notice?: string + servicePhone?: string + serviceEmail?: string + contactName?: string + contactMobile?: string + province?: string + city?: string + district?: string + addressDetail?: string + zipCode?: string +} + +const merchantToFormValues = (merchant?: MerchantInfo): MerchantFormValues => ({ + merchantName: merchant?.merchantName ?? '', + logo: merchant?.logo ?? '', + banner: merchant?.banner ?? '', + notice: merchant?.notice ?? '', + servicePhone: merchant?.servicePhone ?? '', + serviceEmail: merchant?.serviceEmail ?? '', + contactName: merchant?.contactName ?? '', + contactMobile: merchant?.contactMobile ?? '', + province: merchant?.province ?? '', + city: merchant?.city ?? '', + district: merchant?.district ?? '', + addressDetail: merchant?.addressDetail ?? '', + zipCode: merchant?.zipCode ?? '', +}) + +export default function ProfilePage() { + const authToken = useSessionStore(state => state.token) + const updateIdentity = useSessionStore(state => state.updateIdentity) + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [form] = Form.useForm() + + const applyProfileData = (data: Awaited>) => { + updateIdentity(resolveMerchantSessionIdentity(data.merchant)) + form.setFieldsValue(merchantToFormValues(data.merchant)) + } + + const fetchProfileData = async () => { + if (!authToken?.accessToken) { + return null + } + + const data = await merchantProfileGet() + applyProfileData(data) + return data + } + + useEffect(() => { + if (!authToken?.accessToken) { + return + } + + let cancelled = false + const loadProfile = async () => { + try { + setLoading(true) + const data = await merchantProfileGet() + if (cancelled) { + return + } + applyProfileData(data) + } catch (error) { + const message = error instanceof Error ? error.message : '加载商家资料失败' + toast.error(message) + } finally { + if (!cancelled) { + setLoading(false) + } + } + } + + void loadProfile() + return () => { + cancelled = true + } + }, [authToken?.accessToken]) // eslint-disable-line react-hooks/exhaustive-deps + + const handleSaveProfile = async () => { + if (!authToken?.accessToken) { + return + } + + try { + const values = await form.validateFields() + setSaving(true) + await merchantProfileUpdate({ + merchantName: values.merchantName.trim(), + logo: values.logo?.trim(), + banner: values.banner?.trim(), + notice: values.notice?.trim(), + servicePhone: values.servicePhone?.trim(), + serviceEmail: values.serviceEmail?.trim(), + contactName: values.contactName?.trim(), + contactMobile: values.contactMobile?.trim(), + province: values.province?.trim(), + city: values.city?.trim(), + district: values.district?.trim(), + addressDetail: values.addressDetail?.trim(), + zipCode: values.zipCode?.trim(), + }) + + toast.success('商家资料已更新') + await fetchProfileData() + } catch (error) { + const message = error instanceof Error ? error.message : '更新商家资料失败' + toast.error(message) + } finally { + setSaving(false) + } + } + + return ( + + + void handleSaveProfile()} + > + 保存资料 + + } + > +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/routes/components/ProtectedRoute.tsx b/src/routes/components/ProtectedRoute.tsx new file mode 100644 index 0000000..facfc23 --- /dev/null +++ b/src/routes/components/ProtectedRoute.tsx @@ -0,0 +1,20 @@ +import type { PropsWithChildren } from 'react' +import { Navigate } from 'react-router-dom' +import { useSessionStore } from '@/store/session' + +interface ProtectedRouteProps extends PropsWithChildren { + redirectTo?: string +} + +export default function ProtectedRoute({ + children, + redirectTo = '/login', +}: ProtectedRouteProps) { + const token = useSessionStore(state => state.token) + + if (!token?.accessToken) { + return + } + + return <>{children} +} diff --git a/src/routes/config.tsx b/src/routes/config.tsx new file mode 100644 index 0000000..cec967e --- /dev/null +++ b/src/routes/config.tsx @@ -0,0 +1,63 @@ +import type { RouteObject } from 'react-router-dom' +import { Navigate } from 'react-router-dom' +import RootLayout from '@/layouts/RootLayout' +import AuthLayout from '@/layouts/auth/AuthLayout' +import MainLayout from '@/layouts/main/MainLayout' +import ProtectedRoute from './components/ProtectedRoute' +import LoginPage from '@/pages/login' +import ProfilePage from '@/pages/profile' +import GoodsPage from '@/pages/goods' +import FeaturedGoodsPage from '@/pages/featuredGoods' +import OrdersPage from '@/pages/orders' +import AfterSalePage from '@/pages/afterSale' + +export const routeConfig: RouteObject[] = [ + { + path: '/', + element: , + children: [ + { + element: , + children: [ + { + path: 'login', + element: , + }, + ], + }, + { + element: ( + + + + ), + children: [ + { + path: 'profile', + element: , + }, + { + path: 'goods', + element: , + }, + { + path: 'featured-goods', + element: , + }, + { + path: 'orders', + element: , + }, + { + path: 'after-sales', + element: , + }, + ], + }, + { + path: '*', + element: , + }, + ], + }, +] diff --git a/src/routes/index.tsx b/src/routes/index.tsx new file mode 100644 index 0000000..7b19c41 --- /dev/null +++ b/src/routes/index.tsx @@ -0,0 +1,7 @@ +import { useRoutes } from 'react-router-dom' +import { routeConfig } from './config' + +export default function AppRoutes() { + return useRoutes(routeConfig) +} + diff --git a/src/store/session.ts b/src/store/session.ts new file mode 100644 index 0000000..05100a9 --- /dev/null +++ b/src/store/session.ts @@ -0,0 +1,96 @@ +import { create } from 'zustand' +import { createJSONStorage, persist } from 'zustand/middleware' +import type { ITokenData } from '@/types/ITokenData' +import type { IUserInfo } from '@/types/IUserInfo' + +export interface SessionState { + token: ITokenData | null + userInfo: IUserInfo | null + loginAccount: string + merchantId: string | null + merchantName: string + setSession: (payload: Partial>) => void + setToken: (token: ITokenData | null) => void + setUserInfo: (userInfo: IUserInfo | null) => void + updateIdentity: (payload: Partial>) => void + clearSession: () => void +} + +const STORAGE_KEY = 'mall-merchant-session' + +const normalizeMerchantId = (value: unknown): string | null => { + if (typeof value === 'string') { + const nextValue = value.trim() + return nextValue || null + } + + if (typeof value === 'number' || typeof value === 'bigint') { + return String(value) + } + + return null +} + +export const useSessionStore = create()( + persist( + set => ({ + token: null, + userInfo: null, + loginAccount: '', + merchantId: null, + merchantName: '', + setSession: payload => + set(state => ({ + ...state, + ...payload, + merchantId: + 'merchantId' in payload ? normalizeMerchantId(payload.merchantId) : state.merchantId, + })), + setToken: token => + set(state => ({ + ...state, + token, + })), + setUserInfo: userInfo => + set(state => ({ + ...state, + userInfo, + })), + updateIdentity: payload => + set(state => ({ + ...state, + ...payload, + merchantId: + 'merchantId' in payload ? normalizeMerchantId(payload.merchantId) : state.merchantId, + })), + clearSession: () => + set({ + token: null, + userInfo: null, + loginAccount: '', + merchantId: null, + merchantName: '', + }), + }), + { + name: STORAGE_KEY, + storage: createJSONStorage(() => localStorage), + partialize: state => ({ + token: state.token, + userInfo: state.userInfo, + loginAccount: state.loginAccount, + merchantId: state.merchantId, + merchantName: state.merchantName, + }), + merge: (persistedState, currentState) => { + const state = (persistedState as Partial | undefined) ?? {} + + return { + ...currentState, + ...state, + merchantId: normalizeMerchantId(state.merchantId), + } + }, + } + ) +) diff --git a/src/types/ITokenData.ts b/src/types/ITokenData.ts new file mode 100644 index 0000000..4a6ae38 --- /dev/null +++ b/src/types/ITokenData.ts @@ -0,0 +1,7 @@ +export interface ITokenData { + accessToken: string + accessExpire: number + refreshAfter: number + role: string[] + uType: string +} diff --git a/src/types/IUserInfo.ts b/src/types/IUserInfo.ts new file mode 100644 index 0000000..1046cdb --- /dev/null +++ b/src/types/IUserInfo.ts @@ -0,0 +1,6 @@ +export interface IUserInfo { + uid: number + name: string + account: string +} + diff --git a/src/types/mall.ts b/src/types/mall.ts new file mode 100644 index 0000000..3698465 --- /dev/null +++ b/src/types/mall.ts @@ -0,0 +1,516 @@ +import type { PageRequest } from '@/apis/base/types' + +export type EnableStatus = 'enabled' | 'disabled' +export type AuditStatus = 'pending' | 'approved' | 'rejected' +export type SettleStatus = 'pending' | 'active' | 'frozen' +export type SaleStatus = 'off' | 'on' +export type OrderStatus = + | 'pending_pay' + | 'paid_wait_ship' + | 'shipped' + | 'finished' + | 'canceled' + | 'after_sale' + | 'refund_finished' +export type AfterSaleStatus = + | 'none' + | 'pending' + | 'approved' + | 'rejected' + | 'refunding' + | 'finished' + | 'closed' + +export interface MerchantInfo { + id: string | number + uid: number + merchantNo: string + merchantName: string + status: EnableStatus + auditStatus: AuditStatus + settleStatus: SettleStatus + contactName: string + contactMobile: string + contactEmail: string + licenseNo: string + licenseImagesJson: string + extraInfoJson: string + remark: string + approvedAt: number + createdAt: number + updatedAt: number + logo: string + banner: string + notice: string + servicePhone: string + serviceEmail: string + province: string + city: string + district: string + addressDetail: string + zipCode: string + sort: number +} + +export interface GoodsSkuInfo { + id: number + spuId: number + merchantId: string + skuNo: string + skuName: string + barcode: string + imageUrl: string + specJson: string + salePrice: string + originPrice: string + costPrice: string + weight: string + status: EnableStatus + stock: number + lockedStock: number + warnStock: number + createdAt: number + updatedAt: number +} + +export interface GoodsSummary { + id: number + merchantId: string + categoryId: number + spuNo: string + spuName: string + subTitle: string + coverUrl: string + status: EnableStatus + auditStatus: AuditStatus + saleStatus: SaleStatus + sort: number + salesVolume: number + virtualSales: number + viewCount: number + minPrice: string + maxPrice: string + totalStock: number + lockedStock: number + createdAt: number + updatedAt: number + categoryName: string +} + +export interface GoodsDetailInfo { + id: number + merchantId: string + categoryId: number + spuNo: string + spuName: string + subTitle: string + coverUrl: string + mediaJson: string + detailJson: string + extraInfoJson: string + status: EnableStatus + auditStatus: AuditStatus + saleStatus: SaleStatus + sort: number + salesVolume: number + virtualSales: number + viewCount: number + minPrice: string + maxPrice: string + totalStock: number + lockedStock: number + paramJson: string + serviceJson: string + skus: GoodsSkuInfo[] + createdAt: number + updatedAt: number +} + +export interface MallCategoryTreeParams { + parentId?: number + status?: EnableStatus +} + +export interface MallCategoryItem { + id: number + parentId: number + categoryName: string + categoryLevel: number + categoryPath: string + categoryIcon: string + isLeaf: boolean + hasChildren?: boolean + sort: number + status: EnableStatus + goodsCount: number + createdAt: number + updatedAt: number + children: MallCategoryItem[] +} + +export interface MallCategoryTreeData { + list: MallCategoryItem[] +} + +export interface MerchantCategoryAttributeValue { + valueId: string | number + valueName: string + sort?: number + status?: EnableStatus +} + +export type MerchantCategoryAttributeType = 'SALES_PROPERTY' | 'PRODUCT_PROPERTY' + +export interface MerchantCategoryAttributeItem { + attributeId: string | number + attributeName: string + attributeType: MerchantCategoryAttributeType | string + isRequired: boolean + isCustomizable: boolean + isMultipleSelection: boolean + values: MerchantCategoryAttributeValue[] +} + +export interface MerchantCategoryAttributeSource { + sourcePlatform?: string + sourceKey?: string +} + +export interface MerchantCategoryAttributeListParams { + categoryId: number + sourcePlatform?: string + sourceKey?: string + status?: EnableStatus +} + +export interface MerchantCategoryAttributeListData { + categoryId: number + categoryName: string + sourcePlatform: string + sourceKey: string + sources: MerchantCategoryAttributeSource[] + list: MerchantCategoryAttributeItem[] +} + +export interface OrderItemInfo { + id: number + orderId: number + tradeId: number + orderNo: string + spuId: number + skuId: number + spuName: string + skuName: string + coverUrl: string + skuSpecJson: string + buyNum: number + salePrice: string + originPrice: string + discountAmount: string + itemAmount: string + refundAmount: string + afterSaleStatus: AfterSaleStatus +} + +export interface OrderSummary { + id: number + tradeId: number + tradeNo: string + orderNo: string + uid: number + merchantId: string + merchantName: string + orderStatus: OrderStatus + afterSaleStatus: AfterSaleStatus + currency: string + goodsAmount: string + freightAmount: string + discountAmount: string + payAmount: string + payTime: number + shipTime: number + finishTime: number + cancelTime: number + createdAt: number + updatedAt: number +} + +export interface OrderDetailInfo { + id: number + tradeId: number + tradeNo: string + orderNo: string + uid: number + merchantId: string + merchantName: string + orderStatus: OrderStatus + afterSaleStatus: AfterSaleStatus + currency: string + goodsAmount: string + freightAmount: string + discountAmount: string + payAmount: string + buyerRemark: string + sellerRemark: string + receiverName: string + receiverMobile: string + receiverCountryCode: string + receiverProvince: string + receiverCity: string + receiverDistrict: string + receiverAddress: string + receiverZipCode: string + deliveryCompany: string + deliveryNo: string + payChannel: string + extraInfoJson: string + payTime: number + shipTime: number + finishTime: number + cancelTime: number + autoConfirmAt: number + items: OrderItemInfo[] + createdAt: number + updatedAt: number +} + +export interface AfterSaleSummary { + id: number + afterSaleNo: string + tradeId: number + orderId: number + orderItemId: number + orderNo: string + uid: number + merchantId: string + merchantName: string + spuName: string + skuName: string + afterSaleType: 1 | 2 + afterSaleStatus: AfterSaleStatus + reason: string + description: string + evidenceJson: string + applyAmount: string + refundAmount: string + rejectReason: string + handleRemark: string + auditTime: number + successTime: number + closeTime: number + createdAt: number + updatedAt: number +} + +export interface MerchantGoodsSkuInput { + skuId: number + skuNo: string + skuName: string + barcode: string + imageUrl: string + specJson: string + salePrice: string + originPrice: string + costPrice: string + weight: string + status: EnableStatus + stock: number + warnStock: number +} + +export interface MerchantProfileGetParams { + merchantId?: string +} + +export interface MerchantProfileGetData { + merchant: MerchantInfo +} + +export interface MerchantProfileUpdateParams { + merchantName?: string + logo?: string + banner?: string + notice?: string + servicePhone?: string + serviceEmail?: string + contactName?: string + contactMobile?: string + province?: string + city?: string + district?: string + addressDetail?: string + zipCode?: string +} + +export interface MerchantProfileUpdateData { + merchantId?: string +} + +export interface MerchantGoodsPageParams extends PageRequest { + merchantId: string + categoryId?: number + keyword?: string + auditStatus?: AuditStatus + saleStatus?: SaleStatus +} + +export interface MerchantGoodsPageData { + total: number + list: GoodsSummary[] +} + +export interface MerchantGoodsDetailParams { + merchantId: string + spuId: number +} + +export interface MerchantGoodsDetailData { + data: GoodsDetailInfo +} + +export interface MerchantGoodsSaveParams { + merchantId: string + spuId?: number + categoryId: number + spuName: string + subTitle?: string + coverUrl?: string + mediaJson?: string + detailJson?: string + paramJson?: string + serviceJson?: string + extraInfoJson?: string + status?: EnableStatus + saleStatus?: SaleStatus + sort?: number + skus: MerchantGoodsSkuInput[] +} + +export interface MerchantGoodsSaveData { + spuId: number + skuIds: number[] +} + +export interface MerchantGoodsPublishParams { + merchantId: string + spuId: number + saleStatus: SaleStatus +} + +export interface MerchantGoodsPublishData { + spuId: number + saleStatus: SaleStatus +} + +export interface MerchantFeaturedGoodsItem { + id: number + spuId: number + spuNo: string + spuName: string + subTitle: string + coverUrl: string + goodsStatus: EnableStatus + auditStatus: AuditStatus + saleStatus: SaleStatus + configStatus: EnableStatus + sort: number + minPrice: string + maxPrice: string + totalStock: number + createdAt: number + updatedAt: number +} + +export interface MerchantFeaturedGoodsPageParams extends PageRequest { + merchantId: string + keyword?: string + status?: EnableStatus +} + +export interface MerchantFeaturedGoodsPageData { + total: number + list: MerchantFeaturedGoodsItem[] +} + +export interface MerchantFeaturedGoodsSaveParams { + merchantId: string + id?: number + spuId: number + sort?: number + status?: EnableStatus +} + +export interface MerchantFeaturedGoodsSaveData { + id: number + spuId: number +} + +export interface MerchantFeaturedGoodsDeleteParams { + merchantId: string + id: number +} + +export interface MerchantFeaturedGoodsDeleteData { + id: number +} + +export interface MerchantOrderPageParams extends PageRequest { + merchantId: string + orderStatus?: OrderStatus + afterSaleStatus?: AfterSaleStatus + keyword?: string +} + +export interface MerchantOrderPageData { + total: number + list: OrderSummary[] +} + +export interface MerchantOrderDetailParams { + merchantId: string + orderId: number +} + +export interface MerchantOrderDetailData { + data: OrderDetailInfo +} + +export interface MerchantOrderShipParams { + merchantId: string + orderId: number + deliveryCompany: string + deliveryNo: string + sellerRemark?: string +} + +export interface MerchantOrderShipData { + orderId: number + orderStatus: OrderStatus + shipTime: number +} + +export interface MerchantAfterSalePageParams extends PageRequest { + merchantId: string + afterSaleType?: 0 | 1 | 2 + afterSaleStatus?: AfterSaleStatus + keyword?: string +} + +export interface MerchantAfterSalePageData { + total: number + list: AfterSaleSummary[] +} + +export interface MerchantAfterSaleAuditParams { + merchantId: string + afterSaleId: number + auditStatus: Exclude + refundAmount?: string + rejectReason?: string + handleRemark?: string +} + +export interface MerchantAfterSaleAuditData { + afterSaleId: number + afterSaleStatus: AfterSaleStatus +} diff --git a/src/utils/hqbImport.ts b/src/utils/hqbImport.ts new file mode 100644 index 0000000..7a2fefe --- /dev/null +++ b/src/utils/hqbImport.ts @@ -0,0 +1,374 @@ +import type { HqbGoodsInfo, HqbImageItem, HqbSkuInfo, HqbSpecGroup, HqbSpecItem } from '@/apis/hqb' +import type { MerchantGoodsSaveParams, MerchantGoodsSkuInput } from '@/types/mall' + +export const HQB_TARGET_CATEGORY_ID = 1126672 +export const HQB_TARGET_CATEGORY_NAME = '手机壳及手机套' + +const COLOR_ATTRIBUTE = { + attributeId: '100000', + attributeName: '颜色', +} + +const SIZE_ATTRIBUTE = { + attributeId: '100007', + attributeName: '尺码', +} + +interface HqbSpecValueMeta { + sourceSpecName: string + valueName: string + sortIndex: number +} + +interface MallSpecValue { + attributeId: string + attributeName: string + valueName: string +} + +export interface HqbImportPreview { + sourceGoodsId: string + sourceUrl: string + title: string + subTitle: string + coverUrl: string + imageUrls: string[] + videoUrls: string[] + skuCount: number + categoryName: string + sourceCategoryPath: string + shopName: string + payload: MerchantGoodsSaveParams +} + +export const extractHqbGoodsId = (input: string) => { + const content = input.trim() + const patterns = [ + /(?:https?:\/\/)?(?:www\.)?52hqb\.com\/good\/(\d+)/i, + /(?:https?:\/\/)?(?:m\.)?52hqb\.com\/pages\/[^?]+[?&]goodsId=(\d+)/i, + /\/good\/(\d+)/i, + /"goodsId"\s*:\s*"?(\d+)"?/i, + /"id"\s*:\s*"?(\d{6,})"?/i, + ] + + for (const pattern of patterns) { + const matched = content.match(pattern) + if (matched?.[1]) { + return matched[1] + } + } + + return '' +} + +const normalizeText = (value: unknown) => { + if (typeof value === 'string') { + return value.trim() + } + + if (typeof value === 'number' || typeof value === 'boolean') { + return String(value) + } + + return '' +} + +const normalizeImageUrl = (value: unknown) => { + const url = normalizeText(value) + if (!url) { + return '' + } + + if (url.startsWith('//')) { + return `https:${url}` + } + + return url +} + +const uniqueNonEmpty = (values: Array) => { + const result: string[] = [] + const existed = new Set() + + values.forEach(value => { + const normalized = normalizeImageUrl(value) + if (!normalized || existed.has(normalized)) { + return + } + + existed.add(normalized) + result.push(normalized) + }) + + return result +} + +const pickImageUrl = (item?: HqbImageItem) => + normalizeImageUrl(item?.url || item?.imgSrc || item?.thumbUrl) + +const toMoneyText = (value: unknown, fallback = '0.00') => { + if (value === null || value === undefined || value === '') { + return fallback + } + + const parsed = Number(value) + if (!Number.isFinite(parsed) || parsed < 0) { + return fallback + } + + return parsed.toFixed(2) +} + +const toOptionalMoneyText = (value: unknown) => { + if (value === null || value === undefined || value === '') { + return '' + } + + const parsed = Number(value) + if (!Number.isFinite(parsed) || parsed < 0) { + return '' + } + + return parsed.toFixed(2) +} + +const toStockNumber = (value: unknown) => { + const parsed = Number(value) + if (!Number.isFinite(parsed) || parsed < 0) { + return 0 + } + + return Math.floor(parsed) +} + +const parseUnknownImageList = (value: unknown) => { + if (Array.isArray(value)) { + return value + .map(item => { + if (typeof item === 'string') { + return normalizeImageUrl(item) + } + + if (item && typeof item === 'object') { + return pickImageUrl(item as HqbImageItem) + } + + return '' + }) + .filter(Boolean) + } + + if (typeof value !== 'string') { + return [] + } + + try { + const parsed = JSON.parse(value) as unknown + return parseUnknownImageList(parsed) + } catch { + return Array.from(value.matchAll(/https?:\/\/[^\s"']+\.(?:jpg|jpeg|png|webp|gif)/gi)).map( + item => normalizeImageUrl(item[0]) + ) + } +} + +const buildSpecValueMap = (specsList?: Record) => { + const valueMap = new Map() + + Object.values(specsList ?? {}).forEach((group, groupIndex) => { + const sourceSpecName = normalizeText(group.specName) || `规格${groupIndex + 1}` + ;(group.items ?? []).forEach((item: HqbSpecItem) => { + const valueId = item.valueId === undefined || item.valueId === null ? '' : String(item.valueId) + const valueName = normalizeText(item.value) + if (!valueId || !valueName) { + return + } + + valueMap.set(valueId, { + sourceSpecName, + valueName, + sortIndex: groupIndex, + }) + }) + }) + + return valueMap +} + +const resolveMallAttribute = (sourceSpecName: string, sortIndex: number) => { + if (/色/.test(sourceSpecName)) { + return COLOR_ATTRIBUTE + } + + if (/尺码|尺寸|型号|机型|规格|款式/.test(sourceSpecName)) { + return SIZE_ATTRIBUTE + } + + return sortIndex === 0 ? COLOR_ATTRIBUTE : SIZE_ATTRIBUTE +} + +const buildSkuSpecs = (sku: HqbSkuInfo, valueMap: Map) => { + const keyIds = normalizeText(sku.key).split('_').filter(Boolean) + const keyNameValues = normalizeText(sku.keyName).split('_').filter(Boolean) + + const specs = keyIds + .map((keyId, index): MallSpecValue | null => { + const meta = valueMap.get(keyId) + const attribute = resolveMallAttribute(meta?.sourceSpecName || '', meta?.sortIndex ?? index) + const valueName = meta?.valueName || keyNameValues[index] || '' + + if (!valueName) { + return null + } + + return { + attributeId: attribute.attributeId, + attributeName: attribute.attributeName, + valueName, + } + }) + .filter((item): item is MallSpecValue => Boolean(item)) + + if (specs.length > 0) { + return specs + } + + return keyNameValues.map((valueName, index) => { + const attribute = resolveMallAttribute('', index) + return { + attributeId: attribute.attributeId, + attributeName: attribute.attributeName, + valueName, + } + }) +} + +const buildSkuName = (title: string, specs: MallSpecValue[], fallback?: string) => { + const specName = specs.map(item => item.valueName).filter(Boolean).join('-') + if (specName) { + return specName + } + + return normalizeText(fallback) || title || '默认 SKU' +} + +const mapHqbSku = ( + sku: HqbSkuInfo, + goods: HqbGoodsInfo, + title: string, + valueMap: Map +): MerchantGoodsSkuInput => { + const specs = buildSkuSpecs(sku, valueMap) + const salePrice = toMoneyText(sku.price ?? goods.price ?? goods.yprice ?? goods.maxPrice) + + return { + skuId: 0, + skuNo: normalizeText(sku.skuCode || sku.id), + skuName: buildSkuName(title, specs, sku.keyName), + barcode: '', + imageUrl: normalizeImageUrl(sku.skuImg || sku.imgUrl || sku.thumbUrl || goods.imageUrl), + specJson: JSON.stringify(specs), + salePrice, + originPrice: toOptionalMoneyText(sku.originalPrice ?? goods.maxPrice), + costPrice: '', + weight: normalizeText(sku.weight) === '0' ? '' : normalizeText(sku.weight), + status: 'enabled', + stock: toStockNumber(sku.storeCount), + warnStock: 0, + } +} + +const buildDefaultSku = (goods: HqbGoodsInfo, title: string): MerchantGoodsSkuInput => ({ + skuId: 0, + skuNo: normalizeText(goods.goodsSn || goods.id), + skuName: '默认 SKU', + barcode: '', + imageUrl: normalizeImageUrl(goods.imageUrl), + specJson: '[]', + salePrice: toMoneyText(goods.price ?? goods.yprice ?? goods.maxPrice), + originPrice: toOptionalMoneyText(goods.maxPrice), + costPrice: '', + weight: '', + status: 'enabled', + stock: 0, + warnStock: 0, +}) + +export const buildMallGoodsFromHqb = ( + goods: HqbGoodsInfo, + merchantId: string, + sourceUrl: string +): HqbImportPreview => { + const title = normalizeText(goods.title) || `52hqb 商品 ${goods.id}` + const subTitle = normalizeText(goods.remark || goods.goodsSn) + const carouselImages = uniqueNonEmpty([ + goods.imageUrl, + ...(goods.goodsImageList ?? []).map(item => pickImageUrl(item)), + ]) + const detailImages = uniqueNonEmpty([ + ...parseUnknownImageList(goods.descImageList), + ...(carouselImages.length > 0 ? carouselImages : []), + ]) + const videoUrls = uniqueNonEmpty([goods.videoUrl]) + const valueMap = buildSpecValueMap(goods.specsList) + const skus = + goods.skuInfoList && goods.skuInfoList.length > 0 + ? goods.skuInfoList.map(item => mapHqbSku(item, goods, title, valueMap)) + : [buildDefaultSku(goods, title)] + const sourceGoodsId = String(goods.id) + const shopName = normalizeText(goods.shopInfo?.shopName) + const detailBlocks = [ + ...(normalizeText(goods.textDesc) + ? [{ type: 'text', value: normalizeText(goods.textDesc) }] + : []), + ...detailImages.map(url => ({ type: 'image', value: url })), + ] + + const payload: MerchantGoodsSaveParams = { + merchantId, + spuId: 0, + categoryId: HQB_TARGET_CATEGORY_ID, + spuName: title, + subTitle, + coverUrl: carouselImages[0] || normalizeImageUrl(goods.imageUrl), + mediaJson: JSON.stringify({ + images: carouselImages, + videos: videoUrls, + }), + detailJson: JSON.stringify({ + blocks: detailBlocks, + }), + paramJson: '[]', + serviceJson: '[]', + extraInfoJson: JSON.stringify({ + source: '52hqb', + sourceUrl, + sourceGoodsId, + sourceSite: goods.siteCode || 'hqb', + sourceCategoryPath: normalizeText(goods.catePath), + sourceCategoryIdPath: normalizeText(goods.cateIdPath), + sourceShopName: shopName, + sourceShopId: goods.shopInfo?.id ? String(goods.shopInfo.id) : '', + importedAt: new Date().toISOString(), + }), + status: 'enabled', + saleStatus: 'off', + sort: 0, + skus, + } + + return { + sourceGoodsId, + sourceUrl, + title, + subTitle, + coverUrl: payload.coverUrl || '', + imageUrls: carouselImages, + videoUrls, + skuCount: skus.length, + categoryName: HQB_TARGET_CATEGORY_NAME, + sourceCategoryPath: normalizeText(goods.catePath), + shopName, + payload, + } +} diff --git a/src/utils/mall.ts b/src/utils/mall.ts new file mode 100644 index 0000000..1ae4440 --- /dev/null +++ b/src/utils/mall.ts @@ -0,0 +1,160 @@ +import dayjs from 'dayjs' +import type { MerchantInfo } from '@/types/mall' + +export const ENABLE_STATUS_OPTIONS = [ + { label: '全部', value: '' }, + { label: '启用', value: 'enabled' }, + { label: '停用', value: 'disabled' }, +] as const + +export const AUDIT_STATUS_OPTIONS = [ + { label: '全部', value: '' }, + { label: '待审核', value: 'pending' }, + { label: '审核通过', value: 'approved' }, + { label: '审核驳回', value: 'rejected' }, +] as const + +export const SALE_STATUS_OPTIONS = [ + { label: '全部', value: '' }, + { label: '下架', value: 'off' }, + { label: '上架', value: 'on' }, +] as const + +export const ORDER_STATUS_OPTIONS = [ + { label: '全部', value: '' }, + { label: '待支付', value: 'pending_pay' }, + { label: '待发货', value: 'paid_wait_ship' }, + { label: '已发货', value: 'shipped' }, + { label: '已完成', value: 'finished' }, + { label: '已取消', value: 'canceled' }, + { label: '售后中', value: 'after_sale' }, + { label: '退款完成', value: 'refund_finished' }, +] as const + +export const AFTER_SALE_STATUS_OPTIONS = [ + { label: '全部', value: '' }, + { label: '无售后', value: 'none' }, + { label: '待审核', value: 'pending' }, + { label: '审核通过', value: 'approved' }, + { label: '审核驳回', value: 'rejected' }, + { label: '退款中', value: 'refunding' }, + { label: '已完成', value: 'finished' }, + { label: '已关闭', value: 'closed' }, +] as const + +export const AFTER_SALE_TYPE_OPTIONS = [ + { label: '全部', value: 0 }, + { label: '仅退款', value: 1 }, + { label: '退货退款', value: 2 }, +] as const + +const STATUS_LABEL_MAP: Record = { + enabled: '启用', + disabled: '停用', + pending: '待处理', + approved: '已通过', + rejected: '已驳回', + active: '正常结算', + frozen: '结算冻结', + off: '下架', + on: '上架', + none: '无售后', + refunding: '退款中', + finished: '已完成', + closed: '已关闭', + pending_pay: '待支付', + paid_wait_ship: '待发货', + shipped: '已发货', + canceled: '已取消', + after_sale: '售后中', + refund_finished: '退款完成', +} + +const STATUS_COLOR_MAP: Record = { + enabled: 'green', + disabled: 'default', + pending: 'gold', + approved: 'green', + rejected: 'red', + active: 'green', + frozen: 'red', + off: 'default', + on: 'processing', + none: 'default', + refunding: 'purple', + finished: 'green', + closed: 'default', + pending_pay: 'gold', + paid_wait_ship: 'processing', + shipped: 'cyan', + canceled: 'default', + after_sale: 'orange', + refund_finished: 'green', +} + +export const getStatusLabel = (value?: string) => { + if (!value) { + return '-' + } + + return STATUS_LABEL_MAP[value] ?? value +} + +export const getStatusColor = (value?: string) => STATUS_COLOR_MAP[value ?? ''] ?? 'default' + +export const formatUnixTime = (value?: number) => { + if (!value) { + return '-' + } + + return dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss') +} + +export const prettyJsonText = (value?: string) => { + if (!value?.trim()) { + return '' + } + + try { + return JSON.stringify(JSON.parse(value), null, 2) + } catch { + return value + } +} + +export const normalizeJsonText = (value: string | undefined, fieldLabel: string, allowEmpty = true) => { + const trimmed = value?.trim() ?? '' + + if (!trimmed) { + if (allowEmpty) { + return '' + } + throw new Error(`${fieldLabel} 不能为空`) + } + + try { + return JSON.stringify(JSON.parse(trimmed)) + } catch { + throw new Error(`${fieldLabel} 不是合法 JSON`) + } +} + +export const compactObject = (payload: T): T => { + return Object.fromEntries( + Object.entries(payload as Record).filter( + ([, value]) => value !== '' && value !== undefined && value !== null + ) + ) as T +} + +export const resolveMerchantSessionIdentity = ( + merchant?: Pick | null +) => { + const merchantId = + merchant?.id === undefined || merchant?.id === null ? null : String(merchant.id).trim() || null + + return { + merchantId, + merchantName: merchant?.merchantName ?? '', + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..ed77210 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// + diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..ad8b50c --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1d648be --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} + diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..eecc516 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "noEmit": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..3fd5f3e --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,13 @@ +import path from 'node:path' +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react-swc' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}) +