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,332 @@
# 年级组域Management/Grade待完成功能分析
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\management\grade\`5 个 page.tsx
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\`**无对应页面,全部缺失**
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.5 缺口新增5 页B2/B5 末 N
> 分析日期2026-07-24
> 分析方式:逐页面读取 CICD page.tsx 源码 + 检查 portal-shell 对应路由是否存在
---
## 〇、方法论与对比基线
1. CICD 单体Next.js App Router + Server Actions + Drizzle作为**功能基线**,反映"老版单体已实现"的年级组功能完整态。
2. portal-shell 作为**目标态**,遵循 ARCH §9.5 的"缺口新增"规划B2/B5 末N=新建)。
3. CICD 的 5 个页面位于 `(dashboard)/management/grade/` 顶级路由组下,**不属于 /teacher 也不属于 /admin**,是独立的"年级组管理"维度,按 `GRADE_MANAGE` / `GRADE_RECORD_READ` / `ADAPTIVE_PRACTICE_READ` 三类权限放行。
4. CICD 的 `navigation.ts` 显示该菜单组同时出现在 `teacher``grade_head` 两个角色的导航中permission 均为 `Permissions.GRADE_MANAGE`),即"年级组长"既可以是带 `grade_head` 角色的独立账号,也可以是普通教师被授予 `GRADE_MANAGE` 权限。
5. portal-shell 现有目录结构仅含 `/shell/{teacher|admin|student|parent|dev|forbidden}/*`**未规划 `/shell/management/*` 独立段**,且 `route-permissions.ts` 中无任何 `management/grade` 条目。
6. ARCH §9.5 表格仅以"缺口新增"一行汇总 10 页5 grade + 5 register/onboarding/privacy/terms未细化每页的目标路由与契约本文档补充细化。
---
## 一、页面完成度总览
| 状态 | 数量 | 说明 |
| ------- | ---- | -------------------------------------- |
| ❌ 缺失 | 5 | 5 个页面在 portal-shell 中均无对应实现 |
| 🟡 部分 | 0 | — |
| ✅ 完成 | 0 | — |
> portal-shell 路径检查:`apps/portal-shell/src/app/shell/**/management/**` 与 `apps/portal-shell/src/app/shell/**/grade/**` Glob 均返回 "No file found"。`route-permissions.ts` 亦无 `management` / `grade` 关键字命中。
---
## 二、缺失页面清单
### 2.1 年级组入口重定向page.tsx
- **CICD 路径**`e:\desktop\CICD\src\app\(dashboard)\management\grade\page.tsx`
- **建议目标路由**`/shell/teacher/management/grade`(见 §三角色归属分析)
- **功能描述**:空页面入口,仅做权限校验与重定向:
- `await requirePermission(Permissions.GRADE_MANAGE)` — 校验年级管理权限
- `redirect("/management/grade/classes")` — 重定向到班级列表页
- **技术栈**
- Next.js `redirect` (from `next/navigation`)
- `requirePermission` 共享鉴权工具(`@/shared/lib/auth-guard`
- `Permissions.GRADE_MANAGE` 权限位(`grade:manage`
- `export const dynamic = "force-dynamic"` — 强制动态渲染(避免静态缓存鉴权信息)
- **权限**`GRADE_MANAGE``grade:manage`)— 年级组长专属权限
- **ARCHITECTURE.md 契约**§9.5 缺口新增,无 GraphQL schema
- **批次**B2/B5 末N
### 2.2 年级组仪表盘dashboard
- **CICD 路径**`e:\desktop\CICD\src\app\(dashboard)\management\grade\dashboard\page.tsx`
- **建议目标路由**`/shell/teacher/management/grade/dashboard`
- **功能描述**:年级组长仪表盘,包含 4 个 Tab 切换的概览面板:
- **年级筛选器**FilterBar + ChipNav拉取 `getGradesForStaff(teacherId)` 得到该教师可见年级列表URL `?gradeId=xxx` 即时切换,无整页刷新
- **Tab distribution成绩分布**:调用 `getGradeDistributionByGradeId({ gradeId, scope: ctx.dataScope })`,渲染 `GradeDistributionPanel`
- **Tab homework作业洞察**:调用 `getGradeHomeworkInsights({ gradeId, limit: 50 })`,渲染 `GradeHomeworkPanel`
- **Tab exams考试列表**:调用 `getExamsByGradeId({ gradeId, scope: ctx.dataScope })`,渲染 `GradeExamsPanel`
- **Tab progress课程计划进度**:调用 `getGradeCoursePlanProgress({ gradeId })`,渲染 `GradeProgressPanel`
- **懒加载策略**:仅渲染当前激活 Tab 的数据(按 `tab` 参数选择性 fetch其他 Tab 不预取
- **空状态**:无可见年级 / 未选年级 / 数据为 null 三种 EmptyState 兜底
- **错误边界**`SectionErrorBoundary` 包裹面板区域namespace="school"
- **技术栈**
- Server Component`force-dynamic`+ `generateMetadata` 动态标题i18n `school.grades.gradeDashboard.*`
- `next-intl/server``getTranslations`
- `getParam` / `SearchParams` 工具(`@/shared/lib/search-params`
- 跨模块数据访问:`classes` / `school` / `grades` / `exams` / `course-plans` 5 个模块的 `data-access`
- 共享 UI`FilterBar``ChipNav``EmptyState``SectionErrorBoundary`
- 业务组件:`modules/school/components/grade-dashboard/grade-{distribution,homework,exams,progress}-panel`
- DataScope 二次过滤:`ctx.dataScope` 传入查询,按教师可见范围过滤
- **权限**`GRADE_RECORD_READ``grade_record:read`)— 比入口权限宽松,允许只读查看成绩的教师访问
- **ARCHITECTURE.md 契约**5 个 data-access 全部走 Drizzle 直查 DB未走 GraphQL schema
- **批次**B2/B5 末N
### 2.3 年级组班级列表classes
- **CICD 路径**`e:\desktop\CICD\src\app\(dashboard)\management\grade\classes\page.tsx`
- **建议目标路由**`/shell/teacher/management/grade/classes`
- **功能描述**:年级组长管辖的班级管理页,并行拉取三组数据:
- `getGradeManagedClasses(userId)` — 该用户作为年级组长管辖的班级列表
- `getTeacherOptions()` — 教师下拉选项(用于班主任分配)
- `getManagedGrades(userId)` — 该用户管辖的年级列表
- 渲染 `GradeClassesClient` 客户端组件,承载班级 CRUD建/删/改)交互
- **技术栈**
- Server Component`force-dynamic`+ `generateMetadata`i18n `school.classManagement.grade.*`
- `Promise.all` 并行数据获取
- 客户端组件 `GradeClassesClient``modules/classes/components/grade-classes-view.tsx`
- 客户端组件依赖:
- `createGradeClassAction` / `deleteGradeClassAction` / `updateGradeClassAction`Server Actions`modules/classes/actions.ts`
- `useClassData` Hook`modules/classes/hooks/use-class-data.ts`
- `ClassDeleteDialog` / `ClassFormDialog` / `ClassListTable` / `ClassListToolbar`4 个子组件)
- `ctx.userId` 用于 owner 维度过滤
- **权限**`GRADE_MANAGE``grade:manage`)— 年级组长写权限
- **ARCHITECTURE.md 契约**5 个 data-access 全走 Drizzle无 GraphQL
- **批次**B2/B5 末N
- **迁移注意**portal-shell 现有 `/shell/teacher/classes` 是普通教师班级列表(教师授课班级),与年级组长管辖的班级列表**语义不同**(前者按 `teacherId` 关联 `class_teachers`,后者按年级组管辖范围 `grade_head`)。**不可合并**。
### 2.4 年级组作业洞察insights
- **CICD 路径**`e:\desktop\CICD\src\app\(dashboard)\management\grade\insights\page.tsx`
- **建议目标路由**`/shell/teacher/management/grade/insights`
- **功能描述**:年级组作业洞察页,单年级作业统计与班级排名:
- **年级筛选器**FilterBar + ChipNav`getGradesForStaff(teacherId)` + `?gradeId=xxx` 切换
- **4 个统计卡StatCard**
- 班级数(`insights.classCount`
- 学生数(`studentCounts.total` / `active` / `inactive`
- 整体平均分(`overallScores.avg`
- 最近作业平均分(`insights.latest.scoreStats.avg` + 作业标题)
- **作业时间线表Table**:每个作业一行,列含:作业名 / 状态 / 创建时间 / 应交 / 已交 / 已批 / 平均分 / 中位数分
- **班级排名表Table**:每个班一行,列含:班级名(含班主任)/ 学生数 / 最近作业平均 / 上次平均 / 涨跌 / 整体平均
- **三段空状态**:无年级 / 未选 / 数据为空 / 无 insights 各自 EmptyState
- **移动端适配**`overflow-x-auto` 包裹两个 Tablev4-P1-11
- **技术栈**
- Server Component`force-dynamic`+ `generateMetadata`i18n `school.grades.gradeInsights.*`
- `formatDate` / `formatNumber` 工具(`@/shared/lib/utils`
- 共享 UI`FilterBar` / `ChipNav` / `EmptyState` / `StatCard` / `Card` / `Badge` / `Table` 全套
- 数据访问:`getGradeHomeworkInsights({ gradeId, limit: 50 })``modules/classes/data-access`limit=50
- 辅助查询:`getTeacherIdForMutations` + `getGradesForStaff`
- 图标:`BarChart3`lucide-react
- **权限**`GRADE_RECORD_READ``grade_record:read`)— 只读权限
- **ARCHITECTURE.md 契约**Drizzle 直查,无 GraphQL
- **批次**B2/B5 末N
### 2.5 年级组练习概览practice
- **CICD 路径**`e:\desktop\CICD\src\app\(dashboard)\management\grade\practice\page.tsx`
- **建议目标路由**`/shell/teacher/management/grade/practice`
- **功能描述**:年级组自适应练习概览页,三段式数据可视化:
- **年级筛选器**FilterBar + ChipNav`getGradesForStaff(teacherId)` + `?gradeId=xxx` 切换
- **5 个统计卡StatsGrid**
- 班级总数(`overview.totalClasses`BookOpen 图标)
- 练习会话总数(`overview.totalSessions`Activity 图标)
- 答题总数(`overview.totalQuestionsAnswered`Target 图标)
- 平均正确率(`overview.averageAccuracy * 100`%CheckCircle2 图标)
- 参与率(`overview.participationRate * 100`%Users 图标)
- **班级练习对比表**`ClassPracticeComparisonTable``modules/adaptive-practice/components`)渲染 `classComparison`
- **练习类型分布图**`PracticeTypeBreakdownChart`(同模块)渲染 `typeBreakdown`
- **懒加载**:仅 `selected` 存在时执行 `Promise.all` 并行 3 查询
- **三段空状态**:无年级 / 未选 / 数据为 null 各自 EmptyState
- **技术栈**
- Server Component`force-dynamic`+ `generateMetadata`i18n `practice.grade.*`
- 双 i18n namespace`practice`(主)+ `school`(年级筛选器复用 `grades.gradeInsights.selectGrade`
- 数据访问:`modules/adaptive-practice/data-access-analytics` 的 3 个查询:
- `getGradePracticeOverview(gradeId)`
- `getGradeClassPracticeComparison(gradeId)`
- `getPracticeTypeBreakdown(studentIds)` — 需要先调 `getUserIdsByGradeId(gradeId)` 取学生 ID 列表
- 共享 UI`StatsGrid`5 列)、`FilterBar``ChipNav``EmptyState`
- 业务组件:`ClassPracticeComparisonTable` + `PracticeTypeBreakdownChart`
- 6 个 lucide-react 图标:`Activity` / `BarChart3` / `BookOpen` / `CheckCircle2` / `Target` / `Users`
- **权限**`ADAPTIVE_PRACTICE_READ``adaptive_practice:read`)— 自适应练习读权限
- **ARCHITECTURE.md 契约**Drizzle 直查,无 GraphQL
- **批次**B2/B5 末N
- **迁移注意**CICD 的 `navigation.ts``teacher.gradeManagement` 菜单组**只列了 classes/dashboard/insights 3 项**,未列 practice 子菜单。但 page.tsx 实际存在,应作为隐藏/直接 URL 访问入口保留。
---
## 三、角色归属分析
### 3.1 CICD 中的权限校验
| 页面 | requirePermission | 权限语义 |
| --------------- | ------------------------------------ | ---------------- |
| page.tsx (入口) | `Permissions.GRADE_MANAGE` | 年级组管理(写) |
| classes | `Permissions.GRADE_MANAGE` | 年级组管理(写) |
| dashboard | `Permissions.GRADE_RECORD_READ` | 成绩记录读 |
| insights | `Permissions.GRADE_RECORD_READ` | 成绩记录读 |
| practice | `Permissions.ADAPTIVE_PRACTICE_READ` | 自适应练习读 |
> 5 个页面共用 3 类权限,无 `admin:*` 权限校验。**年级组页面是"年级组长"维度,不属于"管理员"维度**。
### 3.2 CICD 导航归属
`e:\desktop\CICD\src\modules\layout\config\navigation.ts` 中:
- `teacher` 角色菜单组:包含 `teacher.gradeManagement`permission=`GRADE_MANAGE`),子项含 classes/dashboard/insights
- `grade_head` 角色菜单组:同样包含 `teacher.gradeManagement`,子项相同
- 两个角色都把年级组放在**与"teacher.grades"(成绩录入)平级**的位置,不在 `admin.*` 之下
### 3.3 portal-shell 角色与目录约定
- `apps\portal-shell\ARCHITECTURE.md` §9.1 教师域规划到 `/shell/teacher/*`§9.4 管理域规划到 `/shell/admin/*`
- portal-shell 现有目录仅 `/shell/{teacher|admin|student|parent|dev|forbidden}/*`,无独立 `grade-head`
- `src/middleware.ts` 默认合成身份为 `role: "teacher"`,未对 `grade_head` 角色做特殊路由分发
- `src/shared/lib/route-permissions.ts` 中无 `management` / `grade` 路由条目(需新增)
### 3.4 建议归属
**推荐方案:归入教师域子路径** `/shell/teacher/management/grade/*`
理由:
1. CICD 在 `teacher``grade_head` 两角色菜单中都把"年级组管理"挂在教师主菜单下,语义上是"教师的扩展职责"
2. portal-shell 已有 `/shell/teacher/*` 体系复用其布局壳、middleware 身份合成、route-permissions 表
3. 避免新建 `/shell/grade-head/*` 顶层段(会带来 sidebar 配置、middleware 分发、layout 重复实现等成本)
4. 路由权限按 `GRADE_MANAGE` / `GRADE_RECORD_READ` / `ADAPTIVE_PRACTICE_READ` 精确声明,与角色解耦——拥有这些权限位的教师(无论 role 字段是 `teacher` 还是 `grade_head`)都可访问
**备选方案**:若后续 portal-shell 引入 `grade_head` 独立角色段,可平滑迁移到 `/shell/grade-head/*`page.tsx 实现完全可复用,仅需调整路由前缀)。
**目标路由建议**
| CICD 源路径 | 建议目标路由 | 权限 |
| ----------------------------- | ------------------------------------------- | ------------------------ |
| `/management/grade` | `/shell/teacher/management/grade` | `GRADE_MANAGE` |
| `/management/grade/classes` | `/shell/teacher/management/grade/classes` | `GRADE_MANAGE` |
| `/management/grade/dashboard` | `/shell/teacher/management/grade/dashboard` | `GRADE_RECORD_READ` |
| `/management/grade/insights` | `/shell/teacher/management/grade/insights` | `GRADE_RECORD_READ` |
| `/management/grade/practice` | `/shell/teacher/management/grade/practice` | `ADAPTIVE_PRACTICE_READ` |
---
## 四、迁移注意事项
### 4.1 契约缺口5 页全部 ❌)
5 个页面的数据访问在 CICD 中**全部走 Drizzle ORM 直查 DB**,未通过 GraphQL schema。迁移到 portal-shell 时按 ARCH §9.5 "节奏原则"处理:
- **MSW 先行**:用 Mock Service Worker 提供假数据,先把 5 个页面的 UI 跑起来
- **契约工单**:向后端开 5 类查询的 GraphQL schema 工单
- `gradeManagedClasses(userId)` + `managedGrades(userId)` + `teacherOptions()`
- `gradesForStaff(teacherId)`(年级列表)
- `gradeDistributionByGradeId(gradeId, scope)` + `gradeHomeworkInsights(gradeId, limit)` + `examsByGradeId(gradeId, scope)` + `gradeCoursePlanProgress(gradeId)`
- `gradePracticeOverview(gradeId)` + `gradeClassPracticeComparison(gradeId)` + `practiceTypeBreakdown(studentIds)` + `userIdsByGradeId(gradeId)`
- **切换策略**:契约就绪后,只改 hook 的 fetcher 指向(从 MSW 切到 Apollo Client页面不动
### 4.2 跨模块依赖5 个领域模块)
年级组页面是**跨模块聚合页**,依赖 5 个领域模块的 data-access
| 依赖模块 | 用到的查询 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `classes` | `getGradeManagedClasses` / `getManagedGrades` / `getTeacherIdForMutations` / `getGradeHomeworkInsights` / `getTeacherOptions` |
| `school` | `getGradesForStaff` |
| `grades` | `getGradeDistributionByGradeId` |
| `exams` | `getExamsByGradeId` |
| `course-plans` | `getGradeCoursePlanProgress` |
| `adaptive-practice` | `getGradePracticeOverview` / `getGradeClassPracticeComparison` / `getPracticeTypeBreakdown` |
| `users` | `getUserIdsByGradeId` |
迁移时需确保 portal-shell 对应的 7 个领域模块契约齐备或 MSW 兜底覆盖。
### 4.3 DataScope 二次过滤
CICD 的 `requirePermission` 返回 `ctx.dataScope`,传入 `getGradeDistributionByGradeId``getExamsByGradeId` 进行**数据范围二次过滤**(按教师可见的年级/班级范围裁剪结果)。
portal-shell 中需在 hook 层保留 dataScope 参数,避免越权读取非管辖年级数据。
### 4.4 i18n 命名空间
5 个页面用到 3 个 i18n namespace
- `school`dashboard / classes / insights 主命名空间)
- `practice`practice 页主命名空间)
- `school.grades.gradeInsights.selectGrade`practice 页复用)
迁移时需将 CICD 的 `messages/zh-CN/school.json` / `practice.json` 中相关键同步到 portal-shell 的 i18n 资源。
### 4.5 业务组件迁移
以下业务组件需要从 CICD 迁移到 portal-shell按页面分组
| 页面 | 组件 | 来源模块 |
| --------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| dashboard | `GradeDistributionPanel` / `GradeHomeworkPanel` / `GradeExamsPanel` / `GradeProgressPanel` | `modules/school/components/grade-dashboard` |
| classes | `GradeClassesClient` + 4 个子组件(`ClassDeleteDialog` / `ClassFormDialog` / `ClassListTable` / `ClassListToolbar` | `modules/classes/components` |
| practice | `ClassPracticeComparisonTable` / `PracticeTypeBreakdownChart` | `modules/adaptive-practice/components` |
| insights | 仅用共享 UIStatCard / Card / Table无业务组件 | — |
### 4.6 route-permissions.ts 同步
迁移完成后需在 `apps/portal-shell/src/shared/lib/route-permissions.ts` 新增 5 条路由权限声明:
```ts
// 示例(实际实现时按真实 schema 编写)
"/shell/teacher/management/grade": { permission: "grade:manage", ... },
"/shell/teacher/management/grade/classes": { permission: "grade:manage", ... },
"/shell/teacher/management/grade/dashboard": { permission: "grade_record:read", ... },
"/shell/teacher/management/grade/insights": { permission: "grade_record:read", ... },
"/shell/teacher/management/grade/practice": { permission: "adaptive_practice:read", ... },
```
### 4.7 ARCHITECTURE.md 与 arch.db 同步
`e:\Desktop\Edu\.trae\rules\project_rules.md` §1.3「改码必同步图」要求:
- 完成 5 页迁移后运行 `pnpm run arch:scan` 更新 arch.db
-`apps/portal-shell/ARCHITECTURE.md` §9.5 表格中将"缺口新增"行的"契约已就绪"列从 `0` 更新为实际就绪数(按契约工单进度)
-`docs/troubleshooting/known-issues.md` 对应模块分区追加"场景→技术"映射(索引式一行,不标 AI 身份)
### 4.8 批次与顺序
按 ARCH §9.5 规划5 页归入 **B2/B5 末** 批次:
- **B2 末**教师域二期与教师主功能一起补齐classes / dashboard / insights — 因 CICD navigation 中挂在 `teacher.gradeManagement` 菜单下
- **B5 末**管理域与管理员功能一起补齐practice — 因 CICD navigation 未列 practice 子项,按独立入口处理
**推荐实施顺序**(依赖从轻到重):
1. `page.tsx`入口重定向最简单0 数据访问)
2. `insights`(仅查 1 个 data-access + 共享 UI
3. `classes`3 个 data-access + 1 客户端组件 + 4 子组件)
4. `dashboard`4 个 data-access + 4 业务面板组件)
5. `practice`3 个 data-access + 2 业务组件 + 1 辅助查询)
---
## 五、附录CICD 文件清单
```
e:\desktop\CICD\src\app\(dashboard)\management\grade\
├─ page.tsx # 入口重定向 → /classes
├─ error.tsx # 错误边界
├─ loading.tsx # 加载骨架
├─ classes\
│ ├─ page.tsx # 年级组班级列表
│ ├─ error.tsx
│ └─ loading.tsx
├─ dashboard\
│ ├─ page.tsx # 年级组仪表盘4 Tab
│ └─ loading.tsx
├─ insights\
│ ├─ page.tsx # 年级组作业洞察
│ ├─ error.tsx
│ └─ loading.tsx
└─ practice\
├─ page.tsx # 年级组练习概览
├─ error.tsx
└─ loading.tsx
```
> portal-shell 已有统一的 `loading.tsx` / `error.tsx` 模板(见 `apps/portal-shell/src/app/shell/teacher/*` 各目录),迁移时复用模板即可,无需单独迁 error/loading。