|
|
@@ -0,0 +1,219 @@
|
|
|
+# 客户端开发计划 · 详细实现规划 (todoDetail)
|
|
|
+
|
|
|
+> 结合后端实际(`serve_template` 题目/比赛模块已全部实现并运行中,Swagger 在线 `:8080/v3/api-docs`)与前端现状(仅完成 auth + 用户管理)。
|
|
|
+> 实施节奏:一次性完成 Phase 0–5,最后统一 `npm run build` + 手工联调。
|
|
|
+> 提交风格沿用仓库实际约定:`# feat: ...` / `# api: ...` / `# style: ...`(todo.md 中的 `Commit(ui):` 为标注,不作为 commit message 前缀)。
|
|
|
+
|
|
|
+## 全局前置结论(必须遵守)
|
|
|
+
|
|
|
+| 事项 | 结论 |
|
|
|
+|---|---|
|
|
|
+| API 生成 | 后端已全部完成,**只需在 Phase 1 开头跑一次** `npm run api:gen`,一次性生成 `problem-controller.ts` / `category-controller.ts` / `contest-controller.ts` / `user-controller.ts` / `auth-controller.ts` + 全部 models |
|
|
|
+| 生成物约束 | `src/api/models/` 与 `src/api/*.ts` 由 orval `clean:true` 生成,**禁止手改**;所有手写扩展类型放 `src/type/` 或组件内 |
|
|
|
+| 类型缺口 | Swagger 将 `GET /contest/{id}` 声明为 `Response<ContestVo>`,真实返回 `ContestDetailVo`(继承 ContestVo,多 `description/creatorUsername/registered/problems`);`ContestProblemVo` 无 schema。**必须手写扩展类型并强转** |
|
|
|
+| 响应体 | 统一 `Response<T>`:`code` 200=成功,400=错误;`axios-instance.ts` 已兼容 code 0/200,无需改动 |
|
|
|
+| 角色 | 后端角色常量:`user` / `coach` / `admin`(`User.java`)。教练相关接口均用 `hasAnyRole('ROLE_coach','ROLE_admin')` |
|
|
|
+| 状态/难度/类型常量 | 比赛状态 `pending/running/ended`;比赛类型 `public/private/password`;题目难度 `easy/medium/hard`;`isPublic`、`isHidden` 均为 0/1 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 0 路由与权限扩展
|
|
|
+
|
|
|
+### 改动清单
|
|
|
+
|
|
|
+1. `src/type/index.ts`
|
|
|
+ - `User.role` 联合类型由 `'admin' | 'user' | 'guest'` 改为 `'admin' | 'coach' | 'user' | 'guest'`(当前缺 `coach`,是 TS 硬伤,登录后角色即来自 `AuthTokenVo.role`)。
|
|
|
+
|
|
|
+2. `src/router/index.ts`
|
|
|
+ - 新增 `requiresCoach` 语义:admin 或 coach 可访问;`requiresAdmin` 保留为仅 admin。
|
|
|
+ - 路由守卫 `beforeEach` 增加分支:
|
|
|
+ ```
|
|
|
+ if (to.matched.some(r => r.meta.requiresAdmin)) -> 仅 admin,否则拒绝
|
|
|
+ else if (to.matched.some(r => r.meta.requiresCoach)) -> admin 或 coach,否则拒绝
|
|
|
+ 未登录一律跳 /login?redirect=fullPath
|
|
|
+ ```
|
|
|
+ - `adminRoutes` 重构:
|
|
|
+ - 父节点(`/admin`)meta 去掉 `requiresAdmin`,改为 `requiresCoach`(否则 coach 进不了后台)。
|
|
|
+ - 用户管理子项保留 `requiresAdmin`(仅 admin)。
|
|
|
+ - 删除占位路由 `/admin/other`(组件为 `AdminLayout` 嵌套自身,属隐藏 bug)。
|
|
|
+ - 父节点增加默认跳转 `redirect: '/admin/problems'`(Phase 2 注册后生效;暂可先指 `/admin/users`)。
|
|
|
+
|
|
|
+3. `src/layout/AdminLayout.vue`
|
|
|
+ - `menus` 计算属性按角色过滤子路由:
|
|
|
+ - `meta.requiresAdmin` → 仅 admin 显示;
|
|
|
+ - `meta.requiresCoach` → admin|coach 显示;
|
|
|
+ - 无权限标记项不显示。
|
|
|
+
|
|
|
+4. `src/layout/UserLayout.vue`
|
|
|
+ - `isAdmin` 改为 `isStaff = computed(() => ['admin','coach'].includes(role))`;
|
|
|
+ - "后台管理"菜单项 `v-if="isStaff"`。
|
|
|
+
|
|
|
+### 验证
|
|
|
+- `npm run build` 通过。
|
|
|
+- 三种账号(admin/coach/user)登录后查看后台入口与后台菜单差异。
|
|
|
+
|
|
|
+### Commit
|
|
|
+`# feat: 路由与布局支持coach后台入口`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 1 题目模块 · API生成 + 用户浏览页
|
|
|
+
|
|
|
+### Step 1.1 `npm run api:gen`
|
|
|
+- 前置:后端在线(`curl http://localhost:8080/v3/api-docs` 返回 200)。
|
|
|
+- 生成后确认新文件:`src/api/problem-controller.ts`、`src/api/category-controller.ts`、`src/api/contest-controller.ts`;models 新增 `problemVo`、`problemDetailVo`、`problemDto`、`problemCaseVo/Dto`、`problemTestVo/Dto`、`categoryVo/Dto`、`contestVo/Dto`、`contestProblemDto`、`contestRegisterDto`、`contestRegistrationVo`、`rankVo`、`pageVoListProblemVo`、`pageVoListContestVo`、`responseContestVo` 等。
|
|
|
+
|
|
|
+### Step 1.2 `src/view/problem/ProblemListView.vue`(`/problems`)
|
|
|
+- 搜索卡:标题(模糊)、难度下拉(easy/medium/hard)、分类下拉(数据源 `GET /category` → `CategoryVo[]`)、公开状态(isPublic 0/1,可空)。
|
|
|
+- 列表:`GET /problem`,参数 `title/difficulty/categoryId/isPublic/pageNum/pageSize` → `ResponsePageVoListProblemVo`。
|
|
|
+- 行展示:标题、难度标签、公开标签、时间/内存限制、通过/提交数;行点击跳详情。
|
|
|
+- 分页复用 `UserView.vue` 的 `el-pagination` 模式。
|
|
|
+
|
|
|
+### Step 1.3 `src/view/problem/ProblemDetailView.vue`(`/problems/:id`)
|
|
|
+- `GET /problem/{id}`(游客可访问,SecurityConfig 已开放)→ `ProblemDetailVo`。
|
|
|
+- 展示:标题、难度/公开标签、`timeLimitMs`(转秒/ms 展示)、`memoryLimitMb`、`tryCount`/`acceptedCount`、`description`、分类标签、样例列表 `cases`(input/output/description/sortOrder)。
|
|
|
+- 后端字段 `createdByUsername` 可作作者展示。
|
|
|
+
|
|
|
+### Step 1.4 路由与菜单
|
|
|
+- userRoutes 新增两条:`/problems`、`/problems/:id`(meta 带 title 与 icon,icon 可选用 `Document` / `Files`)。
|
|
|
+- UserLayout 顶部导航自动渲染(现有 `menus` 逻辑复用 userRoutes[0].children)。
|
|
|
+
|
|
|
+### Commit
|
|
|
+- `# api: 题目与比赛模块api(通过orval生成)`(Step 1.1 单独提交)
|
|
|
+- `# feat: 题目列表与详情页`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 2 题目模块 · 管理后台页
|
|
|
+
|
|
|
+### Step 2.1 `src/view/admin/ProblemManageView.vue`(`/admin/problems`)
|
|
|
+- 整体仿 `UserView.vue`:搜索卡 + 工具栏 + 表格 + 弹窗 + 分页。
|
|
|
+- 列表搜索:标题、难度、公开状态(isPublic)、分类。
|
|
|
+- 新增/编辑弹窗(`ProblemDto`):标题、描述(textarea)、时间限制(ms)、内存限制(MB)、难度、是否公开;**分类勾选** `categoryIds: number[]`(el-checkbox-group / el-select multiple)。
|
|
|
+- **详情弹窗**(样例与评测用例管理,核心):
|
|
|
+ - 样例 Tab:数据取 `GET /problem/{id}` 的 `cases`;操作 `POST /problem/{id}/case`(body `ProblemCaseDto` 含 problemId)、`PUT /problem/case`、`DELETE /problem/case?id=`。字段:input/output/description/sortOrder。
|
|
|
+ - 评测用例 Tab:`GET /problem/{id}/test` → `ProblemTestVo[]`;操作 `POST /problem/{id}/test`、`PUT /problem/test`、`DELETE /problem/test?id=`。字段:input/output/sortOrder、**`isHidden` 隐藏标记(0/1,el-switch)**。该接口仅 coach/admin 可见。
|
|
|
+- **删除按钮仅 admin 显示**(后端 `DELETE /problem` 为 `hasRole('ROLE_admin')`)。
|
|
|
+
|
|
|
+### Step 2.2 `src/view/admin/CategoryManageView.vue`(`/admin/categories`)
|
|
|
+- CRUD:`GET /category`、`POST /category`、`PUT /category`(body `CategoryDto`:id/categoryName/description)、`DELETE /category?id=`(**单 id 查询参数**,与 user 的 `ids` 数组签名不同,以 orval 生成为准)。
|
|
|
+- 删除按钮 admin-only(coach 可增改)。
|
|
|
+
|
|
|
+### Step 2.3 路由与菜单
|
|
|
+- adminRoutes 新增 `/admin/problems`、`/admin/categories`,meta 标 `requiresCoach` + icon(`Collection` 等)。
|
|
|
+- AdminLayout 侧边菜单自动按角色过滤。
|
|
|
+
|
|
|
+### Commit
|
|
|
+`# feat: 题目管理与分类管理后台页面`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 3 比赛模块 · 用户浏览页
|
|
|
+
|
|
|
+### Step 3.1 手写扩展类型(新增 `src/type/contest.ts`)
|
|
|
+```ts
|
|
|
+import type { ContestVo } from '@/api/models'
|
|
|
+
|
|
|
+export interface ContestProblem {
|
|
|
+ problemId: number
|
|
|
+ title: string
|
|
|
+ difficulty: string
|
|
|
+ displayOrder: number
|
|
|
+ score: number
|
|
|
+}
|
|
|
+
|
|
|
+export interface ContestDetail extends ContestVo {
|
|
|
+ description?: string
|
|
|
+ creatorUsername?: string
|
|
|
+ registered?: boolean
|
|
|
+ problems?: ContestProblem[]
|
|
|
+}
|
|
|
+```
|
|
|
+组件内强转:`const detail = res.data as unknown as ContestDetail`(生成类型 `ResponseContestVo.data` 为 `ContestVo`)。
|
|
|
+
|
|
|
+### Step 3.2 `src/view/contest/ContestListView.vue`(`/contests`)
|
|
|
+- 搜索:标题、状态(pending/running/ended)。
|
|
|
+- `GET /contest`(游客可访问,仅见 public 型)→ `ResponsePageVoListContestVo`。
|
|
|
+- 行展示:标题、type 标签(public/private/password)、起止时间(ISO 格式化 `YYYY-MM-DD HH:mm`)、状态标签、分页。
|
|
|
+
|
|
|
+### Step 3.3 `src/view/contest/ContestDetailView.vue`(`/contests/:id`)
|
|
|
+- **需登录**(`/contest/{id}` 未对游客开放):未登录跳 `/login?redirect=...`;401 由 axios 拦截器兜底跳登录。
|
|
|
+- 详情流程:
|
|
|
+ 1. `GET /contest/{id}`(不带 password);
|
|
|
+ 2. 若 `type==='password'` 且返回体 **无 `problems` 字段**(锁定态 base VO)→ 弹密码框 → 带 `?password=` 重拉;
|
|
|
+ 3. private 型无权限会返回错误消息(code 400),提示后返回。
|
|
|
+- 展示:标题、type/status 标签、起止时间、description、creatorUsername、**题目列表**(displayOrder 题号 / title / difficulty / score)。
|
|
|
+- 报名:
|
|
|
+ - 条件:`status` 为 pending/running 且 `registered !== true`。
|
|
|
+ - 点击 `POST /contest/{id}/register`,body `ContestRegisterDto { password? }`(密码型需再次传密码,后端会校验)。
|
|
|
+ - 成功重拉详情刷新 `registered`。
|
|
|
+- 已结束/进行中差异提示;进入排名入口链接到 `/contests/:id/ranking`。
|
|
|
+
|
|
|
+### Step 3.4 `src/view/contest/ContestRankingView.vue`(`/contests/:id/ranking`)
|
|
|
+- `GET /contest/{id}/ranking`(需登录)→ `RankVo[]`(userId/username/totalScore/maxAcMinutes/regTime)。
|
|
|
+- 表格:名次(序号)/用户/总分/AC最大耗时;**空列表显示空态提示**(无提交模块属正常)。
|
|
|
+- 需要比赛基本信息时可复用 `GET /contest/{id}`。
|
|
|
+
|
|
|
+### Step 3.5 路由与菜单
|
|
|
+- userRoutes 新增 `/contests`、`/contests/:id`、`/contests/:id/ranking`;菜单加"比赛"(icon `Trophy` 等)。
|
|
|
+
|
|
|
+### Commit
|
|
|
+`# feat: 比赛列表、详情与排名页面`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 4 比赛模块 · 管理后台页
|
|
|
+
|
|
|
+### Step 4.1 `src/view/admin/ContestManageView.vue`(`/admin/contests`)
|
|
|
+- 搜索 + 表格:标题、类型、状态、起止时间。
|
|
|
+- CRUD 弹窗(`ContestDto`):
|
|
|
+ - title / description / type(public/private/password)/ password(type=password 时必填)/ startDatetime / endDatetime。
|
|
|
+ - **时间控件**:`el-date-picker type="datetime" value-format="YYYY-MM-DDTHH:mm:ss"`,对齐后端 `LocalDateTime` 反序列化(ISO 格式)。
|
|
|
+ - 校验:start < end。
|
|
|
+- **题目配置弹窗**(核心):
|
|
|
+ - 左侧 `GET /problem`(分页 + 标题搜索 + 多选)选题目;
|
|
|
+ - 对已选题设置 `displayOrder`(显示序号)+ `score`(分值);
|
|
|
+ - 提交:逐条 `POST /contest/{id}/problem`(新增)/ `PUT /contest/{id}/problem`(改)/ `DELETE /contest/{id}/problem?problemId=`(移除),body `ContestProblemDto { problemId, score, displayOrder }`。
|
|
|
+ - 配置态展示可由 `GET /contest/{id}`(含 problems)回显。
|
|
|
+- **报名名单**:`GET /contest/{id}/registrations`(coach/admin)→ `ContestRegistrationVo[]`(id/userId/username/createdAt)表格,独立弹窗或 Tab。
|
|
|
+- 注意:比赛开始后后端禁止修改(`demand.md`),操作失败按后端消息提示。
|
|
|
+
|
|
|
+### Step 4.2 路由与菜单
|
|
|
+- adminRoutes 新增 `/admin/contests`,meta `requiresCoach`;侧边菜单自动渲染。
|
|
|
+
|
|
|
+### Commit
|
|
|
+`# feat: 比赛管理后台页面`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Phase 5 验证
|
|
|
+
|
|
|
+### 5.1 构建
|
|
|
+- `npm run build`(vue-tsc 严格模式)全绿。
|
|
|
+- 重点排查点:`noUnusedLocals/noUnusedParameters` 严格报错;`ContestDetail` 强转写法;orval 生成类型使用是否正确。
|
|
|
+- 可选 `npx prettier --write .` 统一格式。
|
|
|
+
|
|
|
+### 5.2 手工联调(`npm run dev`,需后端在线)
|
|
|
+完整链路:
|
|
|
+1. 游客:浏览公开题目列表/详情、比赛列表 → 访问比赛详情/排名被引导登录。
|
|
|
+2. 登录(user 角色):查看题目详情(含样例)、比赛详情、报名 public 比赛(密码型走密码弹框)、排名页空态正常。
|
|
|
+3. 教练:后台入口可见(无"用户管理"菜单);建题 → 配样例 → 配评测用例(含隐藏标记)→ 建比赛 → 配题设分 → 查看报名名单;coach 无删除按钮。
|
|
|
+4. 管理员:后台可见"用户管理";删除题目/分类/比赛可用。
|
|
|
+5. 角色隔离:user 访问 `/admin/**` 被拒;coach 访问 `/admin/users` 被拒。
|
|
|
+
|
|
|
+### 5.3 收尾(可选)
|
|
|
+- 更新 `/home/yangyi/OJ/AGENTS.md`("仅 User 域实现"已过时,题目/比赛模块已上线)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 风险与注意点清单
|
|
|
+
|
|
|
+1. **`/contest/{id}` 类型不符**:Swagger 类型是 `ContestVo`,真实返回 `ContestDetailVo`,必须用 `src/type/contest.ts` 扩展 + 强转,不能依赖生成 models。
|
|
|
+2. **`coach` 角色缺失**:`src/type/index.ts` 未含 `coach`,Phase 0 必须补,否则登录为 coach 时 TS 报错。
|
|
|
+3. **时间格式**:前端提交比赛时间用 `YYYY-MM-DDTHH:mm:ss`,后端 `LocalDateTime` 才可反序列化;展示时 ISO → 可读格式。
|
|
|
+4. **删除权限差异**:题目/分类删除仅 admin,比赛删除 coach/admin(service 限制只能删自己创建的);前端按角色隐藏按钮,后端仍会兜底校验。
|
|
|
+5. **单删 vs 批量删参数**:user/contest 删除用 `ids` 数组;category/case/test 删除用单个 `id` 查询参数,按 orval 生成签名调用。
|
|
|
+6. **密码型比赛锁定态判定**:以返回体是否含 `problems` 字段区分 base VO 与 detail(Swagger 未提供显式标记)。
|
|
|
+7. **`api:gen` 前置**:需后端在线;`clean:true` 会重写全部 controller/models,属正常。
|
|
|
+8. **`/admin` 无默认页**:Phase 0 加 redirect;点击 UserLayout"后台管理"应先跳到默认后台页。
|
|
|
+9. **排名为空属正常**:提交/判题模块本期不实现,`RankVo[]` 空数组给空态 UI 即可。
|