# 年级组域(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` 包裹两个 Table(v4-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 | 仅用共享 UI(StatCard / 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。