diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..100597b --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,122 @@ +{ + "permissions": { + "allow": [ + "Bash(npm install:*)", + "Bash(where node:*)", + "Bash(where npm:*)", + "Bash(where pnpm:*)", + "Bash(where yarn:*)", + "Bash(cmd.exe /c \"where node 2>nul & where npm 2>nul & where pnpm 2>nul & where yarn 2>nul\")", + "Bash(cmd.exe /c \"node --version && npm --version\")", + "Bash(export PATH=\"/c/Program Files/nodejs:$PATH\")", + "Bash(npm --version)", + "Bash(npm run:*)", + "Bash(npx next:*)", + "Bash(pip install:*)", + "Bash(python manage.py migrate)", + "Bash(python manage.py runserver 0.0.0.0:8000)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/api/)", + "Bash(find c:/Users/admin/Desktop/Lila-Server/qy-lty-admin -type f -name *.ts -o -name *.tsx -o -name *.jsx -o -name *.js)", + "Bash(curl -s -o /dev/null -w \"HTTP %{http_code}\" http://localhost:8000/swagger/)", + "Bash(grep -r \"WebSocket\\\\|websocket\\\\|ws://\" /c/Users/admin/Desktop/Lila-Server/qy-lty-admin --include=*.ts --include=*.tsx --include=*.js)", + "Bash(grep -r \"WebSocket\\\\|websocket\" /c/Users/admin/Desktop/Lila-Server/qy_lty --include=*.py)", + "Bash(xargs grep:*)", + "Bash(grep -r \"apiClient\\\\|API_BASE_URL\\\\|http://localhost:8000\" /c/Users/admin/Desktop/Lila-Server/qy-lty-admin/app --include=*.ts --include=*.tsx)", + "Bash(find /c/Users/admin/Desktop/Lila-Server/qy_lty -type d -name *affinity* -o -name *好感度*)", + "Bash(xargs cat:*)", + "Bash(grep -E \"\\\\.py$\")", + "Bash(python manage.py makemigrations userapp)", + "Bash(tasklist)", + "Bash(wmic process:*)", + "Bash(taskkill /PID 21260 /PID 54320 /PID 66096 /PID 68736 /F)", + "Bash(powershell -Command \"Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match ''runserver'' } | Select-Object ProcessId, CommandLine\")", + "Bash(powershell -Command \"Stop-Process -Id 21260, 54320, 66096, 68736 -Force -ErrorAction SilentlyContinue; Write-Output ''Done''\")", + "Read(//tmp/**)", + "Bash(powershell -Command \"Test-NetConnection -ComputerName localhost -Port 8000 -InformationLevel Quiet\")", + "Bash(curl -s -X POST http://localhost:8000/api/v1/admin/login/ -H \"Content-Type: application/json\" -d '{\"\"email\"\":\"\"111111@qq.com\"\",\"\"password\"\":\"\"111111\"\"}')", + "Bash(curl -s -w \"\\\\nHTTP_CODE:%{http_code}\" \"http://localhost:8000/api/card/category/song/?page_size=5&page=1\")", + "Bash(curl -s -w \"\\\\nHTTP_CODE:%{http_code}\" -H \"Authorization: Bearer fake_token_12345\" \"http://localhost:8000/api/card/category/song/?page_size=5&page=1\")", + "Skill(update-config)", + "Skill(update-config:*)", + "Bash(python manage.py shell -c \":*)", + "Bash(grep -E \"\\\\.\\(tsx|ts|jsx|js\\)$\")", + "Read(//c/Users/admin/Desktop/Lila-Server/**)", + "Bash(npx tsc:*)", + "Bash(node ./node_modules/.bin/tsc --noEmit)", + "Read(//c/Program Files/nodejs/**)", + "Read(//c/Users/admin/AppData/Roaming/**)", + "Bash(python qy_lty/manage.py makemigrations card --name prop_attributes_optional_fields)", + "Bash(python qy_lty/manage.py migrate card)", + "Bash(ls c:UsersadminDesktopLila-Serverqy_lty/.env*)", + "Bash(ls \"c:/Users/admin/Desktop/Lila-Server/qy_lty/\"*.env*)", + "Bash(python -c \"from common.oss import OSSUploader; u = OSSUploader\\(\\); print\\(''OSSUploader init OK''\\)\")", + "Bash(DJANGO_SETTINGS_MODULE=qy_lty.settings python -c \":*)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/api/common/upload/)", + "Read(//dev/**)", + "Bash(mkdir -p /tmp)", + "Bash(\"c:/Users/admin/Desktop/test.png\")", + "Bash(curl -s -w \"\\\\n%{http_code}\" -X POST http://localhost:8000/api/common/upload/ -F \"file=@c:/Users/admin/Desktop/test.png;type=image/png\" -H \"Authorization: Bearer 3ade1cea-4ee4-4ea0-b5ae-5295a183eca8\")", + "Bash(node_modules/.bin/tsc --noEmit --pretty)", + "Bash(python manage.py migrate card 0011_cardtemplate_image_to_urlfield)", + "Bash(python manage.py showmigrations card)", + "Bash(curl -s http://localhost:8000/api/)", + "Bash(python manage.py check)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:8000/)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://127.0.0.1:38000/api/)", + "Bash(curl -s \"http://127.0.0.1:8000/api/card/category/prop/?page=1&page_size=10\")", + "Bash(node_modules/.bin/next build:*)", + "Bash(\"/c/Program Files/nodejs/node.exe\" node_modules/.bin/next build --no-lint)", + "Bash(\"/c/Program Files/nodejs/node.exe\" node_modules/next/dist/bin/next build --no-lint)", + "Bash(curl -s http://localhost:3000)", + "Bash(taskkill //F //IM node.exe)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000)", + "Bash(python manage.py makemigrations card --name clothing_attributes_optional_fields)", + "Bash(python manage.py migrate card)", + "Bash(findstr /i 'python daphne')", + "Bash(powershell \"Get-CimInstance Win32_Process -Filter \"\"name=''python.exe''\"\" | Select-Object ProcessId,CommandLine | Format-List\")", + "Bash(taskkill //F //PID 22852 //T)", + "Bash(taskkill //F //PID 85184 //T)", + "Read(//c/Users/admin/AppData/Local/Temp/claude/c--Users-admin-Desktop-Lila-Server/a10d9a40-b774-4f1d-8741-be0671dbb102/tasks/**)", + "Bash(findstr /i python)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/api/card/templates/)", + "Bash(python -c \":*)", + "Bash(curl -s -X POST http://localhost:8000/api/card/templates/ -H \"Content-Type: application/json\" -d '{\"\"name\"\":\"\"test\"\",\"\"category\"\":\"\"clothing\"\",\"\"description\"\":\"\"test\"\",\"\"rarity\"\":\"\"common\"\",\"\"clothing_attributes\"\":{\"\"style\"\":\"\"常规服装\"\"}}')", + "Bash(curl -s -X POST http://localhost:8000/api/card/templates/ -H \"Content-Type: application/json\" -H \"Authorization: Bearer 51ae5483-86d0-4b39-86ee-18b50938bf6d\" -d '{\"\"name\"\":\"\"test111\"\",\"\"category\"\":\"\"clothing\"\",\"\"description\"\":\"\"111\"\",\"\"rarity\"\":\"\"common\"\",\"\"clothing_attributes\"\":{\"\"style\"\":\"\"常规服装\"\"}}')", + "Bash(curl -s -X POST http://localhost:8000/api/card/templates/ -H \"Content-Type: application/json; charset=utf-8\" -H \"Authorization: Bearer 51ae5483-86d0-4b39-86ee-18b50938bf6d\" --data-raw \"{\"\"name\"\":\"\"test111\"\",\"\"category\"\":\"\"clothing\"\",\"\"description\"\":\"\"111\"\",\"\"rarity\"\":\"\"common\"\",\"\"clothing_attributes\"\":{\"\"style\"\":\"\"regular\"\"}}\")", + "Bash(powershell \"Get-CimInstance Win32_Process -Filter \"\"name=''''python.exe''''\"\" | Select-Object ProcessId,CommandLine\")", + "Bash(taskkill //F //PID 93368 //T)", + "Bash(taskkill //F //PID 34292 //T)", + "Bash(curl -s -X POST http://localhost:8000/api/card/templates/ -H \"Content-Type: application/json; charset=utf-8\" -H \"Authorization: Bearer c0e1bca3-3bd5-451f-9411-289cc51dac08\" --data-raw '{\"\"name\"\":\"\"test_outfit\"\",\"\"category\"\":\"\"clothing\"\",\"\"description\"\":\"\"test desc\"\",\"\"rarity\"\":\"\"common\"\",\"\"clothing_attributes\"\":{\"\"style\"\":\"\"regular\"\"}}')", + "Bash(find /c/Users/admin/Desktop/Lila-Server/qy_lty -name *prop* -type f)", + "Bash(grep -E \"\\\\.\\(py|json\\)$\")", + "Bash(find /c/Users/admin/Desktop/Lila-Server/qy_lty -name *home* -type f)", + "Bash(ls /c/Users/admin/Desktop/Lila-Server/qy-lty-admin/app/home-decor/[id]/)", + "Bash(python manage.py makemigrations card --name decoration_furniture_optional_fields)", + "Bash(powershell \"Get-CimInstance Win32_Process -Filter \"\"name=''python.exe'' AND CommandLine LIKE ''%runserver%''\"\" | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }; Write-Host ''Killed''\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/api/card/category/decoration/)", + "Bash(TOKEN=\"9f70f7da-7bce-4c98-b7c3-3a9414ddab44\")", + "Bash(curl -s http://localhost:8000/api/user/ -H \"Authorization: Token $TOKEN\")", + "Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(''''status:'''', d.get\\(''''code'''',''''?''''\\), ''''count:'''', d.get\\(''''data'''',{}\\).get\\(''''count'''',''''?''''\\)\\)\")", + "Bash(curl -s http://localhost:8000/api/card/category/dance/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s http://localhost:8000/api/card/category/clothing/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s http://localhost:8000/api/card/category/prop/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s http://localhost:8000/api/card/category/decoration/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s \"http://localhost:8000/api/food/foods/\" -H \"Authorization: Token $TOKEN\")", + "Bash(curl -s http://localhost:8000/api/achievement/achievements/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s http://localhost:8000/api/user/affinity-rules/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s http://localhost:8000/api/user/affinity-levels/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s http://localhost:8000/api/ai/bots/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s http://localhost:8000/api/user/groups/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s \"http://localhost:8000/api/card/category/song/\" -H \"Authorization: Token $TOKEN\")", + "Bash(curl -s http://localhost:8000/api/device/device-types/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s http://localhost:8000/api/device/devices/ -H 'Authorization: Token $TOKEN')", + "Bash(curl -s -X POST http://localhost:8000/api/user/auth/register/ -H 'Content-Type: application/json' -d {})", + "Bash(curl -s -X POST \"http://localhost:8000/api/user/auth/phone/login/\" -H \"Content-Type: application/json\" -d '{}')", + "Bash(git add:*)", + "Bash(git reset:*)" + ], + "additionalDirectories": [ + "C:\\Users\\admin\\.claude" + ] + } +} diff --git a/docs/功能清单与开发状态.md b/docs/功能清单与开发状态.md new file mode 100644 index 0000000..12784f2 --- /dev/null +++ b/docs/功能清单与开发状态.md @@ -0,0 +1,340 @@ +# 洛天依管理系统 - 功能清单与开发状态 + +> 生成时间:2026-03-19(已通过 API 实测验证) + +--- + +## 一、整体架构 + +| 组件 | 技术栈 | 路径 | +|------|--------|------| +| 后端 API | Django + DRF + Channels + Daphne | `qy_lty/` | +| 管理后台前端 | Next.js 15 + React 19 + Tailwind + shadcn/ui | `qy-lty-admin/` | +| 数据库 | PostgreSQL(阿里云 RDS) | 远程 | +| 缓存/消息 | Redis(阿里云) | 远程 | + +--- + +## 二、功能模块状态总览 + +| 模块 | 前端页面 | 后端 API | 前后端对接 | 后端实测数据 | 状态 | +|------|----------|----------|------------|-------------|------| +| 邮箱登录 | ✅ | ✅ | ✅ 已对接 | ✅ 返回 token | ✅ 完成 | +| 手机登录 | ✅ UI | ✅ 已验证 | ❌ 未对接 | ✅ 接口可用 | ⚠️ 待对接 | +| 注册 | ✅ UI | ✅ 已验证 | ❌ 未对接 | ✅ 接口可用(返回参数校验) | ⚠️ 待对接 | +| 忘记密码 | ✅ UI | ✅ 已验证 | ❌ 未对接 | ✅ 接口可用 | ⚠️ 待对接 | +| 仪表盘 | ✅ UI | ❌ 无统计接口 | ❌ 硬编码数据 | — | ⚠️ 待开发 | +| 用户管理 | ✅ | ✅ | ✅ 已对接 | ✅ 5 条数据 | ✅ 完成 | +| 歌曲管理 | ✅ | ✅ | ✅ 已对接 | ✅ 9 条数据 | ✅ 完成 | +| 舞蹈管理 | ✅ UI | ✅ 已验证 | ❌ 前端用 mock | ✅ 5 条数据 | ⚠️ 待对接 | +| 服装管理 | ✅ UI | ✅ 已验证 | ❌ 前端用 mock | ✅ 11 条数据 | ⚠️ 待对接 | +| 道具管理 | ✅ UI | ✅ 已验证 | ❌ 前端用 mock | ✅ 3 条数据 | ⚠️ 待对接 | +| 家居装饰 | ✅ UI | ✅ 已验证 | ❌ 前端用 mock | ✅ 6 条数据 | ⚠️ 待对接 | +| 食物管理 | ✅ | ✅ | ✅ 已对接 | ✅ 5 条数据 | ✅ 完成 | +| 成就系统 | ✅ UI | ✅ 已验证 | ❌ 前端用 mock | ✅ 13 条数据 | ⚠️ 待对接 | +| 好感度 | ✅ UI | ✅ 已验证 | ❌ 前端用 mock | ✅ 接口可用(暂无数据) | ⚠️ 待对接 | +| AI 模型管理 | ✅ UI | ✅ 已验证 | ❌ 前端纯展示 | ✅ 1 个 Bot | ⚠️ 待对接 | +| 权限管理 | ✅ UI | ✅ 已验证 | ❌ 前端用 mock | ✅ 接口可用(暂无数据) | ⚠️ 待对接 | +| 系统设置 | ✅ UI | ❌ 无接口 | ❌ 无持久化 | — | ❌ 待开发 | +| 设备管理 | ❌ 无页面 | ✅ 已验证 | ❌ | ✅ 2 类型/20 设备 | ❌ 前端待开发 | +| 订阅管理 | ❌ 无页面 | ⚠️ 有模型无接口 | ❌ | — | ❌ 待开发 | +| 工作流 | ❌ 无页面 | ❌ 空模块 | ❌ | — | ❌ 待开发 | + +--- + +## 三、已完成功能详细说明 + +### 1. 邮箱登录(✅ 完成) + +- **前端**:`/login` 页面,邮箱 + 密码表单 +- **后端**:`POST /api/v1/admin/login/` → 返回 token +- **使用方式**:输入邮箱和密码,登录后 token 自动存储到 localStorage 和 Cookie +- **测试账号**:邮箱 `111111@qq.com`,密码 `111111` + +### 2. 用户管理(✅ 完成) + +- **前端**:`/users` 页面,完整 CRUD +- **后端**:`/api/user/` ViewSet +- **功能**: + - 用户列表(分页、搜索) + - 新增用户 + - 编辑用户信息 + - 删除用户 + - 切换用户状态(启用/禁用) + +### 3. 歌曲管理(✅ 完成) + +- **前端**:`/songs` 列表页 + `/songs/[id]` 详情页 +- **后端**:`/api/card/category/song/` + `/api/card/templates/` +- **功能**: + - 歌曲列表(分页、搜索) + - 新增/编辑歌曲(含音频上传) + - 发布/取消发布 + - 批次生成与管理 + - 音频播放器(播放/暂停/音量控制) + +### 4. 食物管理(✅ 完成) + +- **前端**:`/food` 列表页 + `/food/[id]` 详情页 +- **后端**:`/api/food/foods/` +- **功能**: + - 食物列表(分页、搜索、分类筛选) + - 新增/编辑/删除食物 + - 食物分类和稀有度管理 + - 使用记录查看 + +--- + +## 四、待对接功能(前后端都有,需要连接) + +以下模块前端 UI 已完成,后端 API 已存在,但前端页面使用的是 mock 硬编码数据,需要替换为真实 API 调用。 + +### 5. 舞蹈管理(⚠️ 待对接) + +- **前端**:`/dances` 页面,使用 `initialDances` 硬编码 5 条数据 +- **后端 API**: + - `GET /api/card/category/dance/` — 舞蹈列表 + - `POST /api/card/templates/` — 创建 + - `PATCH /api/card/templates/{id}/` — 更新 + - `DELETE /api/card/templates/{id}/` — 删除 +- **前端 API 模块**:`lib/api/dances.ts` 已写好,页面未调用 +- **工作量**:将页面中 mock 数据替换为 API 调用即可 + +### 6. 服装管理(⚠️ 待对接) + +- **前端**:`/outfits` 页面,内联硬编码数据 +- **后端 API**:`/api/card/category/clothing/` + `/api/card/templates/` +- **前端 API 模块**:`lib/api/outfits.ts` 已写好 +- **工作量**:页面重构程度较大,CRUD 交互不完整 + +### 7. 道具管理(⚠️ 待对接) + +- **前端**:`/props` 页面,使用 `initialProps` 硬编码数据 +- **后端 API**:`/api/card/category/prop/` + `/api/card/templates/` +- **前端 API 模块**:`lib/api/props.ts` 已写好 +- **工作量**:与舞蹈管理类似,替换 mock 数据 + +### 8. 家居装饰(⚠️ 待对接) + +- **前端**:`/home-decor` 页面,使用 `initialDecors` 硬编码数据 +- **后端 API**:`/api/card/category/decoration/` + `/api/card/templates/` +- **前端 API 模块**:`lib/api/home-decor.ts` 已写好 +- **工作量**:与舞蹈管理类似,替换 mock 数据 + +### 9. 成就系统(⚠️ 待对接) + +- **前端**:`/achievements` 页面,使用 `mockData` 硬编码 10 条数据 +- **后端 API**: + - `GET /api/achievement/achievements/` — 列表 + - `POST /api/achievement/achievements/` — 创建 + - `PATCH /api/achievement/achievements/{id}/` — 更新 + - `DELETE /api/achievement/achievements/{id}/` — 删除 +- **前端 API 模块**:`lib/api/achievements.ts` 已写好 +- **工作量**:替换 mock 数据 + 对齐数据结构 + +### 10. 好感度系统(⚠️ 待对接) + +- **前端**:`/affinity` 页面,使用 `initialRules` 和 `initialLevels` 硬编码 +- **后端 API**: + - `/api/user/affinity-rules/` — 好感度规则 CRUD + - `/api/user/affinity-levels/` — 好感度等级 CRUD +- **前端 API 模块**:`lib/api/affinity.ts` 已写好 +- **工作量**:替换 mock 数据 + +### 11. AI 模型管理(⚠️ 待对接) + +- **前端**:`/ai-model` 页面,纯展示页面(框架/微调/语音/知识库 tab) +- **后端 API**: + - `GET/POST/PATCH/DELETE /api/ai/bots/` — Bot CRUD +- **前端 API 模块**:`lib/api/ai-models.ts` 已写好 +- **工作量**:需要将展示页改为管理页,添加 CRUD 交互 + +### 12. 权限管理(⚠️ 待对接) + +- **前端**:`/permissions` 页面,使用 `initialRoles` 硬编码 5 个角色 +- **后端 API**:`/api/user/groups/` — Django Group CRUD +- **前端 API 模块**:`lib/api/roles.ts` 已写好 +- **工作量**:替换 mock 数据,权限矩阵需与后端权限体系对齐 + +### 13. 手机登录 / 注册 / 忘记密码(⚠️ 待对接) + +- **前端**:UI 表单已完成,但使用 `setTimeout` 模拟请求 +- **后端**: + - 手机登录:`POST /api/user/auth/phone/login/` + - 发送验证码:`POST /api/user/auth/phone/verify/` + - 注册:`POST /api/user/auth/register/` + - 重置密码:`POST /api/user/auth/password/reset/` +- **工作量**:前端对接后端已有接口 + +--- + +## 五、待开发功能(缺前端或缺后端) + +### 14. 仪表盘数据(❌ 待开发) + +- **现状**:前端 `/` 页面显示硬编码统计数据(12,345 用户、5,732 活跃等) +- **需要**:后端开发统计聚合接口,前端对接 +- **建议接口**: + - `GET /api/v1/admin/dashboard/stats/` — 用户统计、卡片统计等 + +### 15. 系统设置(❌ 待开发) + +- **现状**:前端 `/settings` 页面有基本/数据库/安全/通知 4 个 tab,但无持久化 +- **需要**:后端设计系统配置模型和接口 +- **工作量**:前后端都需要开发 + +### 16. 设备管理(❌ 前端待开发) + +- **后端已有完整 API**: + - `/api/device/device-types/` — 设备类型 CRUD + - `/api/device/device-batches/` — 设备批次 CRUD + - `/api/device/devices/` — 设备 CRUD(含批量创建) + - `/api/device/user-devices/` — 用户设备绑定 + - `/api/device/messages/` — 消息管理 + - WebSocket:`ws://domain/ws/device/{user_id}/` +- **需要**:开发前端管理页面 + +### 17. 订阅管理(❌ 前后端待开发) + +- **现状**:后端有模型定义(SubscriptionPlan、Subscription、AddOnPackage 等),但无 API 接口 +- **需要**:后端开发 API + 前端开发页面 + +### 18. 工作流系统(❌ 前后端待开发) + +- **现状**:后端 `workflow_app` 为空模块,无模型无接口 +- **需要**:完整设计和开发 + +### 19. 阿里云 VI(视觉智能)(❌ 已禁用) + +- **现状**:后端 `ali_vi_app` 有人脸相关接口,但在主 urls.py 中已注释禁用 +- **接口**:人脸模板、人脸合成、美颜、美妆等 +- **状态**:暂不需要管理后台 + +--- + +## 六、后端 API 完整清单 + +### 用户模块 `/api/user/` +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/` | 用户列表 | +| POST | `/` | 创建用户 | +| GET/PUT/PATCH/DELETE | `/{id}/` | 用户详情/更新/删除 | +| GET | `/info/` | 当前用户信息 | +| PUT/PATCH | `/update_profile/` | 更新个人资料 | +| POST | `/bind_phone/` | 绑定手机号 | +| GET/POST | `/groups/` | 角色列表/创建 | +| GET/PUT/DELETE | `/groups/{id}/` | 角色详情/更新/删除 | +| GET/POST | `/affinity-rules/` | 好感度规则列表/创建 | +| GET/PUT/DELETE | `/affinity-rules/{id}/` | 规则详情/更新/删除 | +| GET/POST | `/affinity-levels/` | 好感度等级列表/创建 | +| GET/PUT/DELETE | `/affinity-levels/{id}/` | 等级详情/更新/删除 | + +### 认证模块 `/api/user/auth/` +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/login/` | 用户名密码登录 | +| POST | `/logout/` | 登出 | +| POST | `/register/` | 注册 | +| POST | `/email/login/` | 邮箱登录 | +| POST | `/phone/login/` | 手机号登录 | +| POST | `/phone/verify/` | 发送验证码 | +| POST | `/username/login/` | 用户名登录 | +| POST | `/mac/login/` | MAC 地址登录 | +| POST | `/password/reset/` | 重置密码 | + +### 管理员模块 `/api/v1/admin/` +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/login/` | 管理员邮箱登录 | +| POST | `/logout/` | 管理员登出 | + +### AI 模块 `/api/ai/` +| 方法 | 路径 | 说明 | +|------|------|------| +| GET/POST | `/bots/` | Bot 列表/创建 | +| GET/PUT/DELETE | `/bots/{id}/` | Bot 详情/更新/删除 | +| POST | `/chat/{bot_id}/` | 单轮对话 | +| POST | `/multichat/` | 多轮对话 | + +### 卡片模块 `/api/card/` +| 方法 | 路径 | 说明 | +|------|------|------| +| GET/POST | `/templates/` | 模板列表/创建 | +| GET/PUT/DELETE | `/templates/{id}/` | 模板详情/更新/删除 | +| POST | `/templates/{id}/publish/` | 发布模板 | +| POST | `/templates/{id}/archive/` | 归档模板 | +| GET/POST | `/cards/` | 卡片列表/创建 | +| POST | `/cards/scan/` | 扫描卡片(需认证) | +| POST | `/cards/scan_public/` | 扫描卡片(公开) | +| POST | `/cards/use/` | 使用卡片 | +| GET/POST | `/batches/` | 批次列表/创建 | +| POST | `/batches/generate/` | 批量生成卡片 | +| GET | `/batches/{id}/export/` | 导出批次 | +| POST | `/batches/{id}/mark_produced/` | 标记生产完成 | +| POST | `/batches/{id}/publish/` | 发布批次 | +| GET | `/category/{category}/` | 按分类查询模板 | +| GET | `/user/cards/` | 用户卡片列表 | + +### 食物模块 `/api/food/` +| 方法 | 路径 | 说明 | +|------|------|------| +| GET/POST | `/foods/` | 食物列表/创建 | +| GET/PUT/PATCH/DELETE | `/foods/{id}/` | 食物详情/更新/删除 | +| GET | `/foods/categories/` | 食物分类信息 | +| POST | `/use-food/` | 使用食物 | +| GET | `/my-foods/` | 我的食物 | +| GET | `/user-foods/` | 用户食物列表 | +| GET | `/food-stats/` | 食物统计 | +| GET | `/usage-logs/` | 使用记录 | + +### 成就模块 `/api/achievement/` +| 方法 | 路径 | 说明 | +|------|------|------| +| GET/POST | `/achievements/` | 成就列表/创建 | +| GET/PUT/DELETE | `/achievements/{id}/` | 成就详情/更新/删除 | +| GET | `/achievements/{id}/check_achievement/` | 检查成就 | +| GET/POST | `/user-achievements/` | 用户成就列表/授予 | +| POST | `/user-achievements/check_and_grant/` | 检查并授予 | +| GET | `/user-achievements/stats/` | 成就统计 | + +### 设备模块 `/api/device/` +| 方法 | 路径 | 说明 | +|------|------|------| +| CRUD | `/device-types/` | 设备类型管理 | +| CRUD | `/device-batches/` | 设备批次管理 | +| CRUD | `/devices/` | 设备管理 | +| POST | `/devices/batch_create/` | 批量创建设备 | +| CRUD | `/user-devices/` | 用户设备绑定 | +| GET | `/messages/` | 消息列表 | +| POST | `/rtc-token/` | 获取 RTC Token | +| POST | `/send_message_to_user/{user_id}/` | WebSocket 发消息 | + +### 通用 `/api/common/` +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/upload/` | 文件上传 | + +--- + +## 七、开发优先级建议 + +### 高优先级(API 已有,仅需前端对接) +1. **舞蹈管理** — 前端 API 模块已写好,替换 mock 数据即可 +2. **道具管理** — 同上 +3. **家居装饰** — 同上 +4. **成就系统** — 同上 +5. **好感度系统** — 同上 +6. **权限管理** — 同上 +7. **手机登录/注册/忘记密码** — 前端 UI 已有,对接后端接口 + +### 中优先级(需要一定开发量) +8. **服装管理** — 前端页面结构需调整 +9. **AI 模型管理** — 需要从展示页改为管理页 +10. **仪表盘** — 需要后端开发统计接口 +11. **设备管理** — 后端完整,需要开发前端页面 + +### 低优先级(需要较大开发量) +12. **系统设置** — 前后端都需要开发 +13. **订阅管理** — 后端需要暴露 API,前端需要开发页面 +14. **工作流系统** — 完全空白,需要完整设计 diff --git a/qy-lty-admin/app/food/[id]/page.tsx b/qy-lty-admin/app/food/[id]/page.tsx index 5265100..6ea047b 100644 --- a/qy-lty-admin/app/food/[id]/page.tsx +++ b/qy-lty-admin/app/food/[id]/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from "react" +import { useState, useEffect, use } from "react" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { DashboardShell } from "@/components/dashboard-shell" import { DashboardHeader } from "@/components/dashboard-header" @@ -12,6 +12,7 @@ import Link from "next/link" import { AddPrintBatchDialog } from "@/components/food/add-print-batch-dialog" import { ExportCardsDialog } from "@/components/food/export-cards-dialog" import { useToast } from "@/components/ui/use-toast" +import { isSuperUser } from "@/lib/api/auth" import { getFood } from "@/lib/api/food" import type { Food } from "@/components/food/food-detail-dialog" @@ -28,7 +29,8 @@ type FoodWithBatches = Food & { }> } -export default function FoodDetailPage({ params }: { params: { id: string } }) { +export default function FoodDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params) const { toast } = useToast() const [food, setFood] = useState(null) const [loading, setLoading] = useState(true) @@ -39,7 +41,7 @@ export default function FoodDetailPage({ params }: { params: { id: string } }) { try { setLoading(true) setError(null) - const response = await getFood(params.id) + const response = await getFood(id) if (response.success && response.data) { // 为演示目的,添加一些模拟的批次数据 @@ -84,7 +86,7 @@ export default function FoodDetailPage({ params }: { params: { id: string } }) { useEffect(() => { fetchFoodDetail() - }, [params.id]) + }, [id]) if (loading) { return ( @@ -104,7 +106,7 @@ export default function FoodDetailPage({ params }: { params: { id: string } }) {

食物不存在

- {error || `找不到ID为 ${params.id} 的食物`} + {error || `找不到ID为 ${id} 的食物`}

- {!isPublished && ( + {(!isPublished || isSuperUser()) && ( - {food.status !== "published" && food.status !== "已发布" && ( + {/* 草稿状态:显示发布按钮 */} + {food.status === "draft" && ( + handlePublishFood(food.id, food.name)} + /> + )} + + {/* 已发布状态:显示归档按钮 */} + {food.status === "published" && ( + + )} + + {(food.status !== "published" || isSuperUser()) && ( <>
- {!isPublished && ( + {(!isPublished || isSuperUser()) && (
- - + + 家居装饰详情 批次管理 数据分析 - +
@@ -198,9 +116,18 @@ export default function HomeDecorDetailPage({ params }: { params: { id: string }
- {decor.name} + {decor.imageUrl && !decor.imageUrl.includes("placeholder") ? ( + {decor.name} + ) : ( +
+ + + + 暂无图片 +
+ )}
- {decor.status} + {decor.status || "未发布"}
@@ -213,39 +140,44 @@ export default function HomeDecorDetailPage({ params }: { params: { id: string }
-

类型

-

{decor.type}

+

装饰类型

+

{decor.category || "-"}

稀有度

-

{decor.rarity}

+

{decor.rarity || "-"}

发布日期

-

{decor.releaseDate || "尚未发布"}

-
-
-

状态

-

{decor.status}

+

{decor.publishedAt || "尚未发布"}

激活数量

-

{decor.activatedCount}

+

{activatedCount}

-

印刷总数

-

{decor.printedCount}

+

创建日期

+

{decor.createdAt || "-"}

-
-

描述

-

{decor.description}

+
+

激活率

+

{activationRate}%

+
+

装饰描述

+

{decor.description || "暂无描述"}

+
+ {isPublished && (
-

该家居装饰已发布,基本属性不可修改。您仍可以增加印刷数量。

+

+ {isSuperUser() + ? "该家居装饰已发布,您以超级管理员身份仍可编辑和删除。请谨慎操作。" + : "该家居装饰已发布,基本属性不可修改。您仍可以增加印刷数量。"} +

)} @@ -253,11 +185,11 @@ export default function HomeDecorDetailPage({ params }: { params: { id: string }
- +
- 印刷批次 + 印刷批次管理 管理家居装饰卡牌的印刷批次和卡牌ID
@@ -270,57 +202,79 @@ export default function HomeDecorDetailPage({ params }: { params: { id: string } 批次ID 创建日期 数量 - 起始ID - 结束ID - 激活数量 + 状态 操作 - {decor.batches.map((batch) => ( - - {batch.id} - {batch.date} - {batch.quantity} - {batch.startId} - {batch.endId} - {batch.activatedCount} - - - - - ))} + + + 批次数据将从后端加载 + +
-
- - + - 数据分析 - 查看家居装饰卡牌的激活数据和使用情况 + 批次操作 + 批量管理卡牌批次 -
-
-

激活数据图表

-
-
-

地区分布图表

-
-
-

时间趋势图表

-
+
+ +
+ + + + + 激活数据分析 + 家居装饰卡牌激活情况统计 + + +
+

激活数据图表将在此显示

+
+
+
+ +
+ + + 地区分布 + + +
+

地区分布图表将在此显示

+
+
+
+ + + + 时间趋势 + + +
+

时间趋势图表将在此显示

+
+
+
+
+
) diff --git a/qy-lty-admin/app/home-decor/page.tsx b/qy-lty-admin/app/home-decor/page.tsx index 15af04b..509b3c4 100644 --- a/qy-lty-admin/app/home-decor/page.tsx +++ b/qy-lty-admin/app/home-decor/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect, useCallback } from "react" import { DashboardShell } from "@/components/dashboard-shell" import { DashboardHeader } from "@/components/dashboard-header" import { Button } from "@/components/ui/button" @@ -8,96 +8,90 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { Input } from "@/components/ui/input" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Badge } from "@/components/ui/badge" -import { Search, Edit, Eye } from "lucide-react" +import { Search, Edit, Eye, Loader2, Archive } from "lucide-react" import { AddHomeDecorDialog } from "@/components/home-decor/add-home-decor-dialog" +import type { HomeDecor as ComponentHomeDecor } from "@/components/home-decor/home-decor-detail-dialog" import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog" +import { PublishConfirmationDialog } from "@/components/publish-confirmation-dialog" import { useToast } from "@/components/ui/use-toast" +import { isSuperUser } from "@/lib/api/auth" +import { getHomeDecors, deleteHomeDecor, publishHomeDecor, archiveHomeDecor } from "@/lib/api/home-decor" +import type { HomeDecor } from "@/lib/api/types" import Link from "next/link" -import type { HomeDecor } from "@/components/home-decor/home-decor-detail-dialog" -// 初始家居装饰数据 -const initialDecors: HomeDecor[] = [ - { - id: "DEC001", - name: "星空投影灯", - type: "灯饰", - rarity: "稀有", - description: "可以在房间内投影出美丽的星空,营造浪漫氛围。", - releaseDate: "2023-10-20", - status: "已发布", - activatedCount: 1342, - image: "/placeholder.svg?height=300&width=300", - }, - { - id: "DEC002", - name: "音乐主题壁纸", - type: "墙饰", - rarity: "普通", - description: "以音乐元素为主题的壁纸,适合洛天依的房间装饰。", - releaseDate: "2023-11-05", - status: "已发布", - activatedCount: 2156, - image: "/placeholder.svg?height=300&width=300", - }, - { - id: "DEC003", - name: "音符地毯", - type: "地饰", - rarity: "稀有", - description: "音符形状的地毯,踩上去会发出悦耳的音符声。", - releaseDate: "2023-12-15", - status: "已发布", - activatedCount: 987, - image: "/placeholder.svg?height=300&width=300", - }, - { - id: "DEC004", - name: "全息投影装置", - type: "科技装饰", - rarity: "传说", - description: "可以投影出洛天依的全息影像,实现虚拟互动。", - releaseDate: "2024-01-20", - status: "已发布", - activatedCount: 456, - image: "/placeholder.svg?height=300&width=300", - }, - { - id: "DEC005", - name: "樱花主题家具套装", - type: "家具套装", - rarity: "史诗", - description: "以樱花为主题的家具套装,包含床、桌椅、柜子等多件家具。", - releaseDate: "", - status: "未发布", - activatedCount: 0, - image: "/placeholder.svg?height=300&width=300", - }, -] +// 格式化日期时间 +function formatDate(dateStr: string): string { + if (!dateStr) return "" + try { + return new Date(dateStr).toLocaleDateString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }) + } catch { + return dateStr + } +} + +// 将 API HomeDecor 转换为组件显示用的 HomeDecor +function toDisplayDecor(decor: HomeDecor): ComponentHomeDecor { + return { + id: decor.id, + name: decor.name, + type: decor.category || "", + rarity: decor.rarity || "", + description: decor.description || "", + releaseDate: formatDate(decor.publishedAt || decor.createdAt || ""), + status: decor.status || "未发布", + activatedCount: decor.activeCardsCount || 0, + image: decor.imageUrl || "/placeholder.svg?height=300&width=300", + } +} export default function HomeDecorPage() { const { toast } = useToast() - const [decors, setDecors] = useState(initialDecors) + const [decors, setDecors] = useState([]) const [searchTerm, setSearchTerm] = useState("") const [currentPage, setCurrentPage] = useState(1) - const [selectedDecor, setSelectedDecor] = useState(null) + const [totalItems, setTotalItems] = useState(0) + const [selectedDecor, setSelectedDecor] = useState(null) const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [loading, setLoading] = useState(true) - const itemsPerPage = 5 + const itemsPerPage = 10 - // 过滤和分页 - const filteredDecors = decors.filter( - (decor) => - decor.name.toLowerCase().includes(searchTerm.toLowerCase()) || - decor.id.toLowerCase().includes(searchTerm.toLowerCase()) || - decor.type.toLowerCase().includes(searchTerm.toLowerCase()), - ) + // 从后端获取家居装饰列表 + const fetchDecors = useCallback(async () => { + try { + setLoading(true) + const response = await getHomeDecors({ + page: currentPage, + pageSize: itemsPerPage, + search: searchTerm || undefined, + }) + setDecors(response.items.map(toDisplayDecor)) + setTotalItems(response.total) + } catch (error) { + console.error("获取家居装饰列表失败:", error) + toast({ + title: "获取失败", + description: "无法获取家居装饰列表,请稍后重试", + variant: "destructive", + }) + } finally { + setLoading(false) + } + }, [currentPage, searchTerm, toast]) - const totalPages = Math.ceil(filteredDecors.length / itemsPerPage) - const paginatedDecors = filteredDecors.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage) + useEffect(() => { + fetchDecors() + }, [fetchDecors]) + + const totalPages = Math.ceil(totalItems / itemsPerPage) // 处理添加家居装饰 - const handleAddDecor = (newDecor: HomeDecor) => { - setDecors((prevDecors) => [...prevDecors, newDecor]) + const handleAddDecor = (newDecor: ComponentHomeDecor) => { + fetchDecors() toast({ title: "添加成功", description: `家居装饰 ${newDecor.name} 已成功添加`, @@ -105,8 +99,8 @@ export default function HomeDecorPage() { } // 处理编辑家居装饰 - const handleEditDecor = (updatedDecor: HomeDecor) => { - setDecors((prevDecors) => prevDecors.map((decor) => (decor.id === updatedDecor.id ? updatedDecor : decor))) + const handleEditDecor = (updatedDecor: ComponentHomeDecor) => { + fetchDecors() setSelectedDecor(null) setIsEditDialogOpen(false) toast({ @@ -117,19 +111,64 @@ export default function HomeDecorPage() { // 处理删除家居装饰 const handleDeleteDecor = async (decorId: string) => { - // 模拟API请求 - await new Promise((resolve) => setTimeout(resolve, 1000)) + try { + await deleteHomeDecor(decorId) + await fetchDecors() + toast({ + title: "删除成功", + description: "家居装饰已成功删除", + variant: "destructive", + }) + } catch (error) { + console.error("删除家居装饰失败:", error) + toast({ + title: "删除失败", + description: "无法删除家居装饰,请稍后重试", + variant: "destructive", + }) + } + } - setDecors((prevDecors) => prevDecors.filter((decor) => decor.id !== decorId)) - toast({ - title: "删除成功", - description: "家居装饰已成功删除", - variant: "destructive", - }) + // 发布家居装饰 + const handlePublishDecor = async (decorId: string, decorName: string) => { + try { + await publishHomeDecor(decorId) + await fetchDecors() + toast({ + title: "发布成功", + description: `家居装饰 "${decorName}" 已成功发布`, + }) + } catch (error) { + console.error("发布家居装饰失败:", error) + toast({ + title: "发布失败", + description: "无法发布家居装饰,请稍后重试", + variant: "destructive", + }) + } + } + + // 归档家居装饰 + const handleArchiveDecor = async (decorId: string, decorName: string) => { + try { + await archiveHomeDecor(decorId) + await fetchDecors() + toast({ + title: "归档成功", + description: `家居装饰 "${decorName}" 已归档`, + }) + } catch (error) { + console.error("归档家居装饰失败:", error) + toast({ + title: "归档失败", + description: "无法归档家居装饰,请稍后重试", + variant: "destructive", + }) + } } // 打开编辑对话框 - const openEditDialog = (decor: HomeDecor) => { + const openEditDialog = (decor: ComponentHomeDecor) => { setSelectedDecor(decor) setIsEditDialogOpen(true) } @@ -151,7 +190,7 @@ export default function HomeDecorPage() { value={searchTerm} onChange={(e) => { setSearchTerm(e.target.value) - setCurrentPage(1) // 重置到第一页 + setCurrentPage(1) }} />
@@ -161,90 +200,122 @@ export default function HomeDecorPage() { - - 家居装饰列表 - + 家居装饰列表
管理洛天依的家居装饰卡牌
- - - - ID - 装饰名称 - 类型 - 稀有度 - 发布日期 - 状态 - 激活数量 - 操作 - - - - {paginatedDecors.map((decor) => ( - - {decor.id} - {decor.name} - {decor.type} - {decor.rarity} - {decor.releaseDate || "-"} - - - {decor.status} - - - {decor.activatedCount} - - + {loading ? ( +
+ + 加载中... +
+ ) : ( +
+ + + ID + 装饰名称 + 类型 + 稀有度 + 发布日期 + 状态 + 激活数量 + 操作 + + + + {decors.map((decor) => ( + + {decor.id} + {decor.name} + {decor.type} + {decor.rarity} + {decor.releaseDate || "-"} + + + {decor.status} + + + {decor.activatedCount} + + - {decor.status !== "已发布" && ( - <> + {/* 草稿状态:显示发布按钮 */} + {decor.status === "草稿" && ( + handlePublishDecor(decor.id, decor.name)} + /> + )} + + {/* 已发布状态:显示归档按钮 */} + {decor.status === "已发布" && ( + )} - handleDeleteDecor(decor.id)} - /> - - )} - - - ))} + {(decor.status !== "已发布" || isSuperUser()) && ( + <> + - {paginatedDecors.length === 0 && ( - - - 没有找到匹配的家居装饰 - - - )} - -
+ handleDeleteDecor(decor.id)} + /> + + )} + + + ))} + + {decors.length === 0 && ( + + + 没有找到匹配的家居装饰 + + + )} + + + )}
- 显示 {paginatedDecors.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0}- - {Math.min(currentPage * itemsPerPage, filteredDecors.length)} 共 {filteredDecors.length} 个家居装饰 + 显示 {decors.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0}- + {Math.min(currentPage * itemsPerPage, totalItems)} 共 {totalItems} 个家居装饰
- {!isPublished && ( + {(!isPublished || isSuperUser()) && (
- - + + 服装详情 批次管理 数据分析 - +
@@ -151,9 +116,18 @@ export default function OutfitDetailPage({ params }: { params: { id: string } })
- {outfit.name} + {outfit.imageUrl && !outfit.imageUrl.includes("placeholder") ? ( + {outfit.name} + ) : ( +
+ + + + 暂无图片 +
+ )}
- {outfit.status} + {outfit.status || "未发布"}
@@ -167,38 +141,43 @@ export default function OutfitDetailPage({ params }: { params: { id: string } })

服装类型

-

{outfit.type}

+

{outfit.category || "-"}

稀有度

-

{outfit.rarity}

+

{outfit.rarity || "-"}

发布日期

-

{outfit.releaseDate || "尚未发布"}

+

{outfit.publishedAt || "尚未发布"}

激活数量

-

{outfit.activatedCount}

+

{activatedCount}

-

印刷总数

-

{outfit.printedCount}

+

创建日期

+

{outfit.createdAt || "-"}

-

剩余库存

-

{outfit.printedCount - outfit.activatedCount}

-
-
-

服装描述

-

{outfit.description}

+

激活率

+

{activationRate}%

+
+

服装描述

+

{outfit.description || "暂无描述"}

+
+ {isPublished && (
-

该服装已发布,基本属性不可修改。您仍可以增加印刷数量。

+

+ {isSuperUser() + ? "该服装已发布,您以超级管理员身份仍可编辑和删除。请谨慎操作。" + : "该服装已发布,基本属性不可修改。您仍可以增加印刷数量。"} +

)} @@ -206,11 +185,11 @@ export default function OutfitDetailPage({ params }: { params: { id: string } })
- +
- 印刷批次 + 印刷批次管理 管理服装卡牌的印刷批次和卡牌ID
@@ -223,55 +202,79 @@ export default function OutfitDetailPage({ params }: { params: { id: string } }) 批次ID 创建日期 数量 - 起始ID - 结束ID + 状态 操作 - {outfit.batches.map((batch) => ( - - {batch.id} - {batch.date} - {batch.quantity} - {batch.startId} - {batch.endId} - - - - - ))} + + + 批次数据将从后端加载 + +
-
- - + - 数据分析 - 查看服装卡牌的激活数据和使用情况 + 批次操作 + 批量管理卡牌批次 -
-
-

激活数据图表

-
-
-

地区分布图表

-
-
-

时间趋势图表

-
+
+ +
+ + + + + 激活数据分析 + 服装卡牌激活情况统计 + + +
+

激活数据图表将在此显示

+
+
+
+ +
+ + + 地区分布 + + +
+

地区分布图表将在此显示

+
+
+
+ + + + 时间趋势 + + +
+

时间趋势图表将在此显示

+
+
+
+
+
) diff --git a/qy-lty-admin/app/outfits/page.tsx b/qy-lty-admin/app/outfits/page.tsx index 8aabe8d..bda6533 100644 --- a/qy-lty-admin/app/outfits/page.tsx +++ b/qy-lty-admin/app/outfits/page.tsx @@ -1,166 +1,173 @@ "use client" -import { useState } from "react" +import { useState, useEffect, useCallback } from "react" import { DashboardShell } from "@/components/dashboard-shell" import { DashboardHeader } from "@/components/dashboard-header" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Input } from "@/components/ui/input" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Badge } from "@/components/ui/badge" -import { Search, Edit, Eye, Sparkles, Plus } from "lucide-react" +import { Search, Edit, Eye, Loader2, Archive } from "lucide-react" +import { AddOutfitDialog } from "@/components/outfits/add-outfit-dialog" +import type { DisplayOutfit } from "@/components/outfits/add-outfit-dialog" +import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog" +import { PublishConfirmationDialog } from "@/components/publish-confirmation-dialog" +import { useToast } from "@/components/ui/use-toast" +import { isSuperUser } from "@/lib/api/auth" +import { getOutfits, deleteOutfit, publishOutfit, archiveOutfit } from "@/lib/api/outfits" +import type { Outfit } from "@/lib/api/types" import Link from "next/link" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" + +function formatDate(dateStr: string): string { + if (!dateStr) return "" + try { + return new Date(dateStr).toLocaleDateString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }) + } catch { + return dateStr + } +} + +function toDisplayOutfit(outfit: Outfit): DisplayOutfit { + return { + id: outfit.id, + name: outfit.name, + type: outfit.category || "", + rarity: outfit.rarity || "", + description: outfit.description || "", + releaseDate: formatDate(outfit.publishedAt || outfit.createdAt || ""), + status: outfit.status || "未发布", + activatedCount: outfit.activeCardsCount || 0, + image: outfit.imageUrl || "", + } +} export default function OutfitsPage() { - // 直接在页面中实现对话框 - const [open, setOpen] = useState(false) - const [step, setStep] = useState(1) - const [outfitType, setOutfitType] = useState("") - const [rarity, setRarity] = useState("") - const [printQuantity, setPrintQuantity] = useState(1000) - const [isSubmitting, setIsSubmitting] = useState(false) + const { toast } = useToast() + const [outfits, setOutfits] = useState([]) + const [searchTerm, setSearchTerm] = useState("") + const [currentPage, setCurrentPage] = useState(1) + const [totalItems, setTotalItems] = useState(0) + const [selectedOutfit, setSelectedOutfit] = useState(null) + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false) + const [loading, setLoading] = useState(true) - const handleSubmit = async () => { - setIsSubmitting(true) - // 模拟API请求 - await new Promise((resolve) => setTimeout(resolve, 1500)) - setIsSubmitting(false) - setOpen(false) - // 重置表单 - setStep(1) + const itemsPerPage = 10 + + const fetchOutfits = useCallback(async () => { + try { + setLoading(true) + const response = await getOutfits({ + page: currentPage, + pageSize: itemsPerPage, + search: searchTerm || undefined, + }) + setOutfits(response.items.map(toDisplayOutfit)) + setTotalItems(response.total) + } catch (error) { + console.error("获取服装列表失败:", error) + toast({ + title: "获取失败", + description: "无法获取服装列表,请稍后重试", + variant: "destructive", + }) + } finally { + setLoading(false) + } + }, [currentPage, searchTerm, toast]) + + useEffect(() => { + fetchOutfits() + }, [fetchOutfits]) + + const totalPages = Math.ceil(totalItems / itemsPerPage) + + const handleAddOutfit = (newOutfit: DisplayOutfit) => { + fetchOutfits() + toast({ + title: "添加成功", + description: `服装 ${newOutfit.name} 已成功添加`, + }) } - const handleNext = () => { - setStep(step + 1) + const handleEditOutfit = (updatedOutfit: DisplayOutfit) => { + fetchOutfits() + setSelectedOutfit(null) + setIsEditDialogOpen(false) + toast({ + title: "更新成功", + description: `服装 ${updatedOutfit.name} 已成功更新`, + }) } - const handleBack = () => { - setStep(step - 1) + const handleDeleteOutfit = async (outfitId: string) => { + try { + await deleteOutfit(outfitId) + await fetchOutfits() + toast({ + title: "删除成功", + description: "服装已成功删除", + variant: "destructive", + }) + } catch (error) { + console.error("删除服装失败:", error) + toast({ + title: "删除失败", + description: "无法删除服装,请稍后重试", + variant: "destructive", + }) + } + } + + const handlePublishOutfit = async (outfitId: string, outfitName: string) => { + try { + await publishOutfit(outfitId) + await fetchOutfits() + toast({ + title: "发布成功", + description: `服装 "${outfitName}" 已成功发布`, + }) + } catch (error) { + console.error("发布服装失败:", error) + toast({ + title: "发布失败", + description: "无法发布服装,请稍后重试", + variant: "destructive", + }) + } + } + + const handleArchiveOutfit = async (outfitId: string, outfitName: string) => { + try { + await archiveOutfit(outfitId) + await fetchOutfits() + toast({ + title: "归档成功", + description: `服装 "${outfitName}" 已归档`, + }) + } catch (error) { + console.error("归档服装失败:", error) + toast({ + title: "归档失败", + description: "无法归档服装,请稍后重试", + variant: "destructive", + }) + } + } + + const openEditDialog = (outfit: DisplayOutfit) => { + setSelectedOutfit(outfit) + setIsEditDialogOpen(true) } return ( - - - - - - - - 添加新服装 - - 填写服装信息以创建新的服装卡牌。创建后将生成唯一的卡牌ID。 - - -
-
-
- - -
-
- - -
-
- -
-
- - -
-
- - setPrintQuantity(Number.parseInt(e.target.value))} - placeholder="输入印刷数量" - className="border-gray-300 focus-visible:ring-pink-500" - required - /> -
-
- -
- -