feat(portal-shell): 管理域 §9.4 B5 全量迁移(24 + 21 补充批次共 44 页 + 42 features)

按 ARCHITECTURE.md §9.4 规划口径 + admin-NeedTodo.md §四补充批次完成管理域全量页面迁移:

【§9.4 规划 24 页(B5)】
- users(2) + roles(1) + permissions(1) + audit-logs(4) + invitation-codes(1)
- school(6: redirect/schools/classes/departments/academic-year/grades)
- announcements(1) + files(1) + ai-settings(1) + system(1) + viewports(1)
- students(1) + teachers(1) + organization(1) + plugins(1, config-service)
- 仪表盘已存在(/shell/admin/page.tsx)

【§四补充批次 21 页】
- course-plans(4) + elective(4) + questions(1) + lesson-plans(2) + error-book(1)
- scheduling(3: auto/changes/rules) + attendance(1) + curriculum-map(1)
- announcements 详情/编辑(2) + roles/[id] 详情(1) + users/import(1)

【实现要点】
- 全部使用 ListPageShell + loading/error/empty 三态规范(§11.3 DoD)
- 走 lib/api hooks;未就绪契约走 MSW + @contract-pending 注释(§11.4)
- 文案走 useTranslations(zh-CN + en 两份同步更新)
- 42 个 features/<domain>/transformations.ts 纯函数 + 配套 vitest 单测
- catch 块统一 notify.error;无空 catch;lint:tokens 通过
- 路由全部登记到 route-permissions.ts(39 EXACT + 8 PREFIX)

【验收】
- tsc --noEmit: 0 errors
- ESLint src: 0 errors (4 generated-files warnings, pre-existing)
- lint:tokens: 0 errors
- vitest: 1639/1639 passed (含 23 admin 测试文件 671 用例)
- check:routes: PASS (143 routes, 4 ghost entries pre-existing)
- check:pages: PASS (146 pages)
- check:codegen: PASS
- arch:scan: 24 modules, 8262 symbols

关联:ARCHITECTURE.md §9.4 / §10 P5 / §11.3 DoD / §11.6
This commit is contained in:
SpecialX
2026-07-24 23:07:20 +08:00
parent 5a9f652943
commit 062d9e9582
394 changed files with 60468 additions and 118 deletions

View File

@@ -0,0 +1,795 @@
# 学生域Student待完成功能分析
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\student\`
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\student\`
> 规划依据:`apps/portal-shell/ARCHITECTURE.md` §9.236 页B3 批次)
> 分析日期2026-07-24
> 任务范围:仅分析,不写代码
---
## 一、页面完成度总览
### 1.1 portal-shell 学生域现状
当前 `apps/portal-shell/src/app/shell/student/` 下**仅有 1 个页面**
| 路径 | 状态 | 说明 |
| ------------------------- | --------- | ------------------------------------------------ |
| `/shell/student/page.tsx` | ✅ 已完成 | 学生仪表盘(仅基础版,缺 trend/weakness 详情页) |
### 1.2 完成度统计
| 状态 | 数量 | 说明 |
| ------------ | ------ | ---------------------------------------------------------- |
| ✅ 已完成 | 1 | `/shell/student/page.tsx`(仪表盘基础版,仍缺 2 个详情页) |
| 🟡 部分完成 | 1 | dashboard 模块基础页有trend/weakness 详情页缺) |
| ❌ 缺失 | 34 | 见下方模块清单 |
| **规划总数** | **36** | ARCHITECTURE.md §9.2 B3 批次 |
### 1.3 portal-shell 仪表盘与 CICD 仪表盘的技术差异(关键架构差异)
| 维度 | CICD 仪表盘 | portal-shell 仪表盘 |
| -------- | ---------------------------------------------------------------------- | --------------------------------------------------- |
| 渲染模式 | Server Component`async function` | Client Component`"use client"` |
| 数据获取 | Server Action `getStudentDashboardAction()` + Suspense 流式渲染 | `useStudentDashboard()` Hook`@/lib/api`,走 BFF |
| 国际化 | `next-intl` `getTranslations` | 无(硬编码中文) |
| 权限校验 | `requirePermission` + `getAuthContext` | 无 |
| 视图组件 | `StudentDashboard` 视图模块(拆分到 `@/modules/dashboard/components` | 直接内联在 page.tsx |
| 数据来源 | 直查 DBDrizzle | 走 data-ana 微服务聚合 |
> **重要提示**portal-shell 已转向微服务 + BFF 架构,迁移时**不能照搬 CICD 的 Server Action + Drizzle 直查模式**。学生页面应通过 `teacher-bff` / `data-ana` 等服务的 gRPC/HTTP API 获取数据,前端通过 hooks 消费。
---
## 二、缺失页面清单(按模块分组)
### 2.1 dashboard 模块3 页规划1 已完成2 缺失)
#### 2.1.1 ✅ `/shell/student/page.tsx`(已完成)
- **portal-shell 实现**:基础仪表盘,显示平均分 / 班级排名 / 待交作业 + 薄弱知识点 + 近期成绩趋势
- **技术栈**Client Component + `useStudentDashboard()` Hook + `DashboardShell` / `StatCard` 共享组件
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\dashboard\page.tsx`
- **CICD 技术栈**Server Component + `getStudentDashboardAction()` Server Action + `StudentDashboard` 视图组件 + Suspense 流式渲染
- **ARCHITECTURE.md 契约**:✅ `studentDashboard`
#### 2.1.2 ❌ `/shell/student/trend`(学习趋势详情页,缺失)
- **CICD 参考实现**CICD 无独立 `/trend` 页面(融合在 dashboard 中以 `recent_trends` 字段呈现)
- **功能描述**:学生学习趋势详情页,应展示更完整的历史成绩趋势、知识点掌握趋势、对比基线
- **技术栈建议**:复用 `learningTrend` 契约 + 趋势图组件Recharts/Echarts
- **ARCHITECTURE.md 契约**:✅ `learningTrend`
- **CICD 关键代码片段**dashboard 中仅以单条 `recent_trends: { date, score }` 呈现,需 portal-shell 自行扩展为详情页
#### 2.1.3 ❌ `/shell/student/weakness`(薄弱知识点详情页,缺失)
- **CICD 参考实现**CICD 无独立 `/weakness` 页面(融合在 dashboard 中以 `weak_points: { title, mastery, error_count }` 字段呈现)
- **功能描述**:学生薄弱知识点详情页,应展示完整的薄弱知识点列表、按学科分组、关联错题、推荐练习
- **技术栈建议**:复用 `studentWeakness` 契约 + 错题本 / 练习模块联动跳转
- **ARCHITECTURE.md 契约**:✅ `studentWeakness`
- **CICD 关键代码片段**dashboard 中仅以单条 `weak_points` 呈现,需 portal-shell 自行扩展
---
### 2.2 grades 模块2 页规划0 已完成2 缺失)
#### 2.2.1 ❌ `/shell/student/grades`(我的成绩列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\grades\page.tsx`
- **功能描述**
- 学生本人成绩汇总(姓名 + 记录数)
- 学科 / 类型 / 学期 / 关键字 4 维过滤器
- **成绩趋势卡片**(含班级平均对比线)
- **排名趋势卡片**
- **班级分布直方图**(隐私保护视图,仅展示本人位置标注)
- **学生纵向成长档案**(跨学年/学期聚合)
- 成绩汇总明细表
- **技术栈**
- Server Component`async function`+ `force-dynamic`
- Server Actions`getStudentGradeSummary` / `getRankingTrend` / `getClassAverageTrend` / `getStudentPositionInClassDistribution` / `getStudentGrowthArchive` / `getSubjectOptions`
- `requirePermission(Permissions.GRADE_RECORD_READ)` + `ctx.dataScope`
- 组件:`StudentGradeSummary` / `StudentGradeFilters` / `GradeTrendCard` / `RankingTrendCard` / `GradeDistributionChart` / `GrowthArchiveChart` / `EmptyState`
- searchParams 过滤(`q` / `subject` / `type` / `semester`
- **ARCHITECTURE.md 契约**:❌(列表,未定义)
#### 2.2.2 ❌ `/shell/student/grades/report-card`(成绩报告卡,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\grades\report-card\page.tsx`
- **功能描述**
- 学年 / 学期可切换的成绩报告卡
- 报告卡视图(学生本人维度聚合)
- **打印操作**`ReportCardPrintAction`
- 返回成绩列表的面包屑
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getReportCardData(ctx.userId, ctx.dataScope, { academicYearId, semester })` + `getAcademicYears()`
- `requirePermission(Permissions.GRADE_RECORD_READ)`
- 组件:`ReportCardView` / `ReportCardPrintAction` / `Button` / `EmptyState`
- searchParams`academicYearId` / `semester`
- **ARCHITECTURE.md 契约**:❌(未单独定义)
---
### 2.3 exams 模块3 页规划0 已完成3 缺失)⚠️ 重点
> **CICD 参考项目无 exams 模块学生页面**。学生考试相关功能需 portal-shell 从零设计。
#### 2.3.1 ❌ `/shell/student/exams`(我的考试列表,缺失)
- **CICD 参考实现**:❌ CICD 无对应源页面
- **功能描述**:学生视角的考试列表,应展示:
- 即将到来的考试(含倒计时、考场、座位号)
- 已结束考试(含成绩、查看结果入口)
- 按学科 / 状态过滤
- **技术栈建议**:参考 CICD `learning/assignments/page.tsx` 的列表分组 + 状态徽章模式
- **ARCHITECTURE.md 契约**:🟡(部分,需补充)
#### 2.3.2 ❌ `/shell/student/exams/[id]/result`(考试结果页,缺失)
- **CICD 参考实现**:❌ CICD 无对应源页面(可参考 `learning/assignments/[assignmentId]/result/page.tsx` 的同构模式)
- **功能描述**
- 考试得分、排名、班级均分对比
- 题目作答详情(每题对错、参考答案、学生答案)
- 知识点掌握情况
- 错题一键加入错题本
- **技术栈建议**:参考 CICD `HomeworkSubmissionResult` 组件 + `getStudentSubmissionResult` 数据访问模式
- **ARCHITECTURE.md 契约**:🟡(部分,需补充)
#### 2.3.3 ❌ `/shell/student/exams/[id]/take`(考试作答工作台,缺失)⚠️ 关键
- **CICD 参考实现**:❌ CICD 无对应源页面(可参考 `learning/assignments/[assignmentId]/page.tsx``HomeworkTakeView` 同构模式)
- **功能描述**:考试作答工作台,应包含:
- 题目导航栏(题号、未答/已答/标记状态)
- 题目作答区(单选/多选/填空/简答)
- 倒计时(自动提交)
- 答案本地暂存(防刷新丢失)
- 提交确认 + 二次确认
- 中途断网恢复
- **技术栈建议**
- 客户端组件(`HomeworkTakeView` 是 client component
- 答案本地存储localStorage / IndexedDB
- 自动保存定时器
- 提交走 Server Action / BFF API
- **ARCHITECTURE.md 契约**:🟡(部分,需补充)
> ⚠️ **此页是学生域最复杂、最关键的页面**,需在 B3 中重点投入。CICD 的 `HomeworkTakeView` 是最近的同构参考,但其路由是 `learning/assignments/[assignmentId]` 而非 `exams/[id]/take`。
---
### 2.4 homework 模块3 页规划0 已完成3 缺失)
> **CICD 实际路由是 `learning/assignments/*`**ARCHITECTURE.md §9.2 计划将其提升到 `homework/*` 顶层。
#### 2.4.1 ❌ `/shell/student/homework`(我的作业列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\assignments\page.tsx`
- **功能描述**
- 作业列表,按**学科分组**展示
- 每组内按"未答 / 已答"二分桶PERF-05 单次遍历分桶)
- 每张卡片:标题、状态徽章、截止时间、剩余尝试次数、最新得分、逾期提示
- 学科 dot 颜色hash 稳定映射)
- 状态过滤all/pending/submitted/graded+ 关键字搜索
- `AssignmentCard``React.memo` 优化高频渲染
- **技术栈**
- Server Component + `force-dynamic`
- Server Action`getStudentHomeworkAssignments(student.id)`
- 组件:`AssignmentFilters` / `StatusBadge` / `Card` / `EmptyState` / `memo(AssignmentCard)`
- 工具:`formatDate` / `getParam` / `cn` / `STUDENT_HOMEWORK_PROGRESS_VARIANT`
- **ARCHITECTURE.md 契约**:🟡(部分)
#### 2.4.2 ❌ `/shell/student/homework/[id]/submit`(作业作答/提交,缺失)⚠️ 关键
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\assignments\[assignmentId]\page.tsx`
- **功能描述**
- 根据 `submission.status` 分流:
- `graded` / `submitted` → 进入 `HomeworkReviewView`(查看提交后/批改后视图)
- `not_started` / `in_progress` → 进入 `HomeworkTakeView`(作答工作台)
- 顶部展示作业标题、截止时间、最大尝试次数
- **技术栈**
- Server Component外壳+ Client Component`HomeworkTakeView` / `HomeworkReviewView`
- Server Action`getStudentHomeworkTakeData(assignmentId, student.id)`
- 组件:`HomeworkTakeView` / `HomeworkReviewView`
- **ARCHITECTURE.md 契约**:🟡(部分)
#### 2.4.3 ❌ `/shell/student/homework/[id]/analysis`(作业分析/结果,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\assignments\[assignmentId]\result\page.tsx`
- **功能描述**
- 作业标题 + 提交结果详情
- 通过 `HomeworkSubmissionResult` 组件展示分析(含每题得分、参考答案等)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getHomeworkAssignmentById(assignmentId)` + `getStudentSubmissionResult(assignmentId, studentId)`
- 通过 `getSession()` 获取 studentId`requirePermission` 模式,需注意)
- 组件:`HomeworkSubmissionResult`
- **ARCHITECTURE.md 契约**:🟡(部分)
---
### 2.5 schedule 模块1 页规划0 已完成1 缺失)
#### 2.5.1 ❌ `/shell/student/schedule`(学生课表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\schedule\page.tsx`
- **功能描述**
- 学生个人课表(按班级聚合)
- 班级过滤器(`classId` searchParam默认 `all`
- 顶部标题 + 班级筛选器
- `StudentScheduleView` 渲染课表
- **技术栈**
- Server Component + `force-dynamic` + `generateMetadata`
- Server Actions`getStudentClasses(student.id)` + `getStudentSchedule(student.id)`
- `requirePermission(Permissions.CLASS_READ)` + `getCurrentStudentUser()`
- 组件:`StudentScheduleFilters` / `StudentScheduleView` / `EmptyState`
- searchParams`classId`
- **ARCHITECTURE.md 契约**:❌
---
### 2.6 attendance 模块1 页规划0 已完成1 缺失)
#### 2.6.1 ❌ `/shell/student/attendance`(学生考勤,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\attendance\page.tsx`
- **功能描述**
- 学生本人考勤汇总(出勤率、迟到/早退/请假次数)
- 通过 `StudentAttendanceView` 渲染详细记录
- 无数据时显示 `EmptyState`
- **技术栈**
- Server Component + `force-dynamic`
- Server Action`getStudentAttendanceSummary(ctx.userId)`
- `requirePermission(Permissions.ATTENDANCE_READ)` + `getAuthContext()`
- 组件:`StudentAttendanceView` / `EmptyState`
- **ARCHITECTURE.md 契约**:❌
---
### 2.7 classes 模块1 页规划0 已完成1 缺失)
#### 2.7.1 ❌ `/shell/student/classes`(我的班级,缺失)
- **CICD 参考实现**:❌ CICD 学生端无独立 `/classes` 路由(班级信息融合在 `learning/courses/*` 中)
- **功能描述**
- 学生所在的班级列表(含班主任、教室、学校、年级、活跃状态)
- 区别于 courses课程classes 侧重"行政班/教学班"维度courses 侧重"学科课程"维度
- 可跳转到该班的课表、作业、同学(受权限限制)
- **技术栈建议**:参考 CICD `learning/courses/page.tsx` 的列表 + 过滤模式
- **ARCHITECTURE.md 契约**:❌ `myClasses`
---
### 2.8 courses 模块2 页规划0 已完成2 缺失)
#### 2.8.1 ❌ `/shell/student/courses`(课程列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\courses\page.tsx`
- **功能描述**
- 学生报名的课程(班级)列表
- 关键字搜索(名称 / 教师 / 学校 / 班主任)
- 通过 `StudentCoursesView` 渲染
- 仅有班级时才显示过滤器
- **技术栈**
- Server Component + `force-dynamic` + `generateMetadata`
- Server Action`getStudentClasses(student.id)`
- `requirePermission(Permissions.CLASS_READ)` + `getCurrentStudentUser()`
- 组件:`CourseFilters` / `StudentCoursesView` / `EmptyState`
- searchParams`q`
- **ARCHITECTURE.md 契约**:❌
#### 2.8.2 ❌ `/shell/student/courses/[id]`(课程详情,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\courses\[classId]\page.tsx`
- **功能描述**
- 班级(课程)详情页:名称、年级、班主任、教室、学校
- **三列卡片**:教师信息(姓名 + 邮箱 mailto/ 学校信息 / 教室信息
- **本班课表**:按 weekday 排序的列表
- 顶部按钮:跳转完整课表、跳转作业列表
- **技术栈**
- Server Component + `force-dynamic` + `generateMetadata`
- Server Actions`getStudentClassById(student.id, classId)` + `getStudentSchedule(student.id)`
- `requirePermission(Permissions.CLASS_READ)` + `getCurrentStudentUser()`
- 组件:`Card` / `Badge` / `Button` / `EmptyState`
- 路由参数:`classId`
- **ARCHITECTURE.md 契约**:❌
---
### 2.9 course-plans 模块2 页规划0 已完成2 缺失)
#### 2.9.1 ❌ `/shell/student/course-plans`(课程计划列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\course-plans\page.tsx`
- **功能描述**
- 学生视角的课程计划列表(仅 `status: "active"`
- 通过 `class_members` scope 过滤可见的 classIds
- `CoursePlanList` 渲染detailBaseHref 指向 `/student/course-plans`
- **技术栈**
- Server Component + `force-dynamic`
- Server Action`getCoursePlans({ status: "active" }, { userId, isAdmin: false, classIds })`
- `requirePermission(Permissions.COURSE_PLAN_READ)`
- 组件:`CoursePlanList`
- **ARCHITECTURE.md 契约**:❌
#### 2.9.2 ❌ `/shell/student/course-plans/[id]`(课程计划详情,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\course-plans\[id]\page.tsx`
- **功能描述**
- 课程计划详情视图(学生只读)
- 通过 class_members scope 过滤
- `CoursePlanDetail` 渲染,关联教材跳转 `/student/learning/textbooks`
- **技术栈**
- Server Component + `force-dynamic`
- Server Action`getCoursePlanById(id, { userId, isAdmin: false, classIds })`
- `requirePermission(Permissions.COURSE_PLAN_READ)`
- 组件:`CoursePlanDetail`
- 路由参数:`id`
- `notFound()` 兜底
- **ARCHITECTURE.md 契约**:❌
---
### 2.10 lesson-plans 模块2 页规划0 已完成2 缺失)
#### 2.10.1 ❌ `/shell/student/lesson-plans`(教案列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\lesson-plans\page.tsx`
- **功能描述**
- 学生视角的教案列表(仅 `status: "published"`
- `LessonPlanList` 学生视图模式(`viewMode="student"`
- `LessonPlanProviderSetup` 注入学生角色配置(让筛选生效)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getLessonPlans({ status: "published" }, ctx.dataScope, ctx.userId)` + `getSubjectOptions()`
- `requirePermission(Permissions.LESSON_PLAN_READ)`
- 组件:`LessonPlanList` / `LessonPlanProviderSetup` / `STUDENT_ROLE_CONFIG` / `Skeleton` + `Suspense`
- **ARCHITECTURE.md 契约**:❌
#### 2.10.2 ❌ `/shell/student/lesson-plans/[planId]/view`(教案只读查看,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\lesson-plans\[planId]\view\page.tsx`
- **功能描述**
- 教案只读视图
- **权限收敛**:学生仅可查看本年级已发布教案(`assertPlanInScope`),未发布显示提示
- 拉取教材标题 + 章节标题(并行 `Promise.all`
- `LessonPlanReadonlyView` 渲染文档内容
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getLessonPlanById(planId, ctx.userId)` + `getTextbookById` + `getChaptersByTextbookId` + `findChapterById`
- `requirePermission(Permissions.LESSON_PLAN_READ)` + `assertPlanInScope(plan, ctx)`
- 组件:`LessonPlanReadonlyView` / `Skeleton` + `Suspense`
- 路由参数:`planId`
- **ARCHITECTURE.md 契约**:❌
---
### 2.11 textbooks 模块2 页规划0 已完成2 缺失)
#### 2.11.1 ❌ `/shell/student/textbooks`(教材列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\textbooks\page.tsx`
- **功能描述**
- 学生视角的教材列表
- **强制按学生所在年级过滤**`getGradeNameById` 解析 gradeId 为年级名称)
- 关键字 / 学科 / 年级 3 维过滤器
- `TextbookCard` 网格4 列响应式)
- 学生端 `hideActions`(无编辑权限)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getTextbooksWithScope(q, subject, grade, { grade: studentGradeName })` + `getGradeNameById`
- `requirePermission(Permissions.TEXTBOOK_READ)` + `getCurrentStudentUser()`
- 组件:`TextbookCard` / `TextbookFilters` / `EmptyState`
- searchParams`q` / `subject` / `grade`
- **ARCHITECTURE.md 契约**:🟡
#### 2.11.2 ❌ `/shell/student/textbooks/[id]/chapters`(教材章节阅读器,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\textbooks\[id]\page.tsx`
- **功能描述**
- 教材阅读器(`TextbookReader`
- 顶部固定标题栏:教材标题 + 学科 Badge + 年级 Badge
- 章节列表 + 阅读区
- **年级校验**:教材年级与学生年级不匹配则 `notFound()`(防越权)
- 学生端不传 `renderQuestionCreator`(无题目创建权限)
- 高度自适应(`h-[calc(100vh-4rem-3rem)]`
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getTextbookById(id)` + `getChaptersByTextbookId(id)` + `getGradeNameById`
- `requirePermission(Permissions.TEXTBOOK_READ)` + `getCurrentStudentUser()`
- 组件:`TextbookReader` / `Badge` / `EmptyState`
- 路由参数:`id`
- **ARCHITECTURE.md 契约**:🟡
- **注**ARCHITECTURE.md §9.2 路径为 `/[id]/chapters`CICD 路径为 `/[id]`,迁移时需对齐
---
### 2.12 error-book 模块1 页规划0 已完成1 缺失)
#### 2.12.1 ❌ `/shell/student/error-book`(错题本,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\error-book\page.tsx`
- **功能描述**
- **5 列统计卡片**:总数 / 新增 / 学习中 / 已掌握(含掌握率%/ 待复习(高亮 overdue
- **新增错题对话框**`AddErrorBookDialogWithQuestions`,带题目选择器)
- 筛选器:关键字 / 状态 / 来源 / 仅看待复习
- 错题列表(客户端组件 `StudentErrorBookListClient`
- **AI 上下文注入**`AiClientProvider` + `createCoreAiClientService`
- `WidgetBoundary` 包裹关键 widget降级容错
- `Suspense` 流式渲染过滤器 + 结果列表
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getErrorBookItems({ studentId, q, status, sourceType, dueOnly, pageSize: 50 })` + `getErrorBookStats(ctx.userId)`
- `requirePermission(Permissions.ERROR_BOOK_READ)`
- 组件:`StatsGrid` / `AddErrorBookDialogWithQuestions` / `ErrorBookFilters` / `StudentErrorBookListClient` / `WidgetBoundary` / `AiClientProvider`
- 状态枚举守卫:`isErrorBookStatus` / `isErrorBookSource`(类型守卫)
- **ARCHITECTURE.md 契约**:✅
---
### 2.13 learning 模块2 页规划0 已完成2 缺失)
#### 2.13.1 ❌ `/shell/student/learning`(学习中心首页,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\page.tsx`
- **功能描述**
- 学习中心首页3 张导航卡片:
1. **课程**(已报名班级数)
2. **作业**(待交数 + 即将到期数7 天内)
3. **教材**(可用数)
- 每张卡片含图标 / 标题 / 描述 / 统计 / 箭头
- 学生身份校验(`getCurrentStudentUser`),无则显示 `EmptyState`
- **技术栈**
- Server Component + `force-dynamic` + `generateMetadata`
- Server Actions`getStudentClasses` + `getStudentHomeworkAssignments` + `getTextbooks`
- `requirePermission(Permissions.CLASS_READ)`
- 组件:`Card` / `EmptyState`
- 业务逻辑:`pendingCount`(未提交+未批改)/ `dueSoonCount`7 天内到期)计算
- **ARCHITECTURE.md 契约**:❌
#### 2.13.2 ❌ `/shell/student/learning-path`AI 学习路径,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\learning\study-path\page.tsx`
- **功能描述**
- AI 生成的个性化学习路径
- `AiStudyPath` 客户端组件,由 dashboard layout 注入 `AiClientProvider`
- 学生身份校验
- **技术栈**
- Server Component + `force-dynamic` + `generateMetadata`
- `requirePermission(Permissions.AI_CHAT)`(与 AI_CHAT 权限点一致,防止学生角色被关闭后页面仍可访问)
- `getCurrentStudentUser()`
- 组件:`AiStudyPath`client component/ `EmptyState`
- **ARCHITECTURE.md 契约**:❌
- **注**ARCHITECTURE.md §9.2 路径为 `/learning-path`顶层CICD 路径为 `/learning/study-path`,迁移时需对齐
---
### 2.14 practice 模块2 页规划0 已完成2 缺失)
#### 2.14.1 ❌ `/shell/student/practice`(自适应练习首页,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\practice\page.tsx`
- **功能描述**
- **4 列统计卡片**:总练习次数 / 已完成 / 总答题数 / 正确率
- 左侧:`PracticeStarterWithNav`(练习启动器 + 知识点选择 + 导航)
- 右侧:`PracticeHistory`(历史会话列表)
- `PracticeServiceProvider` 包裹(注入练习服务)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getPracticeStats(ctx.userId)` + `getPracticeSessions(ctx.userId, { pageSize: 20 })` + `getKnowledgePointOptions()`
- `requirePermission(Permissions.ADAPTIVE_PRACTICE_READ)`
- 组件:`StatsGrid` / `PracticeStarterWithNav` / `PracticeHistory` / `PracticeServiceProvider`
- **ARCHITECTURE.md 契约**:❌
#### 2.14.2 ❌ `/shell/student/practice/[sessionId]`(练习会话页,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\practice\[sessionId]\page.tsx`
- **功能描述**
- 单个练习会话详情
- `PracticeSessionView` 渲染(题目作答 + 即时反馈 + 进度)
- 学生身份校验(防止跨用户访问)
- **技术栈**
- Server Component + `force-dynamic`
- Server Action`getPracticeSessionById(sessionId, ctx.userId)`
- `requirePermission(Permissions.ADAPTIVE_PRACTICE_READ)`
- 组件:`PracticeSessionView` / `PracticeServiceProvider`
- 路由参数:`sessionId`
- `notFound()` 兜底
- **ARCHITECTURE.md 契约**:❌
---
### 2.15 elective 模块2 页规划0 已完成2 缺失)
#### 2.15.1 ❌ `/shell/student/elective`(选课列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\elective\page.tsx`
- **功能描述**
- 上下两段式布局:
1. **我的选课**`MySelectionsLoader`Suspense 流式加载)
2. **可选课程**`AvailableCoursesLoader`Suspense 流式加载 + searchParams 过滤)
- 学生身份校验
- **技术栈**
- Server Component + `force-dynamic`
- `requirePermission(Permissions.ELECTIVE_READ)`
- 子组件:`MySelectionsLoader` / `AvailableCoursesLoader` / `MySelectionsSkeleton` / `AvailableCoursesSkeleton`(均在 `_components/` 下)
- `Suspense` 双段流式渲染
- searchParams 透传到 `AvailableCoursesLoader`
- **ARCHITECTURE.md 契约**:❌
#### 2.15.2 ❌ `/shell/student/elective/[id]`(选课详情,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\elective\[id]\page.tsx`
- **功能描述**
- 选修课程详情视图
- **学生视角隐私保护**:只展示课程信息,**不展示选课名单**`selections=[]``showEditButton=false`
- 通过 `ElectivePageLayout` + `ElectiveCourseDetail` 组合渲染
- **技术栈**
- Server Component + `force-dynamic`
- Server Action`getElectiveCourseById(id)`
- `requirePermission(Permissions.ELECTIVE_READ)`
- 组件:`ElectiveCourseDetail` / `ElectivePageLayout`
- 路由参数:`id`
- `notFound()` 兜底
- **ARCHITECTURE.md 契约**:❌
---
### 2.16 ai-tutor 模块1 页规划0 已完成1 缺失)
#### 2.16.1 ❌ `/shell/student/ai-tutor`AI 辅导,缺失)⚠️ B3 末
- **CICD 参考实现**:❌ CICD 无对应源页面
- **功能描述**
- 学生专属 AI 辅导对话(区别于通用 AI 聊天)
- 应基于学生学情(薄弱点、近期错题、当前进度)提供个性化辅导
- 可能的形态:聊天对话框 + 上下文卡片(学情摘要)
- **技术栈建议**:参考 CICD `learning/study-path/page.tsx``AiStudyPath` + `AiClientProvider` 模式 + `requirePermission(Permissions.AI_CHAT)`
- **ARCHITECTURE.md 契约**:❌
- **批次**B3 末
---
### 2.17 announcements 模块(共享 2 页规划0 已完成2 缺失)
#### 2.17.1 ❌ `/shell/announcements`(公告列表,缺失,共享)
- **CICD 参考实现**:❌ CICD `student/` 下无对应源页面(应在 `(dashboard)/` 顶层共享路由中,但本次未读取)
- **功能描述**:公告列表(学生可见范围)
- **ARCHITECTURE.md 契约**:❌(列表未定义)
- **批次**B3共享路由
#### 2.17.2 ❌ `/shell/announcements/[id]`(公告详情,缺失,共享)
- **CICD 参考实现**:❌ 同上
- **功能描述**:单条公告详情
- **ARCHITECTURE.md 契约**:❌
- **批次**B3共享路由
> **注**portal-shell 当前 `/shell/announcements/*` 路由不存在。需在共享层(非 student/ 下)创建。
---
### 2.18 messages 模块(共享 1 页规划0 已完成1 缺失)
#### 2.18.1 ❌ `/shell/messages`(消息中心,缺失,共享)⚠️ B3 末
- **CICD 参考实现**:❌ CICD `student/` 下无对应源页面
- **功能描述**:站内消息中心(学生收件箱)
- **ARCHITECTURE.md 契约**:❌
- **批次**B3 末,共享路由
> **注**portal-shell 当前 `/shell/messages` 路由不存在。
---
### 2.19 leave 模块1 页规划0 已完成1 缺失)
#### 2.19.1 ❌ `/shell/student/leave`(学生在线请假,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\leave\page.tsx`
- **功能描述**
- 顶部:在线请假表单(`LeaveRequestForm``defaultStudentId` + `defaultClassId` 自动填入当前活跃班级)
- 底部:本人提交的请假申请列表(`LeaveRequestList`
- 返回仪表盘的面包屑
- 无活跃班级时显示提示卡片
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getStudentActiveClass(ctx.userId)` + `getLeaveRequests({ scope, currentUserId, page: 1, pageSize: 50 })`
- `getAuthContext()`(注意:未用 `requirePermission`,直接取 ctx
- 组件:`LeaveRequestForm` / `LeaveRequestList` / `Card` / `Button`
- **scope 处理**:学生 scope 为 `class_members``buildScopeFilter` 返回 `1=0`,但通过 `currentUserId = requesterId` 过滤本人记录
- **ARCHITECTURE.md 契约**:❌
---
### 2.20 diagnostic 模块CICD 额外页面ARCHITECTURE.md 未规划)
#### 2.20.1 ⚠️ CICD `diagnostic/page.tsx`学生诊断报告ARCHITECTURE.md 未列入 36 页)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\student\diagnostic\page.tsx`
- **功能描述**
- 学生诊断学情(`getStudentMasterySummary`+ 已发布诊断报告列表(`getDiagnosticReports`
- `StudentDiagnosticView` 渲染
- 学生仅可见 `status: "published"` 报告(防草稿泄露)
- **技术栈**Server Component + `requirePermission(Permissions.DIAGNOSTIC_READ)`
- **ARCHITECTURE.md 契约**:❌ 未列入 §9.2 36 页清单
- **建议**:若产品确认学生端诊断报告功能保留,需补充到 ARCHITECTURE.md §9.2;否则可在 dashboard/weakness 中替代承载
---
## 三、按模块汇总
| 模块 | 规划页数 | 已完成 | 缺失 | 备注 |
| --------------------- | -------- | ------ | ------ | -------------------------------------------------------------- |
| dashboard | 3 | 1 | 2 | 仪表盘基础页有trend/weakness 详情页缺 |
| grades | 2 | 0 | 2 | 列表 + 报告卡 |
| exams | 3 | 0 | 3 | ⚠️ CICD 无源页面take 工作台最关键 |
| homework | 3 | 0 | 3 | ⚠️ CICD 路由为 `learning/assignments/*`,需迁移到 `homework/*` |
| schedule | 1 | 0 | 1 | |
| attendance | 1 | 0 | 1 | |
| classes | 1 | 0 | 1 | CICD 无独立路由(融合在 courses |
| courses | 2 | 0 | 2 | CICD 路由为 `learning/courses/*` |
| course-plans | 2 | 0 | 2 | |
| lesson-plans | 2 | 0 | 2 | |
| textbooks | 2 | 0 | 2 | CICD 路由为 `learning/textbooks/*`,需对齐 `/[id]/chapters` |
| error-book | 1 | 0 | 1 | 含 AI 上下文 + 客户端列表 |
| learning | 2 | 0 | 2 | 学习中心首页 + AI 学习路径 |
| practice | 2 | 0 | 2 | 自适应练习 |
| elective | 2 | 0 | 2 | 含 Suspense 双段流式渲染 |
| ai-tutor | 1 | 0 | 1 | ⚠️ B3 末CICD 无源页面 |
| announcements共享 | 2 | 0 | 2 | 共享路由,非 student/ 下 |
| messages共享 | 1 | 0 | 1 | ⚠️ B3 末,共享路由 |
| leave | 1 | 0 | 1 | |
| **合计** | **34** | **1** | **33** | 不含 notifications/settings 共享路由 |
> **加上 diagnosticCICD 额外页)共 35**ARCHITECTURE.md §9.2 规划 36 页(含 notifications/settings 共享路由)。
---
## 四、关键技术差异 & 迁移注意事项
### 4.1 架构模式差异(最重要)
| 维度 | CICD参考 | portal-shell目标 |
| -------- | -------------------------------------- | ------------------------------------------------------------------- |
| 渲染模式 | Server Component 为主 | 当前仪表盘是 Client Component需确定后续页面模式 |
| 数据获取 | Server Action + Drizzle 直查 DB | **必须改为** BFF / 微服务 API`teacher-bff` / `data-ana` 等) |
| 权限校验 | `requirePermission` + `getAuthContext` | 需通过 Gateway + BFF 鉴权,前端用 `usePermission().hasPermission()` |
| 国际化 | `next-intl` `getTranslations` | portal-shell 当前仪表盘硬编码中文,需统一策略 |
| 共享 UI | `@/shared/components/ui/*` | `@/shared/components/ui/*`(已迁移部分) |
| 模块组件 | `@/modules/<domain>/components/*` | 需在 portal-shell 重建或下沉到 BFF |
### 4.2 路由路径对齐
| CICD 路径 | ARCHITECTURE.md §9.2 目标路径 | 备注 |
| -------------------------------------------- | ----------------------------- | --------------------------- |
| `learning/assignments` | `homework` | 提升到顶层 |
| `learning/assignments/[assignmentId]` | `homework/[id]/submit` | 路径参数改名 |
| `learning/assignments/[assignmentId]/result` | `homework/[id]/analysis` | 子路径改名 |
| `learning/courses` | `courses` | 提升到顶层 |
| `learning/courses/[classId]` | `courses/[id]` | 参数名 `classId``id` |
| `learning/textbooks` | `textbooks` | 提升到顶层 |
| `learning/textbooks/[id]` | `textbooks/[id]/chapters` | 增加子路径 `chapters` |
| `learning/study-path` | `learning-path` | 提升到顶层 |
| `(无)` | `exams/*` | 全新设计 |
| `(无)` | `ai-tutor` | 全新设计 |
| `(无)` | `classes` | 全新设计(与 courses 区分) |
### 4.3 权限点映射
CICD 中学生页面用到的权限点(迁移时需在 portal-shell `Permissions` 常量中对应):
- `GRADE_RECORD_READ`grades / report-card
- `CLASS_READ`schedule / courses
- `ATTENDANCE_READ`attendance
- `COURSE_PLAN_READ`course-plans
- `LESSON_PLAN_READ`lesson-plans
- `TEXTBOOK_READ`textbooks
- `ERROR_BOOK_READ`error-book
- `AI_CHAT`study-path / ai-tutor
- `ADAPTIVE_PRACTICE_READ`practice
- `ELECTIVE_READ`elective
- `DIAGNOSTIC_READ`diagnosticCICD 额外)
- leave 用 `getAuthContext` 而非 `requirePermission`(需统一)
### 4.4 共享组件清单(需在 portal-shell 补齐或确认已有)
- `DashboardShell` / `DashboardSection` / `StatCard`(仪表盘已有)
- `Card` / `CardContent` / `CardHeader` / `CardTitle`(基础)
- `Button` / `Badge` / `EmptyState` / `Skeleton`(基础)
- `StatsGrid`多列统计error-book/practice 用)
- `WidgetBoundary`widget 降级容错)
- `StatusBadge` + `variantMap`(作业状态徽章)
- `TextbookReader` / `TextbookCard` / `TextbookFilters`(教材相关)
- `LessonPlanReadonlyView` / `LessonPlanList`(教案相关)
- `CoursePlanList` / `CoursePlanDetail`(课程计划)
- `StudentScheduleView` / `StudentScheduleFilters`(课表)
- `StudentCoursesView` / `CourseFilters`(课程)
- `StudentAttendanceView`(考勤)
- `StudentGradeSummary` / `GradeTrendCard` / `RankingTrendCard` / `GradeDistributionChart` / `GrowthArchiveChart` / `ReportCardView` / `ReportCardPrintAction` / `StudentGradeFilters`(成绩,组件最多)
- `HomeworkTakeView` / `HomeworkReviewView` / `HomeworkSubmissionResult` / `AssignmentFilters`(作业,⚠️ take 是最复杂)
- `PracticeStarterWithNav` / `PracticeHistory` / `PracticeSessionView` / `PracticeServiceProvider`(练习)
- `ElectiveCourseDetail` / `ElectivePageLayout` / `MySelectionsLoader` / `AvailableCoursesLoader`(选课)
- `LeaveRequestForm` / `LeaveRequestList`(请假)
- `StudentErrorBookListClient` / `AddErrorBookDialogWithQuestions` / `ErrorBookFilters`(错题本)
- `AiStudyPath` / `AiClientProvider` / `createCoreAiClientService`AI
### 4.5 B3 实施优先级建议
按用户价值 + 实现复杂度排序:
1. **P0必做核心学习闭环**homework 列表 + submit 工作台 + analysis、exams 列表 + take 工作台 + result
2. **P1高频查看**grades 列表 + report-card、schedule、attendance、error-book
3. **P2学习资源**learning 首页、courses 列表 + 详情、textbooks 列表 + chapters、course-plans + 详情、lesson-plans + view
4. **P3辅助功能**classes、practice + session、elective + 详情、leave
5. **P4增强**dashboard trend/weakness 详情页、learning-path
6. **P5B3 末)**ai-tutor、messages
7. **共享**announcements 列表 + 详情
---
## 五、CICD 与 ARCHITECTURE.md §9.2 对照表
| ARCH §9.2 源路由 | ARCH §9.2 目标路由 | CICD 是否有源页面 | portal-shell 是否已有 | 契约状态 | 备注 |
| --------------------------- | ------------------------------------------- | -------------------------------------- | --------------------- | --------------------- | -------------------------------- |
| `dashboard` | `/shell/student` | ✅ | ✅ | ✅ `studentDashboard` | 已完成基础版 |
| `/trend` | `/shell/student/trend` | ❌(融合在 dashboard | ❌ | ✅ `learningTrend` | 需新建详情页 |
| `/weakness` | `/shell/student/weakness` | ❌(融合在 dashboard | ❌ | ✅ `studentWeakness` | 需新建详情页 |
| `my-grades` | `/shell/student/grades` | ✅ | ❌ | ❌ | |
| `/report-card` | `/shell/student/grades/report-card` | ✅ | ❌ | ❌ | |
| `my-exams` | `/shell/student/exams` | ❌ | ❌ | 🟡 | CICD 无,全新设计 |
| `/[id]/result` | `/shell/student/exams/[id]/result` | ❌ | ❌ | 🟡 | 参考作业 result |
| `/[id]/take` | `/shell/student/exams/[id]/take` | ❌ | ❌ | 🟡 | ⚠️ 最关键,参考 HomeworkTakeView |
| `my-homework` | `/shell/student/homework` | ✅learning/assignments | ❌ | 🟡 | 路径迁移 |
| `/[id]/submit` | `/shell/student/homework/[id]/submit` | ✅learning/assignments/[id] | ❌ | 🟡 | ⚠️ take 工作台 |
| `/[id]/analysis` | `/shell/student/homework/[id]/analysis` | ✅learning/assignments/[id]/result | ❌ | 🟡 | |
| `schedule` | `/shell/student/schedule` | ✅ | ❌ | ❌ | |
| `my-attendance` | `/shell/student/attendance` | ✅ | ❌ | ❌ | |
| `my-classes` | `/shell/student/classes` | ❌(融合在 courses | ❌ | ❌ `myClasses` | 全新设计 |
| `courses` | `/shell/student/courses` | ✅learning/courses | ❌ | ❌ | 路径迁移 |
| `/[id]` | `/shell/student/courses/[id]` | ✅learning/courses/[classId] | ❌ | ❌ | 参数名 classId→id |
| `course-plans` | `/shell/student/course-plans` | ✅ | ❌ | ❌ | |
| `/[id]` | `/shell/student/course-plans/[id]` | ✅ | ❌ | ❌ | |
| `lesson-plans` | `/shell/student/lesson-plans` | ✅ | ❌ | ❌ | |
| `/[id]/view` | `/shell/student/lesson-plans/[planId]/view` | ✅ | ❌ | ❌ | |
| `textbooks` | `/shell/student/textbooks` | ✅learning/textbooks | ❌ | 🟡 | 路径迁移 |
| `/[id]/chapters` | `/shell/student/textbooks/[id]/chapters` | ✅learning/textbooks/[id] | ❌ | 🟡 | 增加子路径 |
| `error-book` | `/shell/student/error-book` | ✅ | ❌ | ✅ | 含 AI 上下文 |
| `learning` | `/shell/student/learning` | ✅ | ❌ | ❌ | |
| `learning-path` | `/shell/student/learning-path` | ✅learning/study-path | ❌ | ❌ | 路径迁移 |
| `practice` | `/shell/student/practice` | ✅ | ❌ | ❌ | |
| `/[sessionId]` | `/shell/student/practice/[sessionId]` | ✅ | ❌ | ❌ | |
| `elective` | `/shell/student/elective` | ✅ | ❌ | ❌ | |
| `/[id]` | `/shell/student/elective/[id]` | ✅ | ❌ | ❌ | |
| `ai-tutor` | `/shell/student/ai-tutor` | ❌ | ❌ | ❌ | B3 末,全新设计 |
| `announcements` | `/shell/announcements`(共享) | ❌(不在 student/ 下) | ❌ | ❌ | 共享路由 |
| `/[id]` | `/shell/announcements/[id]`(共享) | ❌ | ❌ | ❌ | 共享路由 |
| `messages` | `/shell/messages`(共享) | ❌ | ❌ | ❌ | B3 末,共享路由 |
| `leave` | `/shell/student/leave` | ✅ | ❌ | ❌ | |
| `notifications``settings` | 共享路由 | — | ❌ | ✅ | B1/B3不在 36 页内 |
| `diagnostic`CICD 额外) | —(未列入 §9.2 | ✅ | ❌ | — | 需产品决策是否保留 |
---
## 六、风险与建议
### 6.1 高风险点
1. **exams/[id]/take 作答工作台**CICD 完全无源可参考(仅有 `HomeworkTakeView` 同构参考),需从零设计考试特性(倒计时、断网恢复、批量提交、防作弊等)
2. **路由路径大调整**6 类路径需迁移learning/* → 顶层),需同步更新导航、面包屑、内链
3. **架构模式转变**portal-shell 已转向 BFF + 微服务,**不能照搬 CICD 的 Server Action + Drizzle 直查模式**每个页面都需重新设计数据获取链路BFF → data-ana / core-edu 等服务)
4. **国际化缺失**当前仪表盘硬编码中文36 页全部完成后需补 i18n建议从一开始就用 `next-intl`
5. **权限校验缺失**:当前仪表盘无 `requirePermission`,需建立 portal-shell 学生域权限校验中间件/装饰器
### 6.2 建议
1. **先建契约**:开工前先在 `shared-proto` 中补齐 §9.2 标 ❌ / 🟡 的契约(特别是 exams 系列、my-classes、homework 等)
2. **统一渲染模式**:建议学生页面统一采用 Server Component + BFF 数据获取(与 CICD 一致),避免当前仪表盘的 Client Component 模式
3. **统一权限模型**:建立 portal-shell 学生域 `RequirePermission` 装饰器/中间件,覆盖所有页面
4. **共享组件下沉**CICD 中 `@/modules/<domain>/components/*` 的视图组件需评估是下沉到 BFF 返回结构化数据,还是在 portal-shell 重建
5. **CICD diagnostic 决策**:产品需确认是否保留学生端诊断报告功能,若保留需补充 ARCHITECTURE.md §9.2
6. **分批推进**:按本报告 §4.5 的 P0→P5 优先级分批实施,每批完成后跑 `pnpm run arch:scan` 同步 arch.db
---
**分析完成。共发现 35 个待完成页面(含 CICD 额外的 diagnostic 1 页),其中 33 个完全缺失、2 个为 dashboard 详情页缺失。最关键的 exams/[id]/take 作答工作台在 CICD 中无源页面,需重点设计。**