Files
Edu/apps/portal-shell/docs/needtodo/management-grade-NeedTodo.md
SpecialX 062d9e9582 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
2026-07-24 23:07:20 +08:00

333 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 年级组域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。