build: 引入tailwindcss并迁移原有样式到原子类

1. 新增postcss、autoprefixer、tailwindcss依赖并初始化配置
2. 移除全局及页面级CSS文件,将样式替换为tailwind原子类
3. 重构API调用逻辑,拆分接口请求函数
4. 统一项目样式规范,优化布局与组件样式
This commit is contained in:
2026-07-02 13:38:03 +08:00
parent 526052d633
commit e160476ecf
27 changed files with 947 additions and 499 deletions

View File

@@ -21,6 +21,9 @@
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^6.0.2",
"autoprefixer": "^10.5.2",
"postcss": "^8.5.16",
"tailwindcss": "^3.4.19",
"typescript": "~6.0.2",
"vite": "^8.1.0"
}

591
pnpm-lock.yaml generated
View File

@@ -35,16 +35,29 @@ importers:
version: 18.3.5(@types/react@18.3.18)
'@vitejs/plugin-react':
specifier: ^6.0.2
version: 6.0.3(vite@8.1.0(@types/node@24.13.2))
version: 6.0.3(vite@8.1.0(@types/node@24.13.2)(jiti@1.21.7))
autoprefixer:
specifier: ^10.5.2
version: 10.5.2(postcss@8.5.16)
postcss:
specifier: ^8.5.16
version: 8.5.16
tailwindcss:
specifier: ^3.4.19
version: 3.4.19
typescript:
specifier: ~6.0.2
version: 6.0.3
vite:
specifier: ^8.1.0
version: 8.1.0(@types/node@24.13.2)
version: 8.1.0(@types/node@24.13.2)(jiti@1.21.7)
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@arco-design/color@0.4.0':
resolution: {integrity: sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g==}
@@ -67,12 +80,37 @@ packages:
'@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@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
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
'@nodelib/fs.stat@2.0.5':
resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
engines: {node: '>= 8'}
'@nodelib/fs.walk@1.2.8':
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@oxc-project/types@0.137.0':
resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==}
@@ -208,9 +246,26 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
autoprefixer@10.5.2:
resolution: {integrity: sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
postcss: ^8.1.0
axios@1.18.1:
resolution: {integrity: sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==}
@@ -220,10 +275,39 @@ packages:
b-validate@1.5.3:
resolution: {integrity: sha512-iCvCkGFskbaYtfQ0a3GmcQCHl/Sv1GufXFGuUQ+FE+WJa7A/espLOuFIn09B944V8/ImPj71T4+rTASxO2PAuA==}
baseline-browser-mapping@2.10.40:
resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==}
engines: {node: '>=6.0.0'}
hasBin: true
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browserslist@4.28.4:
resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
camelcase-css@2.0.1:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
caniuse-lite@1.0.30001800:
resolution: {integrity: sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
@@ -243,6 +327,10 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
@@ -250,6 +338,11 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -276,6 +369,12 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
@@ -283,6 +382,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
electron-to-chromium@1.5.383:
resolution: {integrity: sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -299,6 +401,17 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
fastq@1.20.1:
resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -308,6 +421,10 @@ packages:
picomatch:
optional: true
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
focus-lock@1.3.6:
resolution: {integrity: sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==}
engines: {node: '>=10'}
@@ -325,6 +442,9 @@ packages:
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
engines: {node: '>= 6'}
fraction.js@5.3.4:
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -341,6 +461,14 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@@ -364,6 +492,30 @@ packages:
is-arrayish@0.3.4:
resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-core-module@2.16.2:
resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==}
engines: {node: '>= 0.4'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -441,6 +593,13 @@ packages:
resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==}
engines: {node: '>= 12.0.0'}
lilconfig@3.1.3:
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
engines: {node: '>=14'}
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
@@ -452,6 +611,14 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
@@ -463,11 +630,22 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
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
node-releases@2.0.50:
resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==}
engines: {node: '>=18'}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
number-precision@1.6.0:
resolution: {integrity: sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ==}
@@ -475,13 +653,75 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.2:
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
engines: {node: '>=8.6'}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
postcss-import@15.1.0:
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
engines: {node: '>=14.0.0'}
peerDependencies:
postcss: ^8.0.0
postcss-js@4.1.0:
resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.4.21
postcss-load-config@6.0.1:
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
engines: {node: '>= 18'}
peerDependencies:
jiti: '>=1.21.0'
postcss: '>=8.0.9'
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
jiti:
optional: true
postcss:
optional: true
tsx:
optional: true
yaml:
optional: true
postcss-nested@6.2.0:
resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
postcss-selector-parser@6.1.4:
resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==}
engines: {node: '>=4'}
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.16:
resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==}
engines: {node: ^10 || ^12 || >=14}
@@ -493,6 +733,9 @@ packages:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
react-clientside-effect@1.2.8:
resolution: {integrity: sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==}
peerDependencies:
@@ -545,14 +788,33 @@ packages:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
resize-observer-polyfill@1.5.1:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
resolve@1.22.12:
resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==}
engines: {node: '>= 0.4'}
hasBin: true
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
rolldown@1.1.3:
resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
@@ -572,10 +834,38 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
hasBin: true
supports-preserve-symlinks-flag@1.0.0:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
tailwindcss@3.4.19:
resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==}
engines: {node: '>=14.0.0'}
hasBin: true
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tinyglobby@0.2.17:
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
engines: {node: '>=12.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -587,6 +877,12 @@ packages:
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
use-callback-ref@1.3.3:
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
engines: {node: '>=10'}
@@ -607,6 +903,9 @@ packages:
'@types/react':
optional: true
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite@8.1.0:
resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -652,6 +951,8 @@ packages:
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@arco-design/color@0.4.0':
dependencies:
color: 3.2.1
@@ -695,6 +996,20 @@ snapshots:
tslib: 2.8.1
optional: true
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
dependencies:
'@emnapi/core': 1.11.1
@@ -702,6 +1017,18 @@ snapshots:
'@tybys/wasm-util': 0.10.3
optional: true
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
run-parallel: 1.2.0
'@nodelib/fs.stat@2.0.5': {}
'@nodelib/fs.walk@1.2.8':
dependencies:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@oxc-project/types@0.137.0': {}
'@rolldown/binding-android-arm64@1.1.3':
@@ -775,10 +1102,10 @@ snapshots:
'@types/prop-types': 15.7.15
csstype: 3.2.3
'@vitejs/plugin-react@6.0.3(vite@8.1.0(@types/node@24.13.2))':
'@vitejs/plugin-react@6.0.3(vite@8.1.0(@types/node@24.13.2)(jiti@1.21.7))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.1.0(@types/node@24.13.2)
vite: 8.1.0(@types/node@24.13.2)(jiti@1.21.7)
agent-base@6.0.2:
dependencies:
@@ -786,8 +1113,26 @@ snapshots:
transitivePeerDependencies:
- supports-color
any-promise@1.3.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.2
arg@5.0.2: {}
asynckit@0.4.0: {}
autoprefixer@10.5.2(postcss@8.5.16):
dependencies:
browserslist: 4.28.4
caniuse-lite: 1.0.30001800
fraction.js: 5.3.4
picocolors: 1.1.1
postcss: 8.5.16
postcss-value-parser: 4.2.0
axios@1.18.1:
dependencies:
follow-redirects: 1.16.0
@@ -802,11 +1147,43 @@ snapshots:
b-validate@1.5.3: {}
baseline-browser-mapping@2.10.40: {}
binary-extensions@2.3.0: {}
braces@3.0.3:
dependencies:
fill-range: 7.1.1
browserslist@4.28.4:
dependencies:
baseline-browser-mapping: 2.10.40
caniuse-lite: 1.0.30001800
electron-to-chromium: 1.5.383
node-releases: 2.0.50
update-browserslist-db: 1.2.3(browserslist@4.28.4)
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001800: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
@@ -829,10 +1206,14 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@4.1.1: {}
compute-scroll-into-view@1.0.20: {}
cookie@1.1.1: {}
cssesc@3.0.0: {}
csstype@3.2.3: {}
dayjs@1.11.21: {}
@@ -847,6 +1228,10 @@ snapshots:
detect-node-es@1.1.0: {}
didyoumean@1.2.2: {}
dlv@1.1.3: {}
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.29.7
@@ -858,6 +1243,8 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
electron-to-chromium@1.5.383: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -873,10 +1260,28 @@ snapshots:
has-tostringtag: 1.0.2
hasown: 2.0.4
escalade@3.2.0: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
micromatch: 4.0.8
fastq@1.20.1:
dependencies:
reusify: 1.1.0
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
focus-lock@1.3.6:
dependencies:
tslib: 2.8.1
@@ -891,6 +1296,8 @@ snapshots:
hasown: 2.0.4
mime-types: 2.1.35
fraction.js@5.3.4: {}
fsevents@2.3.3:
optional: true
@@ -914,6 +1321,14 @@ snapshots:
dunder-proto: 1.0.1
es-object-atoms: 1.1.2
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
glob-parent@6.0.2:
dependencies:
is-glob: 4.0.3
gopd@1.2.0: {}
has-symbols@1.1.0: {}
@@ -935,6 +1350,24 @@ snapshots:
is-arrayish@0.3.4: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-core-module@2.16.2:
dependencies:
hasown: 2.0.4
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
jiti@1.21.7: {}
js-tokens@4.0.0: {}
lightningcss-android-arm64@1.32.0:
@@ -986,6 +1419,10 @@ snapshots:
lightningcss-win32-arm64-msvc: 1.32.0
lightningcss-win32-x64-msvc: 1.32.0
lilconfig@3.1.3: {}
lines-and-columns@1.2.4: {}
lodash@4.18.1: {}
loose-envify@1.4.0:
@@ -994,6 +1431,13 @@ snapshots:
math-intrinsics@1.1.0: {}
merge2@1.4.1: {}
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.2
mime-db@1.52.0: {}
mime-types@2.1.35:
@@ -1002,16 +1446,67 @@ snapshots:
ms@2.1.3: {}
mz@2.7.0:
dependencies:
any-promise: 1.3.0
object-assign: 4.1.1
thenify-all: 1.6.0
nanoid@3.3.15: {}
node-releases@2.0.50: {}
normalize-path@3.0.0: {}
number-precision@1.6.0: {}
object-assign@4.1.1: {}
object-hash@3.0.0: {}
path-parse@1.0.7: {}
picocolors@1.1.1: {}
picomatch@2.3.2: {}
picomatch@4.0.4: {}
pify@2.3.0: {}
pirates@4.0.7: {}
postcss-import@15.1.0(postcss@8.5.16):
dependencies:
postcss: 8.5.16
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.12
postcss-js@4.1.0(postcss@8.5.16):
dependencies:
camelcase-css: 2.0.1
postcss: 8.5.16
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.16):
dependencies:
lilconfig: 3.1.3
optionalDependencies:
jiti: 1.21.7
postcss: 8.5.16
postcss-nested@6.2.0(postcss@8.5.16):
dependencies:
postcss: 8.5.16
postcss-selector-parser: 6.1.4
postcss-selector-parser@6.1.4:
dependencies:
cssesc: 3.0.0
util-deprecate: 1.0.2
postcss-value-parser@4.2.0: {}
postcss@8.5.16:
dependencies:
nanoid: 3.3.15
@@ -1026,6 +1521,8 @@ snapshots:
proxy-from-env@2.1.0: {}
queue-microtask@1.2.3: {}
react-clientside-effect@1.2.8(react@18.3.1):
dependencies:
'@babel/runtime': 7.29.7
@@ -1080,8 +1577,25 @@ snapshots:
dependencies:
loose-envify: 1.4.0
read-cache@1.0.0:
dependencies:
pify: 2.3.0
readdirp@3.6.0:
dependencies:
picomatch: 2.3.2
resize-observer-polyfill@1.5.1: {}
resolve@1.22.12:
dependencies:
es-errors: 1.3.0
is-core-module: 2.16.2
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
reusify@1.1.0: {}
rolldown@1.1.3:
dependencies:
'@oxc-project/types': 0.137.0
@@ -1103,6 +1617,10 @@ snapshots:
'@rolldown/binding-win32-arm64-msvc': 1.1.3
'@rolldown/binding-win32-x64-msvc': 1.1.3
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
@@ -1121,17 +1639,77 @@ snapshots:
source-map-js@1.2.1: {}
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
commander: 4.1.1
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.7
tinyglobby: 0.2.17
ts-interface-checker: 0.1.13
supports-preserve-symlinks-flag@1.0.0: {}
tailwindcss@3.4.19:
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
chokidar: 3.6.0
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.3.3
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.21.7
lilconfig: 3.1.3
micromatch: 4.0.8
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.1.1
postcss: 8.5.16
postcss-import: 15.1.0(postcss@8.5.16)
postcss-js: 4.1.0(postcss@8.5.16)
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.16)
postcss-nested: 6.2.0(postcss@8.5.16)
postcss-selector-parser: 6.1.4
resolve: 1.22.12
sucrase: 3.35.1
transitivePeerDependencies:
- tsx
- yaml
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
thenify@3.3.1:
dependencies:
any-promise: 1.3.0
tinyglobby@0.2.17:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
ts-interface-checker@0.1.13: {}
tslib@2.8.1: {}
typescript@6.0.3: {}
undici-types@7.18.2: {}
update-browserslist-db@1.2.3(browserslist@4.28.4):
dependencies:
browserslist: 4.28.4
escalade: 3.2.0
picocolors: 1.1.1
use-callback-ref@1.3.3(@types/react@18.3.18)(react@18.3.1):
dependencies:
react: 18.3.1
@@ -1147,7 +1725,9 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.18
vite@8.1.0(@types/node@24.13.2):
util-deprecate@1.0.2: {}
vite@8.1.0(@types/node@24.13.2)(jiti@1.21.7):
dependencies:
lightningcss: 1.32.0
picomatch: 4.0.4
@@ -1157,3 +1737,4 @@ snapshots:
optionalDependencies:
'@types/node': 24.13.2
fsevents: 2.3.3
jiti: 1.21.7

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,28 +0,0 @@
.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;
}
@media (max-width: 900px) {
.page-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.page-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -4,10 +4,9 @@ import { usePageData } from '../hooks/usePageData';
import { hasPermissions } from '../utils/permission';
import { NoAccessPage } from '../components/NoAccessPage';
import { RequestLogTable } from '../components/RequestLogTable';
import './index.css';
function MetricCard({ label, value }: { label: string; value: number | string }) {
return <Card className="metric-card"><div className="label">{label}</div><div className="value">{value}</div></Card>;
return <Card><div className="mb-2 text-muted">{label}</div><div className="text-[28px] font-bold">{value}</div></Card>;
}
function DashboardPage({ profile }: { profile: AdminProfile | null }) {
@@ -21,7 +20,7 @@ function DashboardPage({ profile }: { profile: AdminProfile | null }) {
return (
<>
<div className="page-grid">
<div className="mb-4 grid grid-cols-1 gap-4 sm:grid-cols-2 min-[901px]:grid-cols-4">
{canReadDramas && <MetricCard label="短剧管理" value={dramas.data.pagination.total} />}
{canReadUsers && <MetricCard label="用户管理" value={users.data.pagination.total} />}
{canReadLogs && <MetricCard label="请求日志" value={logs.data.pagination.total} />}

View File

@@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import { Button, Card, Grid, Message, Statistic, Table } from '@arco-design/web-react';
import { IconRefresh } from '@arco-design/web-react/icon';
import type { ApiResponse, DataOverview, DramaMetric, EpisodeMetric, SyncJob } from '../api/types';
import { api } from '../api/client';
import type { DataOverview, DramaMetric, EpisodeMetric, SyncJob } from '../api/types';
import { getDataOverview, syncDataMetrics } from '../api/data';
import { usePageData } from '../hooks/usePageData';
const { Row, Col } = Grid;
@@ -22,8 +22,7 @@ function DataPage() {
const jobs = usePageData<SyncJob>('/api/admin/v1/data/sync-jobs');
const loadOverview = async () => {
const response = await api.get<ApiResponse<DataOverview>>('/api/admin/v1/data/overview');
setOverview(response.data.data);
setOverview(await getDataOverview());
};
useEffect(() => {
@@ -33,7 +32,7 @@ function DataPage() {
const syncData = async () => {
setSyncing(true);
try {
await api.post('/api/admin/v1/data/sync', {});
await syncDataMetrics();
Message.success('数据同步完成');
await Promise.all([loadOverview(), dramas.reload(), episodes.reload(), jobs.reload()]);
} finally {

View File

@@ -18,8 +18,16 @@ import {
Tag,
} from "@arco-design/web-react";
import { IconArrowLeft, IconPlus, IconRefresh } from "@arco-design/web-react/icon";
import type { ApiResponse, Drama, Episode, UploadTask } from "../api/types";
import { api } from "../api/client";
import type { Drama, Episode } from "../api/types";
import {
configureDramaEpisodes,
createDrama as createDramaApi,
getDrama,
getDramaEpisodes,
updateDrama,
updateEpisode,
} from "../api/dramas";
import { completeUploadTask, createUploadTask, syncReviewStatus } from "../api/media";
import { usePageData } from "../hooks/usePageData";
const { Row, Col } = Grid;
@@ -65,7 +73,7 @@ function DramaListPage() {
const createDrama = async (values: Partial<Drama>) => {
setCreating(true);
try {
await api.post("/api/admin/v1/dramas", values);
await createDramaApi(values);
Message.success("短剧创建成功");
setVisible(false);
await reload();
@@ -218,13 +226,10 @@ function DramaDetailPage({ dramaId }: { dramaId: number }) {
const loadDetail = async () => {
setLoading(true);
try {
const [dramaResponse, episodeResponse] = await Promise.all([
api.get<ApiResponse<Drama>>(`/api/admin/v1/dramas/${dramaId}`),
api.get<ApiResponse<{ list: Episode[] }>>(`/api/admin/v1/dramas/${dramaId}/episodes?pageSize=200`),
]);
setDrama(dramaResponse.data.data);
setEpisodeCount(dramaResponse.data.data.totalEpisodes || 1);
setEpisodes(episodeResponse.data.data.list);
const [dramaData, episodeList] = await Promise.all([getDrama(dramaId), getDramaEpisodes(dramaId)]);
setDrama(dramaData);
setEpisodeCount(dramaData.totalEpisodes || 1);
setEpisodes(episodeList);
} finally {
setLoading(false);
}
@@ -235,15 +240,13 @@ function DramaDetailPage({ dramaId }: { dramaId: number }) {
}, [dramaId]);
const configureEpisodes = async () => {
await api.post(`/api/admin/v1/dramas/${dramaId}/episodes/batch`, {
episodeCount,
});
await configureDramaEpisodes(dramaId, episodeCount);
Message.success("剧集数量已更新");
await loadDetail();
};
const updateDramaStatus = async (status: string) => {
await api.patch(`/api/admin/v1/dramas/${dramaId}`, { status });
await updateDrama(dramaId, { status });
Message.success("短剧状态已更新");
await loadDetail();
};
@@ -251,14 +254,14 @@ function DramaDetailPage({ dramaId }: { dramaId: number }) {
const uploadEpisodeVideo = async (episode: Episode, file: File) => {
setUploadingEpisodeId(episode.id);
try {
const task = await api.post<ApiResponse<UploadTask>>("/api/admin/v1/media/upload-tasks", {
const task = await createUploadTask({
episodeId: episode.id,
fileName: file.name,
contentType: file.type || "video/mp4",
fileSize: file.size,
});
await api.patch(`/api/admin/v1/media/upload-tasks/${task.data.data.jobId}/complete`, {});
await api.post("/api/admin/v1/media/review-status/sync", {});
await completeUploadTask(task.jobId);
await syncReviewStatus();
Message.success("视频上传并审核通过");
await loadDetail();
} finally {
@@ -267,7 +270,7 @@ function DramaDetailPage({ dramaId }: { dramaId: number }) {
};
const publishEpisode = async (episode: Episode) => {
await api.patch(`/api/admin/v1/episodes/${episode.id}`, {
await updateEpisode(episode.id, {
publishStatus: "published",
status: "published",
});

View File

@@ -1,162 +0,0 @@
.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;
}
@media (max-width: 900px) {
.login-page {
grid-template-columns: 1fr;
}
.login-visual {
display: none;
}
}

View File

@@ -2,9 +2,8 @@ import { useState } from 'react';
import { Button, Form, Input, Message } from '@arco-design/web-react';
import { IconLock, IconUser } from '@arco-design/web-react/icon';
import { useNavigate } from 'react-router-dom';
import type { ApiResponse } from '../api/types';
import { API_BASE_URL, TOKEN_KEY, api } from '../api/client';
import './index.css';
import { login } from '../api/auth';
import { API_BASE_URL, TOKEN_KEY } from '../api/client';
function LoginPage() {
const navigate = useNavigate();
@@ -13,8 +12,8 @@ function LoginPage() {
const onSubmit = async (values: { username: string; password: string }) => {
setLoading(true);
try {
const response = await api.post<ApiResponse<{ accessToken: string }>>('/api/admin/v1/auth/login', values);
localStorage.setItem(TOKEN_KEY, response.data.data.accessToken);
const data = await login(values);
localStorage.setItem(TOKEN_KEY, data.accessToken);
Message.success('登录成功');
navigate('/dashboard', { replace: true });
} finally {
@@ -23,30 +22,30 @@ function LoginPage() {
};
return (
<div className="login-page">
<div className="login-visual">
<div className="login-logo">
<span className="logo-mark">CT</span>
<div className="grid min-h-screen grid-cols-1 bg-[#f7f8fa] min-[901px]:grid-cols-[minmax(520px,0.95fr)_minmax(420px,1fr)]">
<div className="relative hidden flex-col justify-between overflow-hidden px-14 pb-12 pt-11 text-white after:pointer-events-none after:absolute after:inset-0 after:bg-[linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0))] min-[901px]:flex bg-[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%)]">
<div className="relative z-[1] flex items-center gap-2.5 font-bold">
<span className="grid h-7 w-7 place-items-center rounded-md bg-brand text-sm text-white">CT</span>
<span>cth-tk-admin</span>
</div>
<div className="login-brand">
<span className="login-eyebrow">TikTok </span>
<h1>CTH TikTok </h1>
<p></p>
<div className="relative z-[1] max-w-[640px]">
<span className="mb-[18px] inline-flex rounded-full border border-white/20 px-3 py-1.5 text-[13px] text-white/80">TikTok </span>
<h1 className="mb-4 mt-0 text-[42px] leading-[1.16]">CTH TikTok </h1>
<p className="m-0 max-w-[560px] text-base leading-[1.8] text-white/75"></p>
</div>
<div className="login-metrics">
<div className="login-metric"><strong></strong><span></span></div>
<div className="login-metric"><strong></strong><span></span></div>
<div className="login-metric"><strong></strong><span></span></div>
<div className="relative z-[1] grid max-w-[640px] grid-cols-3 gap-3">
<div className="rounded-lg border border-white/20 bg-white/10 p-4 backdrop-blur-[10px]"><strong className="mb-1.5 block text-xl"></strong><span className="text-white/70"></span></div>
<div className="rounded-lg border border-white/20 bg-white/10 p-4 backdrop-blur-[10px]"><strong className="mb-1.5 block text-xl"></strong><span className="text-white/70"></span></div>
<div className="rounded-lg border border-white/20 bg-white/10 p-4 backdrop-blur-[10px]"><strong className="mb-1.5 block text-xl"></strong><span className="text-white/70"></span></div>
</div>
</div>
<div className="login-panel">
<div className="login-card">
<div className="login-card-header">
<span className="login-card-mark">CT</span>
<div className="flex items-center justify-center bg-[linear-gradient(180deg,rgba(255,255,255,0.86),rgba(247,248,250,0.94)),#f7f8fa] p-12">
<div className="w-full max-w-[420px] rounded-lg border border-line bg-white p-9 shadow-login">
<div className="mb-[30px] flex items-center gap-3.5">
<span className="grid h-11 w-11 place-items-center rounded-lg bg-brand font-bold text-white">CT</span>
<div>
<h2></h2>
<p className="subtitle">使</p>
<h2 className="mb-2 mt-0 text-2xl"></h2>
<p className="m-0 text-muted">使</p>
</div>
</div>
<Form layout="vertical" onSubmit={onSubmit} initialValues={{ username: 'admin', password: 'admin123' }}>
@@ -58,7 +57,7 @@ function LoginPage() {
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading} long></Button>
</Form>
<div className="login-card-footer">API {API_BASE_URL}</div>
<div className="mt-[22px] border-t border-canvas pt-[18px] text-xs text-muted">API {API_BASE_URL}</div>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import { useState } from 'react';
import { Button, Card, Form, Input, Message, Modal, Select, Space, Table, Tag } from '@arco-design/web-react';
import { IconPlus } from '@arco-design/web-react/icon';
import type { AdminUser, Role } from '../api/types';
import { api } from '../api/client';
import { createPersonnel as createPersonnelApi, updateAdminUserRoles, updateAdminUserStatus } from '../api/personnel';
import { usePageData } from '../hooks/usePageData';
function PersonnelPage() {
@@ -30,10 +30,7 @@ function PersonnelPage() {
}) => {
setCreating(true);
try {
await api.post('/api/admin/v1/admin-users', {
...values,
roleIds: values.roleIds ?? [],
});
await createPersonnelApi(values);
Message.success('人员创建成功');
setCreateVisible(false);
await reload();
@@ -54,9 +51,7 @@ function PersonnelPage() {
}
setAssigning(true);
try {
await api.patch(`/api/admin/v1/admin-users/${selectedAdmin.id}/roles`, {
roleIds: selectedRoleIds,
});
await updateAdminUserRoles(selectedAdmin.id, selectedRoleIds);
Message.success('角色分配已更新');
setAssignVisible(false);
await reload();
@@ -66,7 +61,7 @@ function PersonnelPage() {
};
const updateStatus = async (id: number, status: 'active' | 'disabled') => {
await api.patch(`/api/admin/v1/admin-users/${id}/status`, { status });
await updateAdminUserStatus(id, status);
Message.success('人员状态已更新');
await reload();
};

View File

@@ -1,65 +0,0 @@
.profile-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 420px;
gap: 16px;
align-items: start;
}
.profile-grid .section-card:first-child {
grid-row: span 2;
}
.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) {
.profile-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.profile-info-list {
grid-template-columns: 1fr;
}
}

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import { Avatar, Button, Card, Form, Input, Message } from '@arco-design/web-react';
import { useNavigate } from 'react-router-dom';
import type { ApiResponse, AdminProfile } from '../api/types';
import { TOKEN_KEY, api } from '../api/client';
import './index.css';
import type { AdminProfile } from '../api/types';
import { changePassword as changePasswordApi, getProfile, updateProfile as updateProfileApi } from '../api/auth';
import { TOKEN_KEY } from '../api/client';
function ProfilePage() {
const navigate = useNavigate();
@@ -17,11 +17,11 @@ function ProfilePage() {
const loadProfile = async () => {
setLoading(true);
try {
const response = await api.get<ApiResponse<AdminProfile>>('/api/admin/v1/auth/profile');
setProfile(response.data.data);
const data = await getProfile();
setProfile(data);
form.setFieldsValue({
nickname: response.data.data.nickname,
email: response.data.data.email,
nickname: data.nickname,
email: data.email,
});
} finally {
setLoading(false);
@@ -35,8 +35,8 @@ function ProfilePage() {
const updateProfile = async (values: { nickname?: string; email?: string }) => {
setSaving(true);
try {
const response = await api.patch<ApiResponse<AdminProfile>>('/api/admin/v1/auth/profile', values);
setProfile(response.data.data);
const data = await updateProfileApi(values);
setProfile(data);
Message.success('个人信息已更新');
} finally {
setSaving(false);
@@ -50,7 +50,7 @@ function ProfilePage() {
}
setChangingPassword(true);
try {
await api.patch('/api/admin/v1/auth/password', {
await changePasswordApi({
oldPassword: values.oldPassword,
newPassword: values.newPassword,
});
@@ -64,20 +64,20 @@ function ProfilePage() {
};
return (
<div className="profile-grid">
<Card className="section-card" title="个人中心" loading={loading}>
<div className="profile-summary">
<div className="grid items-start gap-4 min-[901px]:grid-cols-[minmax(0,1fr)_420px]">
<Card className="section-card min-[901px]:row-span-2" title="个人中心" loading={loading}>
<div className="mb-6 flex items-center gap-4">
<Avatar size={56}>{profile?.username?.slice(0, 1).toUpperCase() || 'A'}</Avatar>
<div>
<h2>{profile?.nickname || profile?.username || '管理员'}</h2>
<p>{profile?.email || '未设置邮箱'}</p>
<h2 className="mb-1.5 mt-0 text-[22px]">{profile?.nickname || profile?.username || '管理员'}</h2>
<p className="m-0 text-muted">{profile?.email || '未设置邮箱'}</p>
</div>
</div>
<div className="profile-info-list">
<div><span></span><strong>{profile?.username || '-'}</strong></div>
<div><span></span><strong>{profile?.status === 'active' ? '启用' : '禁用'}</strong></div>
<div><span></span><strong>{profile?.isSuperAdmin ? '超级管理员' : profile?.roles.map((role) => role.name).join('、') || '-'}</strong></div>
<div><span></span><strong>{profile?.createdAt || '-'}</strong></div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="rounded-lg border border-line bg-panel p-3.5"><span className="mb-2 block text-[13px] text-muted"></span><strong className="break-words font-medium text-ink">{profile?.username || '-'}</strong></div>
<div className="rounded-lg border border-line bg-panel p-3.5"><span className="mb-2 block text-[13px] text-muted"></span><strong className="break-words font-medium text-ink">{profile?.status === 'active' ? '启用' : '禁用'}</strong></div>
<div className="rounded-lg border border-line bg-panel p-3.5"><span className="mb-2 block text-[13px] text-muted"></span><strong className="break-words font-medium text-ink">{profile?.isSuperAdmin ? '超级管理员' : profile?.roles.map((role) => role.name).join('、') || '-'}</strong></div>
<div className="rounded-lg border border-line bg-panel p-3.5"><span className="mb-2 block text-[13px] text-muted"></span><strong className="break-words font-medium text-ink">{profile?.createdAt || '-'}</strong></div>
</div>
</Card>
<Card className="section-card" title="修改个人信息">

View File

@@ -137,7 +137,7 @@ export function PermissionTreePicker({
const checkedKeys = value.map((id) => permissionKey(id));
return (
<div className="permission-tree-panel">
<div className="mb-[15px] h-[360px] overflow-auto rounded-md border border-line bg-panel px-4 py-3">
<Tree
blockNode
checkable

View File

@@ -1,23 +0,0 @@
.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;
margin-bottom: 15px;
padding: 12px 16px;
border: 1px solid #e5e6eb;
border-radius: 6px;
background: #fbfcfd;
}

View File

@@ -2,10 +2,9 @@ import { useState } from "react";
import { Button, Card, Form, Input, Message, Modal, Space, Table, Tag } from "@arco-design/web-react";
import { IconPlus } from "@arco-design/web-react/icon";
import type { Permission, Role } from "../api/types";
import { api } from "../api/client";
import { createRole as createRoleApi, updateRolePermissions } from "../api/roles";
import { usePageData } from "../hooks/usePageData";
import { PermissionTreePicker } from "./PermissionTreePicker";
import "./index.css";
function RolesPage() {
const { data, loading, reload, changePage } = usePageData<Role>("/api/admin/v1/roles");
@@ -28,7 +27,7 @@ function RolesPage() {
const createRole = async (values: { code: string; name: string; description?: string }) => {
setCreatingRole(true);
try {
await api.post("/api/admin/v1/roles", {
await createRoleApi({
...values,
permissionIds: createPermissionIds,
});
@@ -52,9 +51,7 @@ function RolesPage() {
}
setSavingPermissions(true);
try {
await api.patch(`/api/admin/v1/roles/${selectedRole.id}/permissions`, {
permissionIds: selectedPermissionIds,
});
await updateRolePermissions(selectedRole.id, selectedPermissionIds);
Message.success("角色权限已更新");
setAssignmentVisible(false);
await reload();
@@ -122,7 +119,7 @@ function RolesPage() {
</Form.Item>
<div className="form-label"></div>
<PermissionTreePicker permissions={permissions.data.list} value={createPermissionIds} onChange={setCreatePermissionIds} />
<div className="form-help"></div>
<div className="form-help mt-5"></div>
<div className="modal-actions">
<Button onClick={() => setCreateVisible(false)}></Button>
<Button type="primary" htmlType="submit" loading={creatingRole}>

View File

@@ -1,12 +1,12 @@
import { Button, Card, Message, Table, Tag } from '@arco-design/web-react';
import type { AppUser } from '../api/types';
import { api } from '../api/client';
import { updateUserStatus } from '../api/users';
import { usePageData } from '../hooks/usePageData';
function UsersPage() {
const { data, loading, reload, changePage } = usePageData<AppUser>('/api/admin/v1/users');
const update状态 = async (id: number, status: 'active' | 'disabled') => {
await api.patch(`/api/admin/v1/users/${id}/status`, { status });
await updateUserStatus(id, status);
Message.success('用户状态已更新');
await reload();
};

44
src/api/auth.ts Normal file
View File

@@ -0,0 +1,44 @@
import { api } from './client';
import type { AdminProfile, ApiResponse } from './types';
export type LoginPayload = {
username: string;
password: string;
};
export type UpdateProfilePayload = {
nickname?: string;
email?: string;
};
export type ChangePasswordPayload = {
oldPassword: string;
newPassword: string;
};
export async function login(payload: LoginPayload) {
const response = await api.post<ApiResponse<{ accessToken: string }>>(
'/api/admin/v1/auth/login',
payload,
);
return response.data.data;
}
export async function getProfile() {
const response = await api.get<ApiResponse<AdminProfile>>(
'/api/admin/v1/auth/profile',
);
return response.data.data;
}
export async function updateProfile(payload: UpdateProfilePayload) {
const response = await api.patch<ApiResponse<AdminProfile>>(
'/api/admin/v1/auth/profile',
payload,
);
return response.data.data;
}
export async function changePassword(payload: ChangePasswordPayload) {
await api.patch('/api/admin/v1/auth/password', payload);
}

13
src/api/data.ts Normal file
View File

@@ -0,0 +1,13 @@
import { api } from './client';
import type { ApiResponse, DataOverview } from './types';
export async function getDataOverview() {
const response = await api.get<ApiResponse<DataOverview>>(
'/api/admin/v1/data/overview',
);
return response.data.data;
}
export async function syncDataMetrics() {
await api.post('/api/admin/v1/data/sync', {});
}

44
src/api/dramas.ts Normal file
View File

@@ -0,0 +1,44 @@
import { api } from './client';
import type { ApiResponse, Drama, Episode } from './types';
export async function createDrama(payload: Partial<Drama>) {
await api.post('/api/admin/v1/dramas', payload);
}
export async function getDrama(dramaId: number) {
const response = await api.get<ApiResponse<Drama>>(
`/api/admin/v1/dramas/${dramaId}`,
);
return response.data.data;
}
export async function getDramaEpisodes(dramaId: number) {
const response = await api.get<ApiResponse<{ list: Episode[] }>>(
`/api/admin/v1/dramas/${dramaId}/episodes`,
{ params: { pageSize: 200 } },
);
return response.data.data.list;
}
export async function configureDramaEpisodes(
dramaId: number,
episodeCount: number,
) {
await api.post(`/api/admin/v1/dramas/${dramaId}/episodes/batch`, {
episodeCount,
});
}
export async function updateDrama(
dramaId: number,
payload: Partial<Pick<Drama, 'status'>>,
) {
await api.patch(`/api/admin/v1/dramas/${dramaId}`, payload);
}
export async function updateEpisode(
episodeId: number,
payload: Partial<Pick<Episode, 'publishStatus' | 'status'>>,
) {
await api.patch(`/api/admin/v1/episodes/${episodeId}`, payload);
}

25
src/api/media.ts Normal file
View File

@@ -0,0 +1,25 @@
import { api } from './client';
import type { ApiResponse, UploadTask } from './types';
export type CreateUploadTaskPayload = {
episodeId: number;
fileName: string;
contentType: string;
fileSize: number;
};
export async function createUploadTask(payload: CreateUploadTaskPayload) {
const response = await api.post<ApiResponse<UploadTask>>(
'/api/admin/v1/media/upload-tasks',
payload,
);
return response.data.data;
}
export async function completeUploadTask(jobId: string) {
await api.patch(`/api/admin/v1/media/upload-tasks/${jobId}/complete`, {});
}
export async function syncReviewStatus() {
await api.post('/api/admin/v1/media/review-status/sync', {});
}

27
src/api/personnel.ts Normal file
View File

@@ -0,0 +1,27 @@
import { api } from './client';
export type CreatePersonnelPayload = {
username: string;
password: string;
nickname?: string;
email?: string;
roleIds?: number[];
};
export async function createPersonnel(payload: CreatePersonnelPayload) {
await api.post('/api/admin/v1/admin-users', {
...payload,
roleIds: payload.roleIds ?? [],
});
}
export async function updateAdminUserRoles(id: number, roleIds: number[]) {
await api.patch(`/api/admin/v1/admin-users/${id}/roles`, { roleIds });
}
export async function updateAdminUserStatus(
id: number,
status: 'active' | 'disabled',
) {
await api.patch(`/api/admin/v1/admin-users/${id}/status`, { status });
}

21
src/api/roles.ts Normal file
View File

@@ -0,0 +1,21 @@
import { api } from './client';
export type CreateRolePayload = {
code: string;
name: string;
description?: string;
permissionIds: number[];
};
export async function createRole(payload: CreateRolePayload) {
await api.post('/api/admin/v1/roles', payload);
}
export async function updateRolePermissions(
roleId: number,
permissionIds: number[],
) {
await api.patch(`/api/admin/v1/roles/${roleId}/permissions`, {
permissionIds,
});
}

8
src/api/users.ts Normal file
View File

@@ -0,0 +1,8 @@
import { api } from './client';
export async function updateUserStatus(
id: number,
status: 'active' | 'disabled',
) {
await api.patch(`/api/admin/v1/users/${id}/status`, { status });
}

View File

@@ -1,42 +1,48 @@
* {
box-sizing: border-box;
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
@apply box-border;
}
html,
body,
#root {
@apply m-0 min-h-full w-full min-w-80;
}
body {
@apply bg-canvas font-sans text-ink;
}
button,
input,
textarea,
select {
font: inherit;
}
}
html,
body,
#root {
width: 100%;
min-width: 320px;
min-height: 100%;
margin: 0;
}
@layer components {
.section-card {
@apply rounded-lg;
}
body {
color: #1d2129;
background: #f2f3f5;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.page-stack {
@apply flex flex-col gap-4;
}
button,
input,
textarea,
select {
font: inherit;
}
.modal-actions {
@apply mt-6 flex justify-end gap-3;
}
.section-card {
border-radius: 8px;
}
.form-label {
@apply mb-2 text-sm font-medium text-[#4e5969];
}
.page-stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
.form-help {
@apply -mt-3 mb-[18px] text-[13px] leading-[1.6] text-muted;
}
}

View File

@@ -2,8 +2,9 @@ import { useEffect, useMemo, useState } from "react";
import { Avatar, Button, Dropdown, Layout, Menu, Space, Typography } from "@arco-design/web-react";
import { IconMenuFold, IconMenuUnfold } from "@arco-design/web-react/icon";
import { Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import type { ApiResponse, AdminProfile } from "../api/types";
import { TOKEN_KEY, api } from "../api/client";
import type { AdminProfile } from "../api/types";
import { getProfile } from "../api/auth";
import { TOKEN_KEY } from "../api/client";
import { filterMenuItems, findCurrentMenu, menuItems, renderMenuItem } from "./menu";
import { PermissionRoute } from "./PermissionRoute";
import DashboardPage from "../Dashboard";
@@ -14,7 +15,6 @@ import RolesPage from "../Role";
import PersonnelPage from "../Personnel";
import ProfilePage from "../Profile";
import LogsPage from "../Log";
import "./layout.css";
const { Header, Sider, Content } = Layout;
const { Text } = Typography;
@@ -29,8 +29,8 @@ export function AdminLayout() {
const [openMenuKeys, setOpenMenuKeys] = useState<string[]>(current?.parent ? [current.parent.key] : []);
useEffect(() => {
api.get<ApiResponse<AdminProfile>>("/api/admin/v1/auth/profile")
.then((response) => setProfile(response.data.data))
getProfile()
.then((data) => setProfile(data))
.catch(() => {
localStorage.removeItem(TOKEN_KEY);
navigate("/login", { replace: true });
@@ -49,10 +49,10 @@ export function AdminLayout() {
};
return (
<Layout className="layout">
<Sider className="sidebar" width={220} collapsed={collapsed} collapsible trigger={null}>
<div className="logo-row">
<span className="logo-mark">TK</span>
<Layout className="min-h-screen">
<Sider className="shadow-[1px_0_0_#e5e6eb]" width={220} collapsed={collapsed} collapsible trigger={null}>
<div className="flex h-14 items-center gap-2.5 border-b border-line px-[18px] font-bold">
<span className="grid h-7 w-7 place-items-center rounded-md bg-brand text-sm text-white">TK</span>
{!collapsed && <span>TK短剧管理后台</span>}
</div>
<Menu
@@ -65,10 +65,10 @@ export function AdminLayout() {
</Menu>
</Sider>
<Layout>
<Header className="header">
<Header className="flex h-14 items-center justify-between border-b border-line bg-white px-5">
<Space>
<Button icon={collapsed ? <IconMenuUnfold /> : <IconMenuFold />} onClick={() => setCollapsed(!collapsed)} />
<div className="header-title">
<div className="flex flex-col gap-0.5">
<strong>{current?.item.label || "工作台"}</strong>
</div>
</Space>
@@ -86,13 +86,13 @@ export function AdminLayout() {
</Menu>
}
>
<button className="user-menu-trigger" type="button">
<button className="inline-flex cursor-pointer items-center gap-2 rounded-md border-0 bg-transparent px-2 py-1 text-ink hover:bg-canvas" type="button">
<Avatar size={28}>{profile?.username?.slice(0, 1).toUpperCase() || "A"}</Avatar>
<Text>{profile?.nickname || profile?.username || "管理员"}</Text>
</button>
</Dropdown>
</Header>
<Content className="content">
<Content className="p-3 sm:p-5">
<Routes>
<Route path="/dashboard" element={<DashboardPage profile={profile} />} />
<Route

View File

@@ -1,79 +0,0 @@
.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;
}
.user-menu-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
border: 0;
border-radius: 6px;
background: transparent;
color: #1d2129;
cursor: pointer;
}
.user-menu-trigger:hover {
background: #f2f3f5;
}
.content {
padding: 20px;
}
@media (max-width: 640px) {
.content {
padding: 12px;
}
}

35
tailwind.config.js Normal file
View File

@@ -0,0 +1,35 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{ts,tsx}'],
important: '#root',
corePlugins: {
preflight: false,
},
theme: {
extend: {
colors: {
ink: '#1d2129',
muted: '#86909c',
line: '#e5e6eb',
canvas: '#f2f3f5',
panel: '#fbfcfd',
brand: '#165dff',
},
fontFamily: {
sans: [
'Inter',
'ui-sans-serif',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'sans-serif',
],
},
boxShadow: {
login: '0 18px 48px rgba(15, 23, 42, 0.08)',
},
},
},
plugins: [],
};