commit 7da5df2419e0483950cc28a9d8d05dc19b8f2d23 Author: zhxiao1124 Date: Wed Jul 1 11:54:45 2026 +0800 chore: 初始化CYH TikTok短剧管理后台项目 创建完整的Vite+React+TypeScript后台项目基础结构,包含登录页、工作台、权限管理等功能页面,配置了项目依赖、构建脚本和基础样式 diff --git a/.env b/.env new file mode 100644 index 0000000..ad1d469 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://localhost:3030 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/.trae/rules/git-commit-message.md b/.trae/rules/git-commit-message.md new file mode 100644 index 0000000..86003bb --- /dev/null +++ b/.trae/rules/git-commit-message.md @@ -0,0 +1,8 @@ +--- +alwaysApply: true +scene: git_message +--- + +默认使用简体中文生成提交信息 + +在此处编写规则,自定义 AI 生成提交信息的风格。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1f5a46 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# CTH TikTok 短剧管理后台 + +基于 Vite + TypeScript + React + Arco Design 的后台管理系统,默认中文界面。 + +## 技术栈 + +- Vite +- React +- TypeScript +- Arco Design +- React Router +- Axios +- pnpm + +## 本地开发 + +```bash +pnpm install +pnpm run dev +``` + +默认访问地址: + +```text +http://localhost:5173 +``` + +## 后端接口 + +前端通过环境变量配置后端地址: + +```text +VITE_API_BASE_URL=http://localhost:3030 +``` + +当前项目已包含 `.env`,默认指向本地后端 `http://localhost:3030`。 + +## 默认账号 + +```text +用户名:admin +密码:admin123 +``` + +## 功能页面 + +- 登录 +- 工作台 +- 短剧管理 +- 用户管理 +- 权限管理 +- 角色管理 +- 管理员管理 +- 日志管理 + +## 验证命令 + +```bash +pnpm run lint +pnpm run build +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..b59d0db --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + CTH TikTok 短剧管理后台 + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..6045ca1 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "cth-tk-admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "tsc --noEmit", + "preview": "vite preview" + }, + "dependencies": { + "@arco-design/web-react": "^2.66.15", + "axios": "^1.18.1", + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.0" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.2", + "typescript": "~6.0.2", + "vite": "^8.1.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..7ae2601 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1148 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@arco-design/web-react': + specifier: ^2.66.15 + version: 2.66.15(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + axios: + specifier: ^1.18.1 + version: 1.18.1 + react: + specifier: ^19.2.7 + version: 19.2.7 + react-dom: + specifier: ^19.2.7 + version: 19.2.7(react@19.2.7) + react-router-dom: + specifier: ^7.18.0 + version: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + devDependencies: + '@types/node': + specifier: ^24.13.2 + version: 24.13.2 + '@types/react': + specifier: ^19.2.17 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^6.0.2 + version: 6.0.3(vite@8.1.0(@types/node@24.13.2)) + typescript: + specifier: ~6.0.2 + version: 6.0.3 + vite: + specifier: ^8.1.0 + version: 8.1.0(@types/node@24.13.2) + +packages: + + '@arco-design/color@0.4.0': + resolution: {integrity: sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==} + + '@arco-design/web-react@2.66.15': + resolution: {integrity: sha512-OjL8SyY/H/f7+cTM8PKruT6Fk8RXROSkXXSl8ltYTWDQ2OyjTAFarkSNlpOYuX941QiaKRqkDsAjSkNeeIJxOg==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/node@24.13.2': + resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@vitejs/plugin-react@6.0.3': + resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.18.1: + resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==} + + b-tween@0.3.3: + resolution: {integrity: sha512-oEHegcRpA7fAuc9KC4nktucuZn2aS8htymCPcP3qkEGPqiBH+GfqtqoG2l7LxHngg6O0HFM7hOeOYExl1Oz4ZA==} + + b-validate@1.5.3: + resolution: {integrity: sha512-iCvCkGFskbaYtfQ0a3GmcQCHl/Sv1GufXFGuUQ+FE+WJa7A/espLOuFIn09B944V8/ImPj71T4+rTASxO2PAuA==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + compute-scroll-into-view@1.0.20: + resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + 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.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + focus-lock@1.3.6: + resolution: {integrity: sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==} + engines: {node: '>=10'} + + 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.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + 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.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + 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'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + number-precision@1.6.0: + resolution: {integrity: sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + 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.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + react-clientside-effect@1.2.8: + resolution: {integrity: sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-focus-lock@2.13.7: + resolution: {integrity: sha512-20lpZHEQrXPb+pp1tzd4ULL6DyO5D2KnR0G69tTDdydrmNhU7pdFmbQUYVyHUgp+xN29IuFR0PVuhOmvaZL9Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-router-dom@7.18.0: + resolution: {integrity: sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.0: + resolution: {integrity: sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + scroll-into-view-if-needed@2.2.31: + resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.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 + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + +snapshots: + + '@arco-design/color@0.4.0': + dependencies: + color: 3.2.1 + + '@arco-design/web-react@2.66.15(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@arco-design/color': 0.4.0 + '@babel/runtime': 7.29.7 + b-tween: 0.3.3 + b-validate: 1.5.3 + compute-scroll-into-view: 1.0.20 + dayjs: 1.11.21 + lodash: 4.18.1 + number-precision: 1.6.0 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-focus-lock: 2.13.7(@types/react@19.2.17)(react@19.2.7) + react-is: 18.3.1 + react-transition-group: 4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + resize-observer-polyfill: 1.5.1 + scroll-into-view-if-needed: 2.2.31 + shallowequal: 1.1.0 + transitivePeerDependencies: + - '@types/react' + + '@babel/runtime@7.29.7': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.137.0': {} + + '@rolldown/binding-android-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-x64@1.1.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.3': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/node@24.13.2': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.0.3(vite@8.1.0(@types/node@24.13.2))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.1.0(@types/node@24.13.2) + + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + asynckit@0.4.0: {} + + axios@1.18.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.6 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + b-tween@0.3.3: {} + + b-validate@1.5.3: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + compute-scroll-into-view@1.0.20: {} + + cookie@1.1.1: {} + + csstype@3.2.3: {} + + dayjs@1.11.21: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.7 + csstype: 3.2.3 + + 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.2: + 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.4 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + focus-lock@1.3.6: + dependencies: + tslib: 2.8.1 + + follow-redirects@1.16.0: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + 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.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + is-arrayish@0.3.4: {} + + js-tokens@4.0.0: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lodash@4.18.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 + + ms@2.1.3: {} + + nanoid@3.3.15: {} + + number-precision@1.6.0: {} + + object-assign@4.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proxy-from-env@2.1.0: {} + + react-clientside-effect@1.2.8(react@19.2.7): + dependencies: + '@babel/runtime': 7.29.7 + react: 19.2.7 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-focus-lock@2.13.7(@types/react@19.2.17)(react@19.2.7): + dependencies: + '@babel/runtime': 7.29.7 + focus-lock: 1.3.6 + prop-types: 15.8.1 + react: 19.2.7 + react-clientside-effect: 1.2.8(react@19.2.7) + use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) + use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + react-is@16.13.1: {} + + react-is@18.3.1: {} + + react-router-dom@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-router: 7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + react-router@7.18.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + cookie: 1.1.1 + react: 19.2.7 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + + react-transition-group@4.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@babel/runtime': 7.29.7 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + react@19.2.7: {} + + resize-observer-polyfill@1.5.1: {} + + rolldown@1.1.3: + dependencies: + '@oxc-project/types': 0.137.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 + + scheduler@0.27.0: {} + + scroll-into-view-if-needed@2.2.31: + dependencies: + compute-scroll-into-view: 1.0.20 + + set-cookie-parser@2.7.2: {} + + shallowequal@1.1.0: {} + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + source-map-js@1.2.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tslib@2.8.1: {} + + typescript@6.0.3: {} + + undici-types@7.18.2: {} + + use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + vite@8.1.0(@types/node@24.13.2): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.16 + rolldown: 1.1.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.2 + fsevents: 2.3.3 diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/icons.svg b/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..b036253 --- /dev/null +++ b/src/App.css @@ -0,0 +1,348 @@ +.app-shell { + min-height: 100vh; + background: #f2f3f5; +} + +.login-page { + min-height: 100vh; + display: grid; + grid-template-columns: minmax(520px, 0.95fr) minmax(420px, 1fr); + background: #f7f8fa; +} + +.login-visual { + position: relative; + overflow: hidden; + padding: 44px 56px 48px; + color: #ffffff; + background: + linear-gradient(135deg, rgba(15, 23, 42, 0.98) 0%, rgba(22, 93, 255, 0.94) 54%, rgba(15, 118, 110, 0.96) 100%), + radial-gradient(circle at 18% 18%, rgba(255, 255, 255, 0.18), transparent 28%), + radial-gradient(circle at 82% 72%, rgba(255, 255, 255, 0.12), transparent 32%); + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.login-visual::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0)); + pointer-events: none; +} + +.login-logo { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; + letter-spacing: 0; +} + +.login-brand { + position: relative; + z-index: 1; + max-width: 640px; +} + +.login-eyebrow { + display: inline-flex; + margin-bottom: 18px; + padding: 6px 12px; + border: 1px solid rgba(255, 255, 255, 0.22); + border-radius: 999px; + color: rgba(255, 255, 255, 0.82); + font-size: 13px; +} + +.login-brand h1 { + margin: 0 0 16px; + font-size: 42px; + line-height: 1.16; + letter-spacing: 0; +} + +.login-brand p { + margin: 0; + max-width: 560px; + color: rgba(255, 255, 255, 0.78); + font-size: 16px; + line-height: 1.8; +} + +.login-metrics { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + max-width: 640px; +} + +.login-metric { + padding: 16px; + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 8px; + background: rgba(255, 255, 255, 0.09); + backdrop-filter: blur(10px); +} + +.login-metric strong { + display: block; + font-size: 20px; + margin-bottom: 6px; +} + +.login-metric span { + color: rgba(255, 255, 255, 0.72); +} + +.login-panel { + display: flex; + align-items: center; + justify-content: center; + padding: 48px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.86), rgba(247, 248, 250, 0.94)), + #f7f8fa; +} + +.login-card { + width: 100%; + max-width: 420px; + padding: 36px; + border: 1px solid #e5e6eb; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 18px 48px rgba(15, 23, 42, 0.08); +} + +.login-card-header { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 30px; +} + +.login-card-mark { + width: 44px; + height: 44px; + display: grid; + place-items: center; + border-radius: 8px; + color: #ffffff; + background: #165dff; + font-weight: 700; +} + +.login-card h2 { + margin: 0 0 8px; + font-size: 24px; +} + +.login-card .subtitle { + margin: 0; + color: #86909c; +} + +.login-card-footer { + margin-top: 22px; + padding-top: 18px; + border-top: 1px solid #f2f3f5; + color: #86909c; + font-size: 12px; +} + +.layout { + min-height: 100vh; +} + +.sidebar { + box-shadow: 1px 0 0 #e5e6eb; +} + +.logo-row { + height: 56px; + display: flex; + align-items: center; + gap: 10px; + padding: 0 18px; + border-bottom: 1px solid #e5e6eb; + font-weight: 700; +} + +.logo-mark { + width: 28px; + height: 28px; + display: grid; + place-items: center; + color: #ffffff; + background: #165dff; + border-radius: 6px; + font-size: 14px; +} + +.header { + height: 56px; + padding: 0 20px; + background: #ffffff; + border-bottom: 1px solid #e5e6eb; + display: flex; + align-items: center; + justify-content: space-between; +} + +.header-title { + display: flex; + flex-direction: column; + gap: 2px; +} + +.header-title strong { + font-size: 16px; +} + +.header-title span { + color: #86909c; + font-size: 12px; +} + +.content { + padding: 20px; +} + +.page-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; + margin-bottom: 16px; +} + +.metric-card .label { + color: #86909c; + margin-bottom: 8px; +} + +.metric-card .value { + font-size: 28px; + font-weight: 700; +} + +.section-card { + border-radius: 8px; +} + +.form-label { + margin: 0 0 8px; + color: #4e5969; + font-size: 14px; + font-weight: 500; +} + +.form-help { + margin: -12px 0 18px; + color: #86909c; + font-size: 13px; + line-height: 1.6; +} + +.permission-tree-panel { + height: 360px; + overflow: auto; + padding: 12px 16px; + border: 1px solid #e5e6eb; + border-radius: 6px; + background: #fbfcfd; +} + +.modal-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; +} + +.profile-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 420px; + gap: 16px; + align-items: start; +} + +.profile-summary { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 24px; +} + +.profile-summary h2 { + margin: 0 0 6px; + font-size: 22px; +} + +.profile-summary p { + margin: 0; + color: #86909c; +} + +.profile-info-list { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.profile-info-list div { + padding: 14px; + border: 1px solid #e5e6eb; + border-radius: 8px; + background: #fbfcfd; +} + +.profile-info-list span { + display: block; + margin-bottom: 8px; + color: #86909c; + font-size: 13px; +} + +.profile-info-list strong { + color: #1d2129; + font-weight: 500; + word-break: break-word; +} + +@media (max-width: 900px) { + .login-page { + grid-template-columns: 1fr; + } + + .login-visual { + display: none; + } + + .page-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .profile-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .content { + padding: 12px; + } + + .page-grid { + grid-template-columns: 1fr; + } + + .profile-info-list { + grid-template-columns: 1fr; + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..015f0dc --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,1008 @@ +import { useEffect, useMemo, useState, type ReactElement, type ReactNode } from 'react'; +import axios, { AxiosError } from 'axios'; +import { + Avatar, + Button, + Card, + Form, + Input, + Layout, + Menu, + Message, + Modal, + Select, + Space, + Table, + Tag, + Tree, + Typography, + ConfigProvider, +} from '@arco-design/web-react'; +import { + IconApps, + IconDashboard, + IconFile, + IconLock, + IconMenuFold, + IconMenuUnfold, + IconPlus, + IconSafe, + IconSettings, + IconUser, + IconVideoCamera, +} from '@arco-design/web-react/icon'; +import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; +import zhCN from '@arco-design/web-react/es/locale/zh-CN'; +import './App.css'; + +const { Header, Sider, Content } = Layout; +const { Text } = Typography; + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000'; +const TOKEN_KEY = 'cth_tk_admin_token'; + +type ApiResponse = { + code: number; + data: T; + message: string; + timestamp: string; + requestId: string; + cost: string; +}; + +type PageData = { + list: T[]; + pagination: { + page: number; + pageSize: number; + total: number; + }; +}; + +type AdminProfile = { + id: number; + username: string; + nickname?: string; + email?: string; + status: string; + isSuperAdmin: boolean; + roles: Role[]; + createdAt: string; +}; + +type Drama = { + id: number; + title: string; + type: string; + status: string; + coverUrl: string; + isRecommended: boolean; + createdAt: string; +}; + +type AppUser = { + id: number; + tiktokOpenId: string; + nickname?: string; + status: string; + createdAt: string; +}; + +type Permission = { + id: number; + code: string; + name: string; + description?: string; +}; + +type Role = { + id: number; + code: string; + name: string; + description?: string; + permissionIds?: number[]; + permissions: Permission[]; +}; + +type AdminUser = { + id: number; + username: string; + nickname?: string; + email?: string; + status: string; + isSuperAdmin: boolean; + roles: Role[]; + createdAt: string; +}; + +type RequestLog = { + id: number; + requestId: string; + method: string; + path: string; + statusCode: number; + responseCode: number; + message: string; + costMs: number; + createdAt: string; +}; + +const api = axios.create({ baseURL: API_BASE_URL }); + +api.interceptors.request.use((config) => { + const token = localStorage.getItem(TOKEN_KEY); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +api.interceptors.response.use( + (response) => response, + (error: AxiosError>) => { + const message = error.response?.data?.message || error.message || '请求失败'; + Message.error(message); + return Promise.reject(error); + }, +); + +async function fetchPage(url: string): Promise> { + const response = await api.get>>(url); + return response.data.data; +} + +function usePageData(url: string) { + const [loading, setLoading] = useState(false); + const [data, setData] = useState>({ + list: [], + pagination: { page: 1, pageSize: 20, total: 0 }, + }); + + const load = async () => { + setLoading(true); + try { + setData(await fetchPage(url)); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void load(); + }, [url]); + + return { data, loading, reload: load }; +} + +function LoginPage() { + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + + const onSubmit = async (values: { username: string; password: string }) => { + setLoading(true); + try { + const response = await api.post>('/api/admin/v1/auth/login', values); + localStorage.setItem(TOKEN_KEY, response.data.data.accessToken); + Message.success('登录成功'); + navigate('/dashboard', { replace: true }); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+ CT + cth-tk-admin +
+
+ TikTok 短剧运营后台 +

CTH TikTok 短剧管理后台

+

集中处理短剧内容、剧集发布、订单支付、播放日志和系统权限,帮助运营人员稳定管理短剧业务。

+
+
+
内容短剧与剧集管理
+
权限角色化后台授权
+
日志请求与播放追踪
+
+
+
+
+
+ CT +
+

后台登录

+

使用管理员账号进入控制台。

+
+
+
+ + } placeholder="admin" /> + + + } placeholder="admin123" /> + + +
+
API 服务:{API_BASE_URL}
+
+
+
+ ); +} + +function ProtectedRoute({ children }: { children: ReactElement }) { + if (!localStorage.getItem(TOKEN_KEY)) { + return ; + } + return children; +} + +type MenuConfig = { + key: string; + label: string; + icon?: ReactNode; + children?: MenuConfig[]; +}; + +const SYSTEM_MENU_KEY = '/system'; + +const menuItems: MenuConfig[] = [ + { key: '/dashboard', label: '工作台', icon: }, + { key: '/dramas', label: '短剧管理', icon: }, + { key: '/users', label: '用户管理', icon: }, + { + key: SYSTEM_MENU_KEY, + label: '系统管理', + icon: , + children: [ + { key: '/permissions', label: '权限管理', icon: }, + { key: '/roles', label: '角色管理', icon: }, + { key: '/personnel', label: '人员管理', icon: }, + ], + }, + { key: '/logs', label: '日志管理', icon: }, +]; + +function findCurrentMenu(pathname: string) { + if (pathname.startsWith('/profile')) { + return { item: { key: '/profile', label: '个人中心' } }; + } + for (const item of menuItems) { + if (item.children) { + const child = item.children.find((childItem) => pathname.startsWith(childItem.key)); + if (child) { + return { item: child, parent: item }; + } + } + if (pathname.startsWith(item.key)) { + return { item }; + } + } + return undefined; +} + +function renderMenuItem(item: MenuConfig) { + if (item.children) { + return ( + {item.icon}{item.label}}> + {item.children.map((child) => renderMenuItem(child))} + + ); + } + + return {item.icon}{item.label}; +} + +function AdminLayout() { + const navigate = useNavigate(); + const location = useLocation(); + const [collapsed, setCollapsed] = useState(false); + const [profile, setProfile] = useState(null); + const [passwordVisible, setPasswordVisible] = useState(false); + const [changingPassword, setChangingPassword] = useState(false); + const current = useMemo(() => findCurrentMenu(location.pathname), [location.pathname]); + const [openMenuKeys, setOpenMenuKeys] = useState(current?.parent ? [current.parent.key] : []); + + useEffect(() => { + api.get>('/api/admin/v1/auth/profile') + .then((response) => setProfile(response.data.data)) + .catch(() => { + localStorage.removeItem(TOKEN_KEY); + navigate('/login', { replace: true }); + }); + }, [navigate]); + + useEffect(() => { + if (current?.parent && !openMenuKeys.includes(current.parent.key)) { + setOpenMenuKeys((keys) => [...keys, current.parent!.key]); + } + }, [current, openMenuKeys]); + + const logout = () => { + localStorage.removeItem(TOKEN_KEY); + navigate('/login', { replace: true }); + }; + + const changePassword = async (values: { oldPassword: string; newPassword: string; confirmPassword: string }) => { + if (values.newPassword !== values.confirmPassword) { + Message.error('两次输入的新密码不一致'); + return; + } + setChangingPassword(true); + try { + await api.patch('/api/admin/v1/auth/password', { + oldPassword: values.oldPassword, + newPassword: values.newPassword, + }); + Message.success('密码修改成功,请重新登录'); + setPasswordVisible(false); + localStorage.removeItem(TOKEN_KEY); + navigate('/login', { replace: true }); + } finally { + setChangingPassword(false); + } + }; + + return ( + + +
+ TK + {!collapsed && TK短剧管理后台} +
+ navigate(key)} + onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)} + > + {menuItems.map((item) => renderMenuItem(item))} + +
+ +
+ + + + + +
+ + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + +
+ setPasswordVisible(false)} footer={null}> +
+ + + + + + + + + + +
+
+
+ ); +} + +function DashboardPage() { + const dramas = usePageData('/api/admin/v1/dramas'); + const users = usePageData('/api/admin/v1/users'); + const logs = usePageData('/api/admin/v1/logs/requests'); + + return ( + <> +
+ + + + +
+ + + + + ); +} + +function MetricCard({ label, value }: { label: string; value: number | string }) { + return
{label}
{value}
; +} + +function DramasPage() { + const { data, loading, reload } = usePageData('/api/admin/v1/dramas'); + const [visible, setVisible] = useState(false); + const [creating, setCreating] = useState(false); + + const createDrama = async (values: Partial) => { + setCreating(true); + try { + await api.post('/api/admin/v1/dramas', { ...values, status: values.status || 'draft' }); + Message.success('短剧创建成功'); + setVisible(false); + await reload(); + } finally { + setCreating(false); + } + }; + + return ( + } onClick={() => setVisible(true)}>新增短剧}> + {status} }, + { title: '推荐', dataIndex: 'isRecommended', width: 140, render: (value) => value ? : }, + ]} /> + setVisible(false)} footer={null}> +
+ + + + + + +
+ + ); +} + +function UsersPage() { + const { data, loading, reload } = usePageData('/api/admin/v1/users'); + const update状态 = async (id: number, status: 'active' | 'disabled') => { + await api.patch(`/api/admin/v1/users/${id}/status`, { status }); + Message.success('用户状态已更新'); + await reload(); + }; + + return ( + +
{status} }, + { title: '操作', width: 160, render: (_, record: AppUser) => }, + ]} /> + + ); +} + +function PermissionsPage() { + const { data, loading } = usePageData('/api/admin/v1/permissions'); + return
; +} + +type PermissionTreeNode = { + key: string; + title: string; + children?: PermissionTreeNode[]; +}; + +type PermissionModuleConfig = { + prefix: string; + title: string; + subject: string; +}; + +const systemPermissionModules: PermissionModuleConfig[] = [ + { prefix: 'permission', title: '权限管理', subject: '权限' }, + { prefix: 'role', title: '角色管理', subject: '角色' }, + { prefix: 'admin-user', title: '人员管理', subject: '人员' }, + { prefix: 'user', title: '用户管理', subject: '用户' }, + { prefix: 'log', title: '日志管理', subject: '日志' }, +]; + +const businessPermissionModules: PermissionModuleConfig[] = [ + { prefix: 'drama', title: '短剧管理', subject: '短剧' }, + { prefix: 'episode', title: '剧集管理', subject: '剧集' }, + { prefix: 'media', title: '媒体管理', subject: '媒体' }, + { prefix: 'payment', title: '支付管理', subject: '支付' }, +]; + +const actionNames: Record = { + read: '查看', + create: '创建', + update: '更新', + delete: '删除', + manage: '管理', +}; + +function permissionKey(id: number) { + return `permission:${id}`; +} + +function permissionIdFromKey(key: string) { + const id = Number(key.replace('permission:', '')); + return Number.isInteger(id) ? id : null; +} + +function getPermissionPrefix(code: string) { + return code.split(':')[0]; +} + +function getPermissionAction(code: string) { + return code.split(':')[1] || code; +} + +function getPermissionTitle(permission: Permission, module?: PermissionModuleConfig) { + const action = actionNames[getPermissionAction(permission.code)] || permission.name; + return module ? `${action}${module.subject}` : permission.name; +} + +function buildPermissionModuleNode( + permissions: Permission[], + module: PermissionModuleConfig, +): PermissionTreeNode | null { + const children = permissions + .filter((permission) => getPermissionPrefix(permission.code) === module.prefix) + .map((permission) => ({ + key: permissionKey(permission.id), + title: getPermissionTitle(permission, module), + })); + + if (!children.length) { + return null; + } + + return { + key: `module:${module.prefix}`, + title: module.title, + children, + }; +} + +function buildPermissionTree(permissions: Permission[]): PermissionTreeNode[] { + const knownPrefixes = new Set([ + ...systemPermissionModules.map((module) => module.prefix), + ...businessPermissionModules.map((module) => module.prefix), + ]); + const systemChildren = systemPermissionModules + .map((module) => buildPermissionModuleNode(permissions, module)) + .filter((node): node is PermissionTreeNode => Boolean(node)); + const businessChildren = businessPermissionModules + .map((module) => buildPermissionModuleNode(permissions, module)) + .filter((node): node is PermissionTreeNode => Boolean(node)); + const otherPermissions = permissions + .filter((permission) => !knownPrefixes.has(getPermissionPrefix(permission.code))) + .map((permission) => ({ + key: permissionKey(permission.id), + title: permission.name, + })); + + if (otherPermissions.length) { + businessChildren.push({ + key: 'module:other', + title: '其他权限', + children: otherPermissions, + }); + } + + return [ + { key: 'group:system', title: '系统权限', children: systemChildren }, + { key: 'group:business', title: '业务权限', children: businessChildren }, + ]; +} + +function collectExpandedKeys(nodes: PermissionTreeNode[]) { + const keys: string[] = []; + const visit = (items: PermissionTreeNode[]) => { + items.forEach((item) => { + if (item.children?.length) { + keys.push(item.key); + visit(item.children); + } + }); + }; + visit(nodes); + return keys; +} + +function PermissionTreePicker({ + permissions, + value, + onChange, +}: { + permissions: Permission[]; + value: number[]; + onChange: (permissionIds: number[]) => void; +}) { + const treeData = useMemo(() => buildPermissionTree(permissions), [permissions]); + const expandedKeys = useMemo(() => collectExpandedKeys(treeData), [treeData]); + const checkedKeys = value.map((id) => permissionKey(id)); + + return ( +
+ { + onChange( + keys + .map((key) => permissionIdFromKey(key)) + .filter((id): id is number => id !== null), + ); + }} + /> +
+ ); +} + +function RolesPage() { + const { data, loading, reload } = usePageData('/api/admin/v1/roles'); + const permissions = usePageData('/api/admin/v1/permissions'); + const [createForm] = Form.useForm(); + const [createVisible, setCreateVisible] = useState(false); + const [creatingRole, setCreatingRole] = useState(false); + const [createPermissionIds, setCreatePermissionIds] = useState([]); + const [assignmentVisible, setAssignmentVisible] = useState(false); + const [selectedRole, setSelectedRole] = useState(null); + const [selectedPermissionIds, setSelectedPermissionIds] = useState([]); + const [savingPermissions, setSavingPermissions] = useState(false); + + const openCreateRole = () => { + createForm.resetFields(); + setCreatePermissionIds([]); + setCreateVisible(true); + }; + + const createRole = async (values: { code: string; name: string; description?: string }) => { + setCreatingRole(true); + try { + await api.post('/api/admin/v1/roles', { + ...values, + permissionIds: createPermissionIds, + }); + Message.success('角色创建成功'); + setCreateVisible(false); + await reload(); + } finally { + setCreatingRole(false); + } + }; + + const openAssignPermissions = (role: Role) => { + setSelectedRole(role); + setSelectedPermissionIds(role.permissions.map((permission) => permission.id)); + setAssignmentVisible(true); + }; + + const saveRolePermissions = async () => { + if (!selectedRole) { + return; + } + setSavingPermissions(true); + try { + await api.patch(`/api/admin/v1/roles/${selectedRole.id}/permissions`, { + permissionIds: selectedPermissionIds, + }); + Message.success('角色权限已更新'); + setAssignmentVisible(false); + await reload(); + } finally { + setSavingPermissions(false); + } + }; + + return ( + } onClick={openCreateRole}>新增角色}> +
{record.permissions.map((item) => {item.code})} }, + { title: '操作', width: 140, render: (_, record: Role) => }, + ]} /> + setCreateVisible(false)} footer={null}> +
+ + + +
角色的唯一标识,创建后不可修改
+ + + + + + +
权限设置
+ +
为角色分配系统权限,决定拥有此角色的用户可以进行的操作
+
+ + +
+ +
+ setAssignmentVisible(false)} + > + + + + ); +} + +function PersonnelPage() { + const { data, loading, reload } = usePageData('/api/admin/v1/admin-users'); + const roles = usePageData('/api/admin/v1/roles'); + const [createForm] = Form.useForm(); + const [createVisible, setCreateVisible] = useState(false); + const [creating, setCreating] = useState(false); + const [assignVisible, setAssignVisible] = useState(false); + const [assigning, setAssigning] = useState(false); + const [selectedAdmin, setSelectedAdmin] = useState(null); + const [selectedRoleIds, setSelectedRoleIds] = useState([]); + + const openCreate = () => { + createForm.resetFields(); + setCreateVisible(true); + }; + + const createPersonnel = async (values: { + username: string; + password: string; + nickname?: string; + email?: string; + roleIds?: number[]; + }) => { + setCreating(true); + try { + await api.post('/api/admin/v1/admin-users', { + ...values, + roleIds: values.roleIds ?? [], + }); + Message.success('人员创建成功'); + setCreateVisible(false); + await reload(); + } finally { + setCreating(false); + } + }; + + const openAssignRoles = (admin: AdminUser) => { + setSelectedAdmin(admin); + setSelectedRoleIds(admin.roles.map((role) => role.id)); + setAssignVisible(true); + }; + + const saveAssignedRoles = async () => { + if (!selectedAdmin) { + return; + } + setAssigning(true); + try { + await api.patch(`/api/admin/v1/admin-users/${selectedAdmin.id}/roles`, { + roleIds: selectedRoleIds, + }); + Message.success('角色分配已更新'); + setAssignVisible(false); + await reload(); + } finally { + setAssigning(false); + } + }; + + const updateStatus = async (id: number, status: 'active' | 'disabled') => { + await api.patch(`/api/admin/v1/admin-users/${id}/status`, { status }); + Message.success('人员状态已更新'); + await reload(); + }; + + const roleOptions = roles.data.list.map((role) => ( + {role.name} + )); + + return ( + } onClick={openCreate}>新增人员}> +
{record.isSuperAdmin ? 超级管理员 : record.roles.map((role) => {role.name})} }, + { title: '状态', dataIndex: 'status', width: 120, render: (status) => {status === 'active' ? '启用' : '禁用'} }, + { title: '创建时间', dataIndex: 'createdAt', width: 190 }, + { + title: '操作', + width: 220, + render: (_, record: AdminUser) => ( + + + + + ), + }, + ]} /> + setCreateVisible(false)} footer={null}> +
+ + + + + + + + + + + + + + + +
+ + +
+ +
+ setAssignVisible(false)} + > + + + + ); +} + +function ProfilePage() { + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [profile, setProfile] = useState(null); + + const loadProfile = async () => { + setLoading(true); + try { + const response = await api.get>('/api/admin/v1/auth/profile'); + setProfile(response.data.data); + form.setFieldsValue({ + nickname: response.data.data.nickname, + email: response.data.data.email, + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void loadProfile(); + }, []); + + const updateProfile = async (values: { nickname?: string; email?: string }) => { + setSaving(true); + try { + const response = await api.patch>('/api/admin/v1/auth/profile', values); + setProfile(response.data.data); + Message.success('个人信息已更新'); + } finally { + setSaving(false); + } + }; + + return ( +
+ +
+ {profile?.username?.slice(0, 1).toUpperCase() || 'A'} +
+

{profile?.nickname || profile?.username || '管理员'}

+

{profile?.email || '未设置邮箱'}

+
+
+
+
用户名{profile?.username || '-'}
+
状态{profile?.status === 'active' ? '启用' : '禁用'}
+
角色{profile?.isSuperAdmin ? '超级管理员' : profile?.roles.map((role) => role.name).join('、') || '-'}
+
创建时间{profile?.createdAt || '-'}
+
+
+ +
+ + + + + + + + +
+
+ ); +} + +function LogsPage() { + const { data, loading } = usePageData('/api/admin/v1/logs/requests'); + return ; +} + +function RequestLogTable({ data, loading }: { data: RequestLog[]; loading: boolean }) { + return ( +
{value} }, + { title: '路径', dataIndex: 'path', ellipsis: true }, + { title: '状态', dataIndex: 'statusCode', width: 100, render: (value) => {value} }, + { title: '耗时', dataIndex: 'costMs', width: 100, render: (value) => String(value) + 'ms' }, + { title: '消息', dataIndex: 'message', width: 180 }, + ]} /> + ); +} + +function App() { + return ( + + + + } /> + } /> + + + + ); +} + +export default App; diff --git a/src/assets/hero.png b/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/src/assets/hero.png differ diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/vite.svg b/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..0ffbe38 --- /dev/null +++ b/src/index.css @@ -0,0 +1,25 @@ +* { + box-sizing: border-box; +} + +html, +body, +#root { + width: 100%; + min-width: 320px; + min-height: 100%; + margin: 0; +} + +body { + color: #1d2129; + background: #f2f3f5; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +button, +input, +textarea, +select { + font: inherit; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..1dae726 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import '@arco-design/web-react/dist/css/arco.css'; +import './index.css'; +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..7f42e5f --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "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..8455dcb --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +})