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,582 @@
# 管理域Admin待完成功能分析
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\admin\`41 个 page.tsx
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\admin\`1 个 page.tsx
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.4(管理域 24 页B5 批次)
> 分析日期2026-07-24
> 分析方式:逐页面对比 CICD 实现 vs portal-shell 实现,逐个读取 CICD page.tsx + ARCH §9.4 契约
---
## 〇、方法论与对比基线
1. CICD 单体Next.js App Router + Server Actions + Drizzle作为**功能基线**,反映"老版单体已实现"的管理员功能完整态。
2. portal-shell 作为**目标态**,遵循 ARCH §9.4 的契约与批次规划B524 页)。
3. CICD admin/layout.tsx 仅为 `<>{children}</>` passthrough无独立侧边栏/权限逻辑(权限由各 page.tsx 调 `requirePermission` 完成)。
4. ARCH §9.4 规划 24 页 = 1 仪表盘 + 23 管理子页portal-shell 仅完成 `/shell/admin/page.tsx`(仪表盘),其余 23 页全部缺失。
5. CICD 中存在但 ARCH §9.4 **未规划**的页面course-plans/curriculum-map/elective/questions/lesson-plans/error-book/scheduling/attendance 等 21 页CICD 历史已实现portal-shell 缺失但**不违反 ARCH §9.4**;这些页面是教师域在管理员视角的"全局只读 + 跨班聚合"版本,可作为 B5 后期/补充批次候选。
6. ARCH §9.4 中存在但 CICD **无对应**的页面4 页):`students``teachers``organization``viewports``plugins`,属 ARCH 新规划,需在 portal-shell 中独立设计。
---
## 一、页面完成度总览
| 状态 | 数量 | 说明 |
| --------------------------------- | ---- | ---------------------------------------------------------------------------------------------- |
| ✅ 已完成且功能完整 | 1 | `/shell/admin/page.tsx`仪表盘4 StatCard + 预警 + AI 用量,已接 `adminDashboard` 真实契约) |
| ❌ 完全缺失ARCH §9.4 规划内) | 23 | 见 §二,按 ARCH 规划口径的 23 个缺失页面 |
| CICD 已实现但 ARCH §9.4 未规划 | 21 | 见 §五,作为补充批次参考 |
| 🆕 ARCH §9.4 规划但 CICD 无源 | 4 | `students``teachers``organization``viewports`+ `plugins` 已有契约 ✅) |
### ✅ 已完成且功能完整1 页)
| 模块 | 页面 | 备注 |
| --------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| dashboard | `/shell/admin/page.tsx` | 客户端组件,`useAdminDashboard` hook 接 data-ana `adminDashboard` 真实契约4 StatCard教师/学生/班级/全校平均分)+ 近期预警 + AI 用量;含 loading/error 兜底;引用 ARCH §7.1 / §10 P1-2 |
---
## 二、缺失页面清单(按 ARCH §9.4 规划口径23 页)
### 2.1 dashboard 模块(无缺口)
`/shell` 已落地,对应 ARCH §9.4 第 1 行 `admin/dashboard``/shell`(动作为 R契约 ✅ `adminDashboard`)。
### 2.2 users 模块2 页缺失)
#### 2.2.1 `/shell/admin/users`(用户管理列表)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\users\page.tsx`
- **功能描述**:用户列表 + 角色 assignment 入口
- URL 参数:`page``search``role`(搜索/角色筛选 + 分页)
- 并行查询:`getAdminUsers({ page, search, role })` + `getAdminUserRoles()` + `getRoles()`
- 渲染 `AdminUsersView` 客户端组件,传入 users / roleOptions / 分页信息 / assignableRoles
- **技术栈**Server Component + `requirePermission(Permissions.USER_MANAGE)` + `force-dynamic` + `getSearchParam` 工具 + Drizzle data-access
- **ARCHITECTURE.md 契约**:❌ `users` 列表B5未就绪
- **CICD 关键代码片段**
```ts
await requirePermission(Permissions.USER_MANAGE);
const [result, roleOptions, roles] = await Promise.all([
getAdminUsers({ page, search, role }),
getAdminUserRoles(),
getRoles(),
]);
```
#### 2.2.2 `/shell/admin/users/import`(用户批量导入)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\users\import\page.tsx`
- **功能描述**:用户批量导入页面,包含
- 顶部 `UserImportDialog`(导入弹窗,触发上传 + Server Action 解析)
- 操作指引卡片4 步流程)
- 注意事项卡片6 条规则)
- 模板字段说明表5 列name/email/role/phone/inviteCode标注是否必填
- **技术栈**Server Component + `requirePermission(Permissions.USER_MANAGE)` + UI 基础组件Card / Table / Button+ next-intl
- **ARCHITECTURE.md 契约**:❌(未单独列,归 `users` 工单)
- **CICD 关键代码片段**:通过 `<UserImportDialog />` 客户端组件触发导入流程,模板字段固定 5 列。
### 2.3 roles + permissions 模块2 页缺失)
#### 2.3.1 `/shell/admin/roles`(角色管理列表)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\roles\page.tsx`
- **功能描述**:角色列表 + 创建/编辑入口
- 查询 `getRoles()` 拉取所有角色
- 渲染 `RoleManagementView` 客户端组件(含创建按钮、角色卡片/列表)
- **技术栈**Server Component + `requirePermission(Permissions.ROLE_READ)` + Drizzle `rbac/data-access`
- **ARCHITECTURE.md 契约**B5
- **CICD 关键代码片段**
```ts
await requirePermission(Permissions.ROLE_READ)
const roles = await getRoles()
return <RoleManagementView roles={roles} />
```
#### 2.3.2 `/shell/admin/permissions`(权限目录)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\permissions\page.tsx`
- **功能描述**:权限目录页(系统所有权限点列表,按模块分组)
- 查询 `getPermissionRoleCounts()` 获取每个权限点关联的角色数
- 渲染 `PermissionCatalogView` 客户端组件
- **技术栈**Server Component + `requirePermission(Permissions.PERMISSION_READ)` + Drizzle `rbac/data-access-permissions`
- **ARCHITECTURE.md 契约**B5
- **CICD 关键代码片段**
```ts
await requirePermission(Permissions.PERMISSION_READ)
const roleCountsByPermission = await getPermissionRoleCounts()
return <PermissionCatalogView roleCountsByPermission={roleCountsByPermission} />
```
> **附注**CICD 另有 `admin/roles/[id]/page.tsx`(角色详情,含 `RolePermissionMatrix` 权限矩阵 + `RoleDetailEditButton`),但 ARCH §9.4 仅规划 `/shell/admin/roles` 一页,未列详情子页。可作 B5 后期补强。
### 2.4 audit-logs 模块4 页缺失)
#### 2.4.1 `/shell/admin/audit-logs`(审计日志列表 + 导出)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\audit-logs\page.tsx`
- **功能描述**:审计日志列表 + 多维筛选 + CSV 导出
- URL 参数:`page`、`module`、`action`、`status`、`userId`、`startDate`、`endDate`
- 并行查询:`getAuditLogs({...})` + `getAuditModuleOptions()`
- `AuditLogExportButton` 触发 CSV 导出(按筛选条件)
- `SectionErrorBoundary` 错误兜底
- **技术栈**Server Component + `requirePermission(Permissions.AUDIT_LOG_READ)` + Drizzle `audit/data-access` + 类型守卫 `isValidAuditLogStatus`
- **ARCHITECTURE.md 契约**B5
- **CICD 关键代码片段**
```ts
const [result, moduleOptions] = await Promise.all([
getAuditLogs({ page, module, action, status, userId, startDate, endDate }),
getAuditModuleOptions(),
]);
```
#### 2.4.2 `/shell/admin/audit-logs/overview`(审计概览)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\audit-logs\overview\page.tsx`
- **功能描述**:审计概览页(统计/趋势/分布三件套)
- 并行查询:`getAuditOverviewStats()` + `getAuditTrend(7)` + `getDataChangeActionStats()`
- `AuditServiceProvider` 包裹 `adminAuditService` 注入服务上下文
- 渲染 `AuditOverviewView` 客户端组件(统计卡 + 7 天趋势图 + 动作分布饼图)
- **技术栈**Server Component + `requirePermission(Permissions.AUDIT_LOG_READ)` + 服务注入模式
- **ARCHITECTURE.md 契约**B5
- **CICD 关键代码片段**
```ts
const [stats, trend, distribution] = await Promise.all([
getAuditOverviewStats(),
getAuditTrend(7),
getDataChangeActionStats(),
]);
```
#### 2.4.3 `/shell/admin/audit-logs/login-logs`(登录日志)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\audit-logs\login-logs\page.tsx`
- **功能描述**:登录日志列表 + 筛选 + 导出
- URL 参数:`page`、`action`signin/signout/signup、`status`success/failure、`userId`、`startDate`、`endDate`
- 查询 `getLoginLogs({...})`
- `AuditLogExportButton exportType="login"` 触发导出
- 渲染 `LoginLogView` 客户端组件
- **技术栈**Server Component + `requirePermission(Permissions.AUDIT_LOG_READ)` + 类型守卫 `isValidLoginLogAction` / `isValidLoginLogStatus`
- **ARCHITECTURE.md 契约**B5
#### 2.4.4 `/shell/admin/audit-logs/data-changes`(数据变更日志)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\audit-logs\data-changes\page.tsx`
- **功能描述**:数据变更日志列表 + 筛选 + 导出 + 统计
- URL 参数:`page`、`tableName`、`action`create/update/delete、`userId`、`startDate`、`endDate`
- 并行查询:`getDataChangeLogs({...})` + `getDataChangeTableOptions()` + `getDataChangeStats()`
- 渲染 `DataChangeLogView` 客户端组件(含表选项下拉 + 变更统计卡)
- **技术栈**Server Component + `requirePermission(Permissions.AUDIT_LOG_READ)` + 类型守卫 `isValidDataChangeAction`
- **ARCHITECTURE.md 契约**B5
### 2.5 invitation-codes 模块1 页缺失)
#### 2.5.1 `/shell/admin/invitation-codes`(邀请码管理)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\invitation-codes\page.tsx`
- **功能描述**:邀请码列表 + 生成 + 删除
- SSR 查询 `listInvitationCodes(200, true)`200 条 + 含已用)
- 服务端计算 `now = new Date().getTime()` 传给客户端(避免 Client 端 render 阶段调用 `Date.now()`,符合 react-hooks/purity 规则)
- 生成/删除通过 Server Action + `router.refresh()` 触发更新
- 渲染 `InvitationCodesView` 客户端组件
- **技术栈**Server Component + `requirePermission(Permissions.USER_MANAGE)` + Drizzle `invitation-codes/data-access`
- **ARCHITECTURE.md 契约**B5
- **CICD 关键代码片段**
```ts
const codes = await listInvitationCodes(200, true)
const now = new Date().getTime()
return <InvitationCodesView initialCodes={codes} now={now} />
```
### 2.6 school 模块6 页缺失)
#### 2.6.1 `/shell/admin/school`学校首页redirect
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\school\page.tsx`
- **功能描述**:纯重定向页 `redirect("/admin/school/schools")`,无 UI
- **技术栈**`redirect()` from `next/navigation`
- **ARCHITECTURE.md 契约**B5
#### 2.6.2 `/shell/admin/school/schools`(学校列表)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\school\schools\page.tsx`
- **功能描述**:学校列表 + CRUD
- 查询 `getSchools()`
- 渲染 `SchoolsClient` 客户端组件
- `SectionErrorBoundary` 错误兜底
- **技术栈**Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle `school/data-access`
- **ARCHITECTURE.md 契约**B5
#### 2.6.3 `/shell/admin/school/classes`(班级管理)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\school\classes\page.tsx`
- **功能描述**:班级管理 + 跨数据聚合
- 并行查询:`getAdminClasses()` + `getTeacherOptions()` + `getSchools()` + `getGrades()`
- 渲染 `AdminClassesClient` 客户端组件(含班级 CRUD + 教师/学校/年级下拉)
- **技术栈**Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle `classes/data-access` + `school/data-access`
- **ARCHITECTURE.md 契约**B5
- **CICD 关键代码片段**
```ts
const [classes, teachers, schools, grades] = await Promise.all([
getAdminClasses(),
getTeacherOptions(),
getSchools(),
getGrades(),
]);
```
> **注**ARCH §9.4 第 7 行将 `admin/classes` 单列 1 页M与 `school/classes` 实际同源,建议合并到 `/shell/admin/school/classes`。
#### 2.6.4 `/shell/admin/school/departments`(部门管理)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\school\departments\page.tsx`
- **功能描述**:部门列表 + CRUD
- 查询 `getDepartments()`
- 渲染 `DepartmentsClient` 客户端组件
- **技术栈**Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle
- **ARCHITECTURE.md 契约**B5
#### 2.6.5 `/shell/admin/school/academic-year`(学年管理)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\school\academic-year\page.tsx`
- **功能描述**:学年列表 + CRUD
- 查询 `getAcademicYears()`
- 渲染 `AcademicYearClient` 客户端组件
- **技术栈**Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle
- **ARCHITECTURE.md 契约**B5
#### 2.6.6 `/shell/admin/school/grades`(年级管理)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\school\grades\page.tsx`
- **功能描述**:年级列表 + CRUD + 统计
- 并行查询:`getGrades()` + `getSchools()` + `getStaffOptions()` + `getGradeOverviewStats()`
- 渲染 `GradesClient` 客户端组件(年级 CRUD + 学校/教职工下拉 + 统计概览)
- **技术栈**Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle
- **ARCHITECTURE.md 契约**B5
> **附注**CICD 另有 `school/grades/insights/page.tsx`(年级洞察,含 `SchoolWideSummaryCard` 全校汇总 + ChipNav 年级筛选 + 作业表 + 班级排名表 + `getSchoolWideGradeSummary(ctx.dataScope)`),属管理员数据分析视角,**ARCH §9.4 未单独规划**,可作 B5 后期补强。
### 2.7 announcements 模块1 页缺失ARCH 口径)
#### 2.7.1 `/shell/admin/announcements`(公告管理列表)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\announcements\page.tsx`
- **功能描述**:公告列表 + 状态筛选
- URL 参数:`status`draft/published/archived
- 查询 `getAdminAnnouncementsPageData(status)`(一次性返回 announcements + grades + classes
- 渲染 `AdminAnnouncementsView` 客户端组件
- **技术栈**Server Component + `requirePermission(Permissions.ANNOUNCEMENT_MANAGE)` + Drizzle `announcements/data-access` + 类型守卫 `isValidStatus`
- **ARCHITECTURE.md 契约**B5
> **附注**CICD 另有 `announcements/[id]/page.tsx`(公告详情,含发布/归档/删除/置顶操作)+ `announcements/[id]/edit/page.tsx`(编辑表单 `AnnouncementForm mode="edit"` + grades 关联),**ARCH §9.4 仅规划 1 页**,建议合并到详情子页或工作流组件。
### 2.8 files 模块1 页缺失)
#### 2.8.1 `/shell/admin/files`(文件管理)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\files\page.tsx`
- **功能描述**:文件列表 + 统计
- 并行查询:`getFileAttachmentsWithFilters({ limit: 200 })` + `getFileStats()`
- 渲染 `AdminFilesView` 客户端组件(含统计卡 + 文件列表)
- **技术栈**Server Component + `requirePermission(Permissions.FILE_READ)` + Drizzle `files/data-access`
- **ARCHITECTURE.md 契约**B5
- **CICD 关键代码片段**
```ts
const [files, stats] = await Promise.all([
getFileAttachmentsWithFilters({ limit: 200 }),
getFileStats(),
]);
```
### 2.9 ai-settings 模块1 页缺失)
#### 2.9.1 `/shell/admin/ai-settings`AI 配置)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\ai-settings\page.tsx`
- **功能描述**AI Provider 配置 + Usage 仪表盘(取代旧 `/settings?tab=ai` 与考试页内嵌 AI 弹窗)
- 权限:`AI_CHAT`(普通用户可访问自己的 private provider管理员额外有 `AI_CONFIGURE`(管理 public provider 与他人 private provider
- 渲染 `AiProviderSettingsCard` 客户端组件(含 isAdmin 开关)
- 管理员额外渲染 `AiUsageDashboard`(用量统计)
- **技术栈**Server Component + `requirePermission(Permissions.AI_CHAT)` + 通过 `ctx.permissions.includes(Permissions.AI_CONFIGURE)` 区分管理员
- **ARCHITECTURE.md 契约**B5
### 2.10 system 模块1 页缺失)
#### 2.10.1 `/shell/admin/system`(系统设置)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\admin\settings\page.tsx`
- **功能描述**:系统设置页
- 渲染 `AdminSettingsView` 客户端组件(具体配置项由组件内部决定)
- **技术栈**Server Component + `requirePermission(Permissions.SETTINGS_ADMIN)` + Drizzle
- **ARCHITECTURE.md 契约**B5对应 ARCH §9.4 第 13 行 `admin/system` → `/shell/admin/system`
### 2.11 viewports 模块1 页缺失ARCH 新建)
#### 2.11.1 `/shell/admin/viewports`(视口配置)
- **CICD 参考实现****无**CICD 未实现)
- **功能描述**:对齐 004 §5.4 视口配置BFF/Service 层 viewport 路由策略可视化)
- **技术栈**:未知,需依据 004 §5.4 设计
- **ARCHITECTURE.md 契约**B5
### 2.12 plugins 模块1 页,契约已就绪 ✅)
#### 2.12.1 `/shell/admin/plugins`(插件管理)
- **CICD 参考实现****无**CICD 未实现)
- **功能描述**:现有 portal-shell `plugin-manager` 升级整页
- **技术栈**:依据 portal-shell 已有 plugin-manager 演化
- **ARCHITECTURE.md 契约**:✅ `config-service`(已就绪,可优先实现)
### 2.13 ARCH §9.4 规划但 CICD 无源的"新建"页面3 页)
#### 2.13.1 `/shell/admin/students`(学生管理)
- **CICD 参考实现****无**
- **ARCHITECTURE.md 契约**B5
- **建议**:参照 CICD 中 `admin/school/classes` 等管理页设计,作学生维度聚合视图。
#### 2.13.2 `/shell/admin/teachers`(教师管理)
- **CICD 参考实现****无**
- **ARCHITECTURE.md 契约**B5
- **建议**:参照 CICD 中 `admin/users` + `getStaffOptions()` 演化。
#### 2.13.3 `/shell/admin/organization`(组织管理)
- **CICD 参考实现****无**
- **ARCHITECTURE.md 契约**B5
- **建议**:合并 `school/departments` + `school/grades` + 师生维度,作组织架构视图。
---
## 三、按 ARCH §9.4 模块汇总
| 模块 | ARCH 规划页数 | portal-shell 已完成 | 缺失 | 契约状态 |
| ---------------- | ---------------------------------------------------------------- | ------------------- | ------------------ | ------------------- |
| dashboard | 1 | 1 | 0 | ✅ `adminDashboard` |
| users | 1+1 import 子页) | 0 | 1+1 子页) | ❌ |
| roles | 1 | 0 | 1 | ❌ |
| permissions | 1 | 0 | 1 | ❌ |
| audit-logs | 4含 overview/login/data-changes | 0 | 4 | ❌ |
| invitation-codes | 1 | 0 | 1 | ❌ |
| school | 6schools/classes/departments/academic-year/grades + redirect | 0 | 6 | ❌ |
| classes | 1与 school/classes 同源) | 0 | 0合并到 school | ❌ |
| students | 1 | 0 | 1 | ❌ |
| teachers | 1 | 0 | 1 | ❌ |
| organization | 1 | 0 | 1 | ❌ |
| announcements | 1 | 0 | 1 | ❌ |
| files | 1 | 0 | 1 | ❌ |
| ai-settings | 1 | 0 | 1 | ❌ |
| system | 1 | 0 | 1 | ❌ |
| viewports | 1 | 0 | 1 | ❌ |
| plugins | 1 | 0 | 1 | ✅ `config-service` |
| **合计** | **~24** | **1** | **~23** | **2 ✅ / ~22 ❌** |
> **批次**:全部 B5契约就绪优先级`plugins` ✅ > `dashboard` ✅(已完成)> 其余 ❌ 需 MSW 兜底先行。
---
## 四、CICD 中存在但 ARCH §9.4 未规划的管理员页面21 页B5 后期候选)
> 这些页面在 CICD 中属于"管理员视角的跨班聚合/全局只读"功能是教师域功能在管理员层级的复用。ARCH §9.4 未将其纳入 24 页规划,但作为完整管理员工作台仍具迁移价值。建议作为 B5 末或 B6 补充批次候选,按需取用。
### 4.1 course-plans 模块4 页)
| CICD 路径 | 功能 | 技术栈 |
| --------------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/course-plans/page.tsx` | 课程计划列表admin 全局视角,`scope.isAdmin=true` | Server Component + `requirePermission(Permissions.COURSE_PLAN_READ)` + `getCoursePlans({ status }, { isAdmin: true })` |
| `admin/course-plans/create/page.tsx` | 创建课程计划 | Server Component + `requirePermission(Permissions.COURSE_PLAN_MANAGE)` + 并行查询 classes/subjects/teachers/academicYears + `CoursePlanForm mode="create"` |
| `admin/course-plans/[id]/page.tsx` | 课程计划详情 | Server Component + `getCoursePlanById(id, { userId, isAdmin: true })` + `CoursePlanDetail` |
| `admin/course-plans/[id]/edit/page.tsx` | 编辑课程计划 | Server Component + `requirePermission(Permissions.COURSE_PLAN_MANAGE)` + `CoursePlanForm mode="edit"` |
### 4.2 curriculum-map 模块1 页)
| CICD 路径 | 功能 | 技术栈 |
| ------------------------------- | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/curriculum-map/page.tsx` | 课程地图(标准覆盖热图 + 5 张统计卡:教师/教案/已发布/已提交/已关联标准数) | Server Component + `requirePermission(Permissions.LESSON_PLAN_READ)` + `getStandardsCoverageHeatmapAction()` + `getGlobalLessonPlanStatsAction()` + `CurriculumMapView` |
### 4.3 elective 模块4 页)
| CICD 路径 | 功能 | 技术栈 |
| ----------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `admin/elective/page.tsx` | 选修课列表(`StatsCardsLoader` + `CourseListLoader` 双 Suspense 流式) | Server Component + `requirePermission(Permissions.ELECTIVE_READ)` + `ElectivePageLayout` |
| `admin/elective/create/page.tsx` | 创建选修课 | Server Component + `getSubjectOptions()/getGrades()/getStaffOptions()` + `ElectiveCourseForm mode="create"` |
| `admin/elective/[id]/page.tsx` | 选修课详情(含 `getCourseSelections` 选课记录) | Server Component + `requirePermission(Permissions.ELECTIVE_READ)` + `ElectiveCourseDetail` |
| `admin/elective/[id]/edit/page.tsx` | 编辑选修课 | Server Component + `ElectiveCourseForm mode="edit"` |
### 4.4 questions 模块1 页)
| CICD 路径 | 功能 | 技术栈 |
| -------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/questions/page.tsx` | 题库管理(筛选 + 导入导出 + 创建) | Server Component + `requirePermission(Permissions.QUESTION_READ)` + `getQuestions({...})` + `QuestionFilters`Suspense + `QuestionBankResultsClient` + `ImportExportButtons` + `CreateQuestionButton` |
### 4.5 lesson-plans 模块2 页)
| CICD 路径 | 功能 | 技术栈 |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/lesson-plans/page.tsx` | 教案列表4 统计卡:总数/已发布/草稿/已归档 + `LessonPlanList viewMode="admin"` + `LessonPlanProviderSetup` 注入 ADMIN_ROLE_CONFIG | Server Component + `requirePermission(Permissions.LESSON_PLAN_READ)` + `getLessonPlans({}, ctx.dataScope, ctx.userId)` + `getLessonPlanStats()` |
| `admin/lesson-plans/[planId]/view/page.tsx` | 教案只读视图(含 textbook/chapter 标题解析) | Server Component + `getLessonPlanById(planId, ctx.userId)` + `getTextbookById/getChaptersByTextbookId` 并行 + `LessonPlanReadonlyView` |
### 4.6 error-book 模块1 页)
| CICD 路径 | 功能 | 技术栈 |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/error-book/page.tsx` | 错题本分析5 统计卡 + 学科 Tab + 学科分布图 + 章节/知识点薄弱度并排 + Top 50 学生 + Top 10 高频错题;限 500 学生 + DataScope 校验 `type === "all"` | Server Component + `requirePermission(Permissions.ERROR_BOOK_ANALYTICS_READ)` + 多 data-access 并行 + `SubjectTabs/StatsGrid/SubjectDistributionChart/ChapterWeaknessChart/KnowledgePointWeaknessChart/GroupedStudentErrorTable/TopWrongQuestions` + `WidgetBoundary` |
### 4.7 scheduling 模块3 页)
| CICD 路径 | 功能 | 技术栈 |
| ----------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/scheduling/auto/page.tsx` | 自动排课(`AutoSchedulePanel` + 跳转 rules 配置入口) | Server Component + `requirePermission(Permissions.SCHEDULE_AUTO)` + `getAdminClassesForScheduling()` |
| `admin/scheduling/changes/page.tsx` | 排课变更审批(变更列表 + 冲突检测 + 课表网格) | Server Component + `requirePermission(Permissions.SCHEDULE_ADJUST)` + 并行 `getAdminClassesForScheduling/getScheduleChanges/getScheduleEntriesForAdmin` + `ScheduleChangeList canApprove` + `ScheduleConflictsView` + `ScheduleGridView` |
| `admin/scheduling/rules/page.tsx` | 排课规则配置 | Server Component + `requirePermission(Permissions.SCHEDULE_ADJUST)` + `getSchedulingRules()` + `SchedulingRulesForm` |
### 4.8 attendance 模块1 页)
| CICD 路径 | 功能 | 技术栈 |
| --------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/attendance/page.tsx` | 考勤管理7 统计卡 + 班级对比卡 + 考勤-成绩关联分析卡 + 筛选器) | Server Component + `requirePermission(Permissions.ATTENDANCE_READ)` + 并行 `getAttendanceRecords/getAttendanceStats/getGrades/getClassComparison/getAttendanceGradeCorrelation` + `AdminAttendanceFilters`(含 class/status/date 筛选) |
> **附注**CICD `admin/attendance/admin-attendance-filters.tsx` 是页内私有 Client Component封装 `FilterBar + Select + Input`,使用 `ATTENDANCE_STATUS_OPTIONS` 常量。迁移时此类"页内 filters 组件"模式应保持。
### 4.9 announcements 详情/编辑2 页)
| CICD 路径 | 功能 | 技术栈 |
| ---------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/announcements/[id]/page.tsx` | 公告详情(`AnnouncementDetail canManage` + 跳转 edit + 发布/归档/删除/置顶操作入口) | Server Component + `requirePermission(Permissions.ANNOUNCEMENT_MANAGE)` + `getAdminAnnouncementDetailPageData(id)` + `AnnouncementsServiceProvider` |
| `admin/announcements/[id]/edit/page.tsx` | 公告编辑(`AnnouncementForm mode="edit"` + grades 关联) | Server Component + `requirePermission(Permissions.ANNOUNCEMENT_MANAGE)` + `getEditAnnouncementPageData(id)` |
### 4.10 users/import 子页1 页)
已在 §2.2.2 列入 ARCH 规划口径(与 `users` 工单合并),此处不重复。
### 4.11 roles/[id] 详情1 页)
| CICD 路径 | 功能 | 技术栈 |
| --------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `admin/roles/[id]/page.tsx` | 角色详情(`RolePermissionMatrix` 权限矩阵 + `RoleDetailEditButton` + admin 角色锁定提示卡) | Server Component + `requirePermission(Permissions.ROLE_READ)` + `getRoleById(id)` + `notFound()` 兜底 + admin 角色强制锁定isLocked |
### 4.12 school/grades/insights1 页)
已在 §2.6 附注中提及,不重复。
---
## 五、迁移注意事项
### 5.1 架构迁移要点
1. **管理员权限校验**:所有 23 缺失页面均需 `requirePermission(Permissions.XXX)` 在 Server Component 入口校验。CICD 模式可直接复用,但需对齐 portal-shell 的 auth-guard004 §5 鉴权链路)。
2. **DataScope 上下文**`school/grades/insights`、`error-book`、`attendance` 等页面依赖 `ctx.dataScope`all / class_taught / class_members / childrenportal-shell 需在 BFF 层补齐数据范围上下文。
3. **Server Action + router.refresh 模式**`invitation-codes`、`announcements/[id]/edit` 等使用此模式portal-shell 需保持等价机制BFF API + 客户端 refetch
4. **导出功能**`audit-logs` 三页 + `questions` 导入导出按钮,需后端提供 CSV 导出端点。
5. **服务注入**`audit-logs/overview` 使用 `AuditServiceProvider + adminAuditService` 模式portal-shell 应在 BFF/Service 层保留服务抽象。
### 5.2 模块复杂度风险
| 模块 | 风险点 | 缓解建议 |
| ----------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| **audit-logs**4 页) | 审计核心,多维筛选 + 导出 + 概览统计 + 数据变更统计 | 后端契约工单优先;前端可 MSW 先行 4 页 UI |
| **school**6 页) | 体系复杂6 子页 + 跨表关联schools/classes/departments/grades/academic-year/staff | 按 schools → grades → classes → departments → academic-year 顺序迁移insights 作 B5 末 |
| **scheduling**3 页,§四补充批次) | 涉及自动排课算法 + 冲突检测 + 审批流 | 算法侧需独立服务/工单;前端先做 changes 审批流auto/rules 后置 |
| **error-book**(§四补充批次) | 5 统计卡 + 4 图表 + Top 50 学生 + Top 10 错题;限 500 学生性能保护 | 复用教师域 error-book 组件,仅换 data-access 为 admin scope图表组件应已存在 |
| **attendance**(§四补充批次) | 7 统计 + 班级对比 + 考勤-成绩关联分析 | 后端需提供 `getClassComparison` + `getAttendanceGradeCorrelation` 两个分析端点 |
| **curriculum-map**(§四补充批次) | 标准覆盖热图 + 5 张统计卡 + `getStandardsCoverageHeatmapAction` Server Action | 依赖 lesson-preparation 模块的 actions-analytics需先迁移教师域 lesson-plans |
| **ai-settings** | 双权限AI_CHAT + AI_CONFIGURE+ Provider 配置 + Usage 仪表盘 | 后端需提供 provider CRUD + usage 聚合端点 |
### 5.3 契约就绪优先级建议
按 ARCH §9.4 节奏原则(契约就绪页先行,❌ 页用 MSW 先上 UI
1. **优先级 1契约 ✅,可立即实现真实查询)**
- `/shell/admin/plugins`(✅ `config-service`
- `/shell/admin`(已完成)
2. **优先级 2高价值 + 中等复杂度,需后端契约工单)**
- `/shell/admin/users` + `/shell/admin/users/import`(用户管理 + 批量导入)
- `/shell/admin/roles` + `/shell/admin/permissions`RBAC 核心)
- `/shell/admin/invitation-codes`(与 users 共享 USER_MANAGE 权限)
- `/shell/admin/announcements`(公告管理)
- `/shell/admin/files`(文件管理)
3. **优先级 3高复杂度建议 MSW 先行)**
- `/shell/admin/audit-logs/*`4 页,审计核心,多维筛选 + 导出)
- `/shell/admin/school/*`6 页,体系复杂)
4. **优先级 4ARCH 新建,需独立设计)**
- `/shell/admin/students`、`/shell/admin/teachers`、`/shell/admin/organization`、`/shell/admin/viewports`、`/shell/admin/system`
5. **优先级 5§四补充批次B5 末或 B6**
- course-plans/curriculum-map/elective/questions/lesson-plans/error-book/scheduling/attendance 共 16 页
### 5.4 CICD admin/layout.tsx 迁移说明
CICD `admin/layout.tsx` 仅 `return <>{children}</>`**无独立侧边栏/权限逻辑**(侧边栏由 `(dashboard)/layout.tsx` 统一处理,权限由各 page 调 `requirePermission`)。
portal-shell 同样无需在 `shell/admin/layout.tsx` 实现额外逻辑,保持 passthrough 即可;侧边栏与权限校验由 `shell/layout.tsx` 与各 page.tsx 分担。
---
## 六、附录CICD admin/page.tsx 完整清单41 个)
| # | CICD 路径 | ARCH §9.4 归属 | 缺失/已有 |
| --- | ------------------------------------------- | ----------------------------- | ------------------------------------------- |
| 1 | `admin/dashboard/page.tsx` | §9.4 第 1 行 ✅ | portal-shell `/shell/admin/page.tsx` 已完成 |
| 2 | `admin/users/page.tsx` | §9.4 第 2 行 ❌ | 缺失 |
| 3 | `admin/users/import/page.tsx` | §9.4 第 2 行 ❌(合并) | 缺失 |
| 4 | `admin/roles/page.tsx` | §9.4 第 3 行 ❌ | 缺失 |
| 5 | `admin/roles/[id]/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 6 | `admin/permissions/page.tsx` | §9.4 第 3 行 ❌ | 缺失 |
| 7 | `admin/audit-logs/page.tsx` | §9.4 第 4 行 ❌ | 缺失 |
| 8 | `admin/audit-logs/overview/page.tsx` | §9.4 第 4 行 ❌ | 缺失 |
| 9 | `admin/audit-logs/login-logs/page.tsx` | §9.4 第 4 行 ❌ | 缺失 |
| 10 | `admin/audit-logs/data-changes/page.tsx` | §9.4 第 4 行 ❌ | 缺失 |
| 11 | `admin/invitation-codes/page.tsx` | §9.4 第 5 行 ❌ | 缺失 |
| 12 | `admin/school/page.tsx`redirect | §9.4 第 6 行 ❌ | 缺失 |
| 13 | `admin/school/schools/page.tsx` | §9.4 第 6 行 ❌ | 缺失 |
| 14 | `admin/school/classes/page.tsx` | §9.4 第 6/7 行 ❌ | 缺失 |
| 15 | `admin/school/departments/page.tsx` | §9.4 第 6 行 ❌ | 缺失 |
| 16 | `admin/school/academic-year/page.tsx` | §9.4 第 6 行 ❌ | 缺失 |
| 17 | `admin/school/grades/page.tsx` | §9.4 第 6 行 ❌ | 缺失 |
| 18 | `admin/school/grades/insights/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 19 | `admin/announcements/page.tsx` | §9.4 第 10 行 ❌ | 缺失 |
| 20 | `admin/announcements/[id]/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 21 | `admin/announcements/[id]/edit/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 22 | `admin/files/page.tsx` | §9.4 第 11 行 ❌ | 缺失 |
| 23 | `admin/ai-settings/page.tsx` | §9.4 第 12 行 ❌ | 缺失 |
| 24 | `admin/settings/page.tsx` | §9.4 第 13 行 ❌(→ /system | 缺失 |
| 25 | `admin/course-plans/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 26 | `admin/course-plans/create/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 27 | `admin/course-plans/[id]/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 28 | `admin/course-plans/[id]/edit/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 29 | `admin/curriculum-map/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 30 | `admin/elective/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 31 | `admin/elective/create/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 32 | `admin/elective/[id]/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 33 | `admin/elective/[id]/edit/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 34 | `admin/questions/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 35 | `admin/lesson-plans/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 36 | `admin/lesson-plans/[planId]/view/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 37 | `admin/error-book/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 38 | `admin/scheduling/auto/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 39 | `admin/scheduling/changes/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 40 | `admin/scheduling/rules/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
| 41 | `admin/attendance/page.tsx` | §四补充批次 | 缺失(非 ARCH §9.4 规划) |
**ARCH §9.4 规划但 CICD 无源5 页)**
| # | ARCH 目标路由 | 说明 | 契约 |
| --- | --------------------------- | -------------------------------- | ------------------- |
| A | `/shell/admin/students` | ARCH 新建CICD 无 | ❌ |
| B | `/shell/admin/teachers` | ARCH 新建CICD 无 | ❌ |
| C | `/shell/admin/organization` | ARCH 新建CICD 无 | ❌ |
| D | `/shell/admin/viewports` | ARCH 新建,对齐 004 §5.4 | ❌ |
| E | `/shell/admin/plugins` | ARCH 新建plugin-manager 升级) | ✅ `config-service` |
---
## 七、关键结论
1. **portal-shell 管理域完成度1/24 ≈ 4%**(仅仪表盘),是各角色域中完成度最低的。
2. **缺失 23 页全部属 B5 批次**,节奏紧迫,建议立即启动契约工单。
3. **契约就绪仅 2 个**`adminDashboard`(已用)+ `config-service`plugins未实现
4. **CICD 有 21 页 ARCH §9.4 未规划**,是教师域功能在管理员视角的复用,建议作为 B5 末或 B6 补充批次,按需取用。
5. **高复杂度模块**audit-logs4 页 + 导出、school6 子页 + 跨表关联、scheduling算法、error-book多图表聚合
6. **CICD admin/layout.tsx 无独立逻辑**portal-shell 保持 passthrough 即可,侧边栏与权限由 shell/layout + 各 page.tsx 分担。
7. **建议实施顺序**:契约就绪的 `plugins` → 高价值低复杂度的 `users/roles/permissions/invitation-codes/announcements/files/ai-settings` → 高复杂度的 `audit-logs/*` + `school/*` → ARCH 新建的 `students/teachers/organization/viewports/system` → §四补充批次 16 页。

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。

View File

@@ -0,0 +1,725 @@
# 家长域Parent待完成功能分析
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\parent\`
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\parent\`
> 规划依据:`apps/portal-shell/ARCHITECTURE.md` §9.324 页B4 批次)
> 分析日期2026-07-24
> 任务范围:仅分析,不写代码
---
## 一、页面完成度总览
### 1.1 portal-shell 家长域现状
当前 `apps/portal-shell/src/app/shell/parent/` 下**仅有 1 个页面**
| 路径 | 状态 | 说明 |
| ------------------------ | --------- | ------------------------------------------------ |
| `/shell/parent/page.tsx` | ✅ 已完成 | 家长仪表盘(仅基础版,缺 trend/weakness 详情页) |
### 1.2 完成度统计
| 状态 | 数量 | 说明 |
| ------------ | ------ | ----------------------------------------------------------------------- |
| ✅ 已完成 | 1 | `/shell/parent/page.tsx`(仪表盘基础版,仍缺 2 个详情页) |
| 🟡 部分完成 | 1 | dashboard 模块基础页有trend/weakness 详情页缺) |
| ❌ 缺失 | 22 | 见下方模块清单 |
| **规划总数** | **24** | ARCHITECTURE.md §9.3 B4 批次(含 notifications/settings 共享路由 2 页) |
### 1.3 portal-shell 仪表盘与 CICD 仪表盘的技术差异(关键架构差异)
| 维度 | CICD 仪表盘 | portal-shell 仪表盘 |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| 渲染模式 | Server Component`async function` | Client Component`"use client"` |
| 数据获取 | Server Action `getParentDashboardAction()` + React `use()` 流式渲染 | `useParentDashboard()` Hook`@/lib/api`,走 BFF |
| 国际化 | `next-intl` `getTranslations` | 无(硬编码中文) |
| 权限校验 | `getAuthContext` + dataScope 类型守卫 | 无 |
| 视图组件 | `ParentDashboard` / `ChildCard` / `ParentAttentionBanner` / `AiChildSummary`(拆分到 `@/modules/dashboard/components` / `@/modules/parent/components` / `@/modules/ai/components` | 直接内联在 page.tsx |
| 数据来源 | 直查 DBDrizzle | 走 data-ana 微服务聚合(`parentDashboard` 契约) |
| 多子女支持 | ✅ 子女卡片网格 + AI 学情摘要(每子女一张) | ❌ 仅展示 `child_avg_score` / `child_class_rank` 等单值字段 |
> **重要提示**portal-shell 已转向微服务 + BFF 架构,迁移时**不能照搬 CICD 的 Server Action + Drizzle 直查模式**。家长页面应通过 `teacher-bff` / `data-ana` 等服务的 gRPC/HTTP API 获取数据,前端通过 hooks 消费。家长域多为**只读视图**,重点在 `myChildren` 契约 + `child-overview` 聚合。
---
## 二、缺失页面清单(按模块分组)
### 2.1 dashboard 模块3 页规划1 已完成2 缺失)
#### 2.1.1 ✅ `/shell/parent/page.tsx`(已完成)
- **portal-shell 实现**:基础仪表盘,显示孩子平均分 / 班级排名 / 薄弱知识点 / 预警通知
- **技术栈**Client Component + `useParentDashboard()` Hook + `DashboardShell` / `StatCard` 共享组件
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\dashboard\page.tsx`
- **CICD 技术栈**Server Component + `getParentDashboardAction()` Server Action + `ParentDashboard` 视图组件 + React `use()` 流式渲染 + `generateMetadata`
- **CICD 关键功能**
- 无子女时显示 `ParentNoChildrenPage` 空态
- 子女卡片列表(移动端水平滑动 + 桌面端网格,响应式)
- AI 学情摘要区域(`AiChildSummary`,每子女一张,客户端组件,由 `AiClientProvider` 注入数据服务)
- 家长关注横幅(`ParentAttentionBanner`
- 传入 `homeworkCompletionRate` 等"已知安全字段"给 AI
- **ARCHITECTURE.md 契约**:✅ `parentDashboard`
#### 2.1.2 ❌ `/shell/parent/trend`(学习趋势详情页,缺失)
- **CICD 参考实现**CICD 无独立 `/trend` 页面(融合在 dashboard 与 grades 中以趋势卡片呈现)
- **功能描述**:家长视角的学习趋势详情页,应展示:
- 多子女学习趋势对比
- 历史成绩趋势、知识点掌握趋势、对比基线
- 跨学年/学期聚合
- **技术栈建议**:复用 `parentDashboard` 契约扩展字段 + 趋势图组件Recharts/Echarts多子女对比布局参考 `ParentChildrenDataPage`
- **ARCHITECTURE.md 契约**:✅ `parentDashboard`(需扩展 trend 子契约)
- **CICD 关键代码片段**dashboard 中仅有 `ParentAttentionBanner`grades 页面中 `GradeTrendCard` + `GrowthArchiveChart` 是趋势相关参考
#### 2.1.3 ❌ `/shell/parent/weakness`(薄弱知识点详情页,缺失)
- **CICD 参考实现**CICD 无独立 `/weakness` 页面(融合在 dashboard / error-book 中以 `weakKps` / `child_weak_points` 呈现)
- **功能描述**:家长视角的薄弱知识点详情页,应展示:
- 多子女薄弱知识点汇总
- 按学科分组、关联错题、推荐练习
- 掌握率进度条
- **技术栈建议**:复用 `error-book` 模块的 `getKnowledgePointWeakness` 模式 + `Progress` 组件
- **ARCHITECTURE.md 契约**:✅ `parentDashboard`(需扩展 weakness 子契约)
- **CICD 关键代码片段**`error-book/page.tsx``weakKps.map` 渲染 `Progress` 进度条 + `kp.knowledgePointName` + `kp.errorCount` + `kp.masteryRate` 是直接参考
---
### 2.2 children 模块1 页规划0 已完成1 缺失)⚠️ 重点
#### 2.2.1 ❌ `/shell/parent/children/[studentId]`(子女详情页,缺失)⚠️ 重要
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\children\[studentId]\page.tsx`
- **功能描述**
- **单个子女的完整详情视图**(家长域最核心页面)
- **双重权限校验**
1. `verifyParentChildRelationAction(studentId)` 校验家长与子女存在关系防跨家庭信息泄露G4-002 审计修复)
2. dataScope 二次校验:`ctx.dataScope.type === "all"``childrenIds.includes(studentId)`
- 校验失败显示 `EmptyState` + `ShieldAlert` 图标("访问被拒绝"
- **头部**`ChildDetailHeader`(子女基本信息)
- **主体面板**`ChildDetailPanel`(多 Tab 切换视图,`initialTab` 来自 searchParams
- **兄弟姐妹切换器**`SiblingSwitcher`(在多子女家庭中快速切换查看不同子女)
- 并行拉取:`getChildDashboardDataAction(studentId)` + `getChildNameListAction()`
- `notFound()` 兜底(子女不存在)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`verifyParentChildRelationAction` / `getChildDashboardDataAction` / `getChildNameListAction`(均来自 `@/modules/parent/actions`
- `requireAuth()` + `getAuthContext()` 双重身份校验
- 组件:`ChildDetailHeader` / `ChildDetailPanel` / `SiblingSwitcher` / `EmptyState`
- 路由参数:`studentId`
- searchParams`tab`(初始 Tab
- 工具:`getSearchParam`
- **ARCHITECTURE.md 契约**:❌ `myChildren`(需新建,是家长域核心契约)
- **CICD 关键代码片段**
```tsx
const relation = await verifyParentChildRelationAction(studentId)
const isInScope = ctx.dataScope.type === "all" ||
(ctx.dataScope.type === "children" && ctx.dataScope.childrenIds.includes(studentId))
if (!relation || !isInScope) { return <EmptyState icon={ShieldAlert} ... /> }
```
> ⚠️ **此页是家长域最核心页面**是其他模块grades/attendance/homework 等)子女切换的"枢纽"。`myChildren` 契约必须在 B4 首位补齐。
---
### 2.3 grades 模块2 页规划0 已完成2 缺失)
#### 2.3.1 ❌ `/shell/parent/grades`(子女成绩列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\grades\page.tsx`
- **功能描述**
- **多子女成绩对比视图**(家长域特色)
- 每个子女一块:姓名标题 + **导出按钮**`ParentExportButton`,按 studentId 导出)
- **成绩趋势卡片**`GradeTrendCard`含班级平均对比线v3-P2-8 补齐)
- **纵向成长档案图**`GrowthArchiveChart`P3-4 新增,跨学年/学期聚合)
- 学生成绩汇总(`StudentGradeSummary`
- 使用 `Promise.allSettled` 容错:单个子女查询失败不影响其他子女展示
- 用循环 + 类型守卫替代 `as` 断言P1-8 修复)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getStudentGradeSummary(id, ctx.dataScope)` / `getClassAverageTrend(id, undefined, undefined, ctx.dataScope)` / `getStudentGrowthArchive(id, ctx.dataScope)`
- `requirePermission(Permissions.GRADE_RECORD_READ)`
- dataScope 校验:`ctx.dataScope.type === "children"` 且 `childrenIds.length > 0`,否则显示 `ParentNoChildrenPage`
- 组件:`ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentExportButton` / `GradeTrendCard` / `GrowthArchiveChart` / `StudentGradeSummary`
- 类型:`ClassAverageTrendResult` / `StudentGrowthArchiveResult`
- **ARCHITECTURE.md 契约**:❌(列表,未定义)
#### 2.3.2 ❌ `/shell/parent/grades/report-card`(子女成绩报告卡,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\grades\report-card\page.tsx`
- **功能描述**
- 学年 / 学期可切换的成绩报告卡(家长视角)
- **必填查询参数**`studentId`(必须在家长子女范围内)
- 可选参数:`academicYearId` / `semester`"1" | "2"
- 缺少 `studentId` 时显示提示空态 + 返回按钮
- 报告卡视图(`ReportCardView`,子女维度聚合)
- **打印操作**`ReportCardPrintAction`
- 返回成绩列表的面包屑(`ArrowLeft` + Link
- 学年数量提示
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getReportCardData(studentId, ctx.dataScope, { academicYearId, semester })` + `getAcademicYears()`
- `requirePermission(Permissions.GRADE_RECORD_READ)`
- 组件:`ReportCardView` / `ReportCardPrintAction` / `Button` / `EmptyState`
- 工具:`getParam` / `SearchParams` 类型
- searchParams`studentId` / `academicYearId` / `semester`
- dataScope 在 data-access 层校验子女归属(防跨家庭泄露)
- **ARCHITECTURE.md 契约**:❌(未单独定义)
---
### 2.4 exams 模块2 页规划0 已完成2 缺失)⚠️ CICD 无源
> **CICD 参考项目无 exams 模块家长页面**。家长考试相关功能需 portal-shell 从零设计。
#### 2.4.1 ❌ `/shell/parent/exams`(子女考试列表,缺失)
- **CICD 参考实现**:❌ CICD 无对应源页面
- **功能描述**:家长视角的子女考试列表,应展示:
- 多子女考试汇总(每个子女一块)
- 即将到来的考试(含倒计时、考场、座位号)
- 已结束考试(含成绩、查看结果入口)
- 按学科 / 状态过滤
- **技术栈建议**:参考 CICD `parent/grades/page.tsx` 的 `ParentChildrenDataPage` 多子女布局模式 + `parent/practice/page.tsx` 的单/多子女分支渲染
- **ARCHITECTURE.md 契约**:🟡(部分,需补充)
#### 2.4.2 ❌ `/shell/parent/exams/[id]/result`(子女考试结果页,缺失)
- **CICD 参考实现**:❌ CICD 无对应源页面(可参考 `parent/grades/report-card/page.tsx` 的 searchParams + 权限校验模式)
- **功能描述**
- 子女考试得分、排名、班级均分对比
- 题目作答详情(每题对错、参考答案、子女答案)
- 知识点掌握情况
- 错题一键加入错题本入口
- **技术栈建议**:参考 CICD `parent/grades/report-card/page.tsx` 的 `verifyParentChildRelationAction` + dataScope 双重校验模式
- **ARCHITECTURE.md 契约**:🟡(部分,需补充)
---
### 2.5 homework 模块1 页规划0 已完成1 缺失)⚠️ CICD 无源
#### 2.5.1 ❌ `/shell/parent/homework`(子女作业查看,缺失)⚠️ CICD 无源
- **CICD 参考实现**:❌ CICD 无对应源页面
- **功能描述**:家长视角查看子女作业,应展示:
- 多子女作业汇总(每个子女一块)
- 作业列表(标题、状态徽章、截止时间、剩余尝试次数、最新得分、逾期提示)
- 按学科分组 + 状态过滤all/pending/submitted/graded
- **只读视图**(家长不能提交作业,仅查看)
- **技术栈建议**:参考 CICD `parent/practice/page.tsx` 的 `ParentChildrenDataPage` + 单/多子女分支 + `StatsGrid` 统计模式;状态徽章参考学生域 `StatusBadge` + `variantMap`
- **ARCHITECTURE.md 契约**:❌
---
### 2.6 attendance 模块1 页规划0 已完成1 缺失)
#### 2.6.1 ❌ `/shell/parent/attendance`(子女考勤,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\attendance\page.tsx`
- **功能描述**
- **多子女考勤对比视图**(家长域特色)
- 顶部 `headerExtra`:出勤率卡片(`ParentAttendanceRateCard`+ 考勤预警(`ParentAttendanceWarning`
- 每个子女一块:姓名标题 + **考勤日历**`ParentAttendanceCalendar`+ **考勤明细**`ParentStudentAttendanceDetail`
- 使用 `Promise.allSettled` 容错
- **类型映射解耦**:将 attendance 模块的 `StudentAttendanceSummary` 映射为 parent 模块的 `ParentStudentAttendanceSummary`P1-2 解耦parent 模块仅消费自身类型)
- 通过接口抽象消费 attendance 数据P1-2 修复:不再直接 import data-access
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`createAttendanceReadService(ctx.dataScope).getStudentSummary(id)`(接口抽象)
- `getAuthContext()`(注意:未用 `requirePermission`,直接取 ctx
- dataScope 校验:`ctx.dataScope.type === "children"` 且 `childrenIds.length > 0`,否则显示 `ParentNoChildrenPage`
- 组件:`ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentAttendanceWarning` / `ParentAttendanceRateCard` / `ParentAttendanceCalendar` / `ParentStudentAttendanceDetail`
- 类型:`ParentStudentAttendanceSummary` / `AttendanceReadService`
- 映射函数:`toParentSummary`(手动字段映射)
- **ARCHITECTURE.md 契约**:❌
---
### 2.7 classes 模块1 页规划0 已完成1 缺失)⚠️ CICD 无源
#### 2.7.1 ❌ `/shell/parent/classes`(子女班级信息,缺失)⚠️ CICD 无源
- **CICD 参考实现**:❌ CICD 无对应源页面CICD 仅在 `course-plans` 中通过 `getStudentActiveClassId` 间接消费班级数据)
- **功能描述**:家长视角的子女班级信息,应展示:
- 多子女班级列表(每个子女一块)
- 班级基本信息(班名、班主任、任课教师、教室)
- 班级近期动态(作业、考试、公告)
- **技术栈建议**:参考 CICD `parent/course-plans/page.tsx` 的 `getStudentActiveClassId(sid)` 多子女并行查询模式 + `ParentChildrenDataPage` 布局
- **ARCHITECTURE.md 契约**:❌
---
### 2.8 course-plans 模块2 页规划0 已完成2 缺失)
#### 2.8.1 ❌ `/shell/parent/course-plans`(课程计划列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\course-plans\page.tsx`
- **功能描述**
- 家长视角:解析**所有孩子的班级 ID**,用于过滤课程计划
- 并行查询每个子女的活跃班级 ID`getStudentActiveClassId`
- 仅查询 `status: "active"` 的课程计划
- 使用 `CoursePlanList` 组件渲染(`detailBaseHref="/parent/course-plans"`
- 权限上下文:`{ userId, isAdmin: false, classIds }`(家长非管理员,仅限指定班级)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getCoursePlans({ status: "active" }, { userId, isAdmin: false, classIds })` + `getStudentActiveClassId(sid)`(并行 `Promise.all`
- `requirePermission(Permissions.COURSE_PLAN_READ)`
- dataScope 类型守卫:`ctx.dataScope.type === "children"` 且 `childrenIds.length > 0`
- 组件:`CoursePlanList`
- 类型守卫:`filter((id): id is string => id !== null)`
- **ARCHITECTURE.md 契约**:❌
#### 2.8.2 ❌ `/shell/parent/course-plans/[id]`(课程计划详情,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\course-plans\[id]\page.tsx`
- **功能描述**
- 家长视角:仅允许查看**孩子所在班级**的课程计划
- 解析所有孩子的班级 ID传入权限上下文
- 若计划不在子女班级范围内,`notFound()` 兜底
- 使用 `CoursePlanDetail` 组件渲染(`backHref` / `successHref` 均指向 `/parent/course-plans`
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getCoursePlanById(id, { userId, isAdmin: false, classIds })` + `getStudentActiveClassId(sid)`(并行)
- `requirePermission(Permissions.COURSE_PLAN_READ)`
- 组件:`CoursePlanDetail`
- 路由参数:`id`
- `notFound()` 兜底
- **ARCHITECTURE.md 契约**:❌
---
### 2.9 lesson-plans 模块2 页规划0 已完成2 缺失)
#### 2.9.1 ❌ `/shell/parent/lesson-plans`(教案列表,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\lesson-plans\page.tsx`
- **功能描述**
- 家长视角:仅查看**已发布**教案(`status: "published"`
- 学科选项并行加载(`getSubjectOptions`
- **角色配置注入**`LessonPlanProviderSetup` 包裹,传入 `PARENT_ROLE_CONFIG`使筛选功能生效P0-13 修复)
- `LessonPlanList` 以 `viewMode="parent"` 渲染(只读模式)
- `Suspense` 流式渲染 + Skeleton 占位6 个 `h-[180px]` 卡片骨架)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getLessonPlans({ status: "published" }, ctx.dataScope, ctx.userId)` + `getSubjectOptions()`
- `requirePermission(Permissions.LESSON_PLAN_READ)`V4 P0-2 修复:页面层补齐权限校验)
- 组件:`LessonPlanList` / `LessonPlanProviderSetup` / `Skeleton`
- Provider 配置:`PARENT_ROLE_CONFIG`(来自 `@/modules/lesson-preparation/providers/lesson-plan-provider`
- `viewMode="parent"` 标识家长只读视图
- **ARCHITECTURE.md 契约**:❌
#### 2.9.2 ❌ `/shell/parent/lesson-plans/[planId]/view`(教案只读详情,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\lesson-plans\[planId]\view\page.tsx`
- **功能描述**
- 家长视角:仅可查看**孩子所在年级**的已发布课案V4 P0-1 修复,防跨年级信息泄露)
- **scope 校验**`assertPlanInScope(plan, ctx)`,失败 `notFound()`
- 未发布计划显示提示卡片(`readonly.notPublished`
- 教材标题 + 章节标题并行查询V4 P2-1 修复,`Promise.all` 并行)
- `LessonPlanReadonlyView` 渲染计划内容
- 全屏布局(`h-[calc(100vh-4rem)]`
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getLessonPlanById(planId, ctx.userId)` + `getTextbookById(plan.textbookId)` + `getChaptersByTextbookId(plan.textbookId)` + `findChapterById(chapters, plan.chapterId)`
- `requirePermission(Permissions.LESSON_PLAN_READ)`
- scope 校验:`assertPlanInScope(plan, ctx)`lib/scope-check
- 组件:`LessonPlanReadonlyView` / `Skeleton`
- 路由参数:`planId`
- `notFound()` 兜底(计划不存在或越权)
- **ARCHITECTURE.md 契约**:❌
---
### 2.10 error-book 模块1 页规划0 已完成1 缺失)
#### 2.10.1 ❌ `/shell/parent/error-book`(子女错题本,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\error-book\page.tsx`
- **功能描述**
- **单/多子女分支渲染**(家长域特色):
- **单子女**:直接展示 `StatsGrid` 5 列统计卡片(总数 / 新增 / 学习中 / 已掌握 / 待复习)
- **多子女**:每个子女一张 `Card`,含姓名 + 掌握率徽章 + 4 项统计 + `Progress` 进度条
- 汇总所有子女的错题:**Top 错题**`TopWrongQuestions`,前 5+ **薄弱知识点**`getKnowledgePointWeakness`,前 5
- 薄弱知识点:每条含知识点名称 + 错误次数 + 掌握率 + `Progress` 进度条
- 待复习数 > 0 时高亮(`highlight: stats.dueReviewCount > 0`
- `WidgetBoundary` 降级容错
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getErrorBookStats(id)` + `getStudentNameMap(childrenIds)` + `getTopWrongQuestionsByStudentIds(childrenIds, 5)` + `getKnowledgePointWeakness(childrenIds, 5)`
- `requirePermission(Permissions.ERROR_BOOK_READ)`
- 组件:`StatsGrid` / `TopWrongQuestions` / `WidgetBoundary` / `Card` / `Badge` / `Progress` / `EmptyState`
- 工具:`formatNumber`
- icons`BookX` / `Clock` / `GraduationCap` / `Repeat` / `Sparkles` / `Users`
- **ARCHITECTURE.md 契约**:✅
---
### 2.11 diagnostic 模块1 页规划0 已完成1 缺失)
#### 2.11.1 ❌ `/shell/parent/diagnostic`(子女学情诊断,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\diagnostic\page.tsx`
- **功能描述**
- **多子女诊断对比视图**v4-P1-9 容错增强)
- 预先查询所有子女姓名(`getUserNamesByIds`),用于错误卡片展示
- **allSettled 容错但保留 rejected 项**rejected 项不再静默丢弃,渲染错误卡片(`AlertCircle` + 学生姓名 + "数据加载失败"提示)
- 每个子女:诊断摘要(`getStudentMasterySummary`+ **已发布诊断报告**`getDiagnosticReports`v4-P1-3 修复:家长仅可见 `status: "published"` 报告,避免草稿泄露)
- `StudentDiagnosticView` 渲染(`role="parent"`
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getStudentMasterySummary(id)` + `getDiagnosticReports({ studentId: id, status: "published" }, ctx.dataScope)` + `getUserNamesByIds(childrenIds)`
- `requirePermission(Permissions.DIAGNOSTIC_READ)`
- 组件:`ParentChildrenDataPage` / `ParentNoChildrenPage` / `StudentDiagnosticView` / `Card` / `CardContent`
- 类型:`ChildDiagnosticSuccessItem` / `ChildDiagnosticErrorItem`(联合类型 `ChildDiagnosticItem`
- icons`Stethoscope` / `AlertCircle`
- **ARCHITECTURE.md 契约**:✅
---
### 2.12 learning-path 模块1 页规划0 已完成1 缺失)⚠️ CICD 无源
#### 2.12.1 ❌ `/shell/parent/learning-path`(子女学习路径,缺失)⚠️ CICD 无源
- **CICD 参考实现**:❌ CICD 无对应源页面(学生域有 `learning/study-path`,但家长域无对应页)
- **功能描述**:家长视角查看子女的 AI 学习路径,应展示:
- 多子女学习路径汇总(每个子女一块)
- AI 生成的个性化学习路径(参考学生域 `AiStudyPath` 组件)
- 路径进度、节点完成情况
- **技术栈建议**:参考 CICD 学生域 `learning/study-path/page.tsx` 的 `AiStudyPath` + `AiClientProvider` 模式 + `requirePermission(Permissions.AI_CHAT)`;家长域需扩展为多子女布局
- **ARCHITECTURE.md 契约**:❌
---
### 2.13 practice 模块1 页规划0 已完成1 缺失)
#### 2.13.1 ❌ `/shell/parent/practice`(子女练习查看,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\practice\page.tsx`
- **功能描述**
- **单/多子女分支渲染**(家长域特色):
- **单子女**:直接展示 4 列 `StatsGrid`(总练习次数 / 已完成 / 总答题数 / 正确率)+ `PracticeHistory` 历史列表
- **多子女**:每个子女一块 `WidgetBoundary`,含姓名标题 + 4 列统计 + 历史列表
- **家长无会话详情页**,仅展示只读卡片(注释明确:`家长无会话详情页,仅展示只读卡片`
- `PracticeServiceProvider` 包裹(注入练习服务)
- `Promise.allSettled` 容错 + 循环 + 类型守卫(非 `as` 断言)
- 姓名映射并行启动(`nameMapPromise` 与 `childResults` 并行,但类型分离以利类型收窄)
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getPracticeStats(id)` + `getPracticeSessions(id, { pageSize: 20 })` + `getUserNamesByIds(childrenIds)`
- `requirePermission(Permissions.ADAPTIVE_PRACTICE_READ)`
- 组件:`PracticeServiceProvider` / `StatsGrid` / `PracticeHistory` / `WidgetBoundary` / `EmptyState`
- 类型:`PracticeSessionSummary` / `PracticeStats` / `ChildPracticeItem`
- icons`Target` / `Users` / `CheckCircle2` / `TrendingUp` / `Award`
- `valueClassName: "tabular-nums"` 数字对齐
- **ARCHITECTURE.md 契约**:❌
---
### 2.14 elective 模块1 页规划0 已完成1 缺失)
#### 2.14.1 ❌ `/shell/parent/elective`(子女选课查看,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\elective\page.tsx`
- **功能描述**
- **多子女选课记录查看**(只读,家长不能选课)
- 每个子女一块:`ParentSelectionView`(子女姓名 + 选课记录列表)
- **双重校验**`getChildrenAction()` 获取关系 + `getChildBasicInfoAction(r.studentId, r.relation)` 二次校验防跨家庭信息泄露G4-002 审计修复)
- `Promise.allSettled` 容错
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getStudentSelections(r.studentId)` + `getChildrenAction()` + `getChildBasicInfoAction(r.studentId, r.relation)`
- `requirePermission(Permissions.ELECTIVE_READ)`
- 组件:`ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentSelectionView`
- 类型:`ChildSelectionData`
- **ARCHITECTURE.md 契约**:❌
---
### 2.15 leave 模块1 页规划0 已完成1 缺失)
#### 2.15.1 ❌ `/shell/parent/leave`(家长在线请假,缺失)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\parent\leave\page.tsx`
- **功能描述**
- **家长域少有的"写"操作页面**(其他多为只读)
- 顶部:**在线请假表单**`LeaveRequestForm`,下拉选择子女,自动写入 classId
- 底部:该家长**所有子女**的请假申请列表(`LeaveRequestList`,按 dataScope=children 过滤)
- 子女选项构造:遍历关系列表,查询每个子女的 `basicInfo`,仅包含有活跃班级的子女(`basic.classId && basic.className`
- 返回仪表盘的面包屑(`ArrowLeft` + Link to `/parent/dashboard`
- 无关联子女时显示提示卡片
- **技术栈**
- Server Component + `force-dynamic`
- Server Actions`getChildrenAction()` + `getLeaveRequests({ scope, currentUserId, page: 1, pageSize: 50 })` + `getChildBasicInfoAction(r.studentId, r.relation)`
- `getAuthContext()`(注意:未用 `requirePermission`,直接取 ctx
- 组件:`LeaveRequestForm` / `LeaveRequestList` / `Card` / `Button`
- 类型:`ChildOption`(含 `id` / `name` / `classId` / `className`
- icons`ArrowLeft` / `CalendarDays`
- scope 处理:家长 scope 为 `children``getLeaveRequests` 按 scope 过滤所有子女记录
- **ARCHITECTURE.md 契约**:❌
---
### 2.16 notifications + settings + preferences 模块(共享 2 页 + 家长专属 1 页0 已完成1 缺失 + 2 共享)
#### 2.16.1 ❌ `/shell/parent/preferences`(家长偏好设置,缺失)
- **CICD 参考实现**:❌ CICD 无对应源页面
- **功能描述**:家长专属偏好设置,可能包括:
- 通知偏好(哪些子女事件需推送:成绩、考勤、作业、预警)
- AI 摘要偏好(摘要频率、详细程度)
- 隐私偏好(是否共享数据给 AI
- **技术栈建议**:参考共享 settings 页面模式 + 家长域专属字段
- **ARCHITECTURE.md 契约**:❌
- **批次**B4
#### 2.16.2 ❌ `/shell/notifications`(通知中心,缺失,共享)
- **CICD 参考实现**:❌ CICD `parent/` 下无对应源页面
- **功能描述**:通知中心(家长可见范围)
- **ARCHITECTURE.md 契约**:✅(共享,已就绪)
- **批次**B1/B3共享路由
#### 2.16.3 ❌ `/shell/settings`(系统设置,缺失,共享)
- **CICD 参考实现**:❌ CICD `parent/` 下无对应源页面
- **功能描述**:系统设置(通用)
- **ARCHITECTURE.md 契约**:✅(共享,已就绪)
- **批次**B1/B3共享路由
> **注**portal-shell 当前 `/shell/notifications` / `/shell/settings` 路由是否存在需在共享层确认。家长域仅需补充 `/shell/parent/preferences`。
---
## 三、按模块汇总
| 模块 | 规划页数 | 已完成 | 缺失 | 备注 |
| --------------------- | -------- | ------ | ------ | --------------------------------------- |
| dashboard | 3 | 1 | 2 | 仪表盘基础页有trend/weakness 详情页缺 |
| children | 1 | 0 | 1 | ⚠️ 家长域核心页,需 `myChildren` 契约 |
| grades | 2 | 0 | 2 | 多子女对比 + 报告卡 |
| exams | 2 | 0 | 2 | ⚠️ CICD 无源页面,全新设计 |
| homework | 1 | 0 | 1 | ⚠️ CICD 无源页面,全新设计 |
| attendance | 1 | 0 | 1 | 多子女考勤对比 |
| classes | 1 | 0 | 1 | ⚠️ CICD 无源页面,全新设计 |
| course-plans | 2 | 0 | 2 | 列表 + 详情,按子女班级过滤 |
| lesson-plans | 2 | 0 | 2 | 列表 + 只读详情scope 校验 |
| error-book | 1 | 0 | 1 | 单/多子女分支,契约 ✅ |
| diagnostic | 1 | 0 | 1 | 多子女诊断,契约 ✅ |
| learning-path | 1 | 0 | 1 | ⚠️ CICD 无源页面,全新设计 |
| practice | 1 | 0 | 1 | 单/多子女分支,只读 |
| elective | 1 | 0 | 1 | 多子女选课查看 |
| leave | 1 | 0 | 1 | 家长域少有的"写"操作 |
| preferences | 1 | 0 | 1 | 家长专属偏好 |
| notifications共享 | 1 | 0 | 1 | 共享路由,契约 ✅ |
| settings共享 | 1 | 0 | 1 | 共享路由,契约 ✅ |
| **合计** | **23** | **1** | **22** | 不含 dashboard 的 2 个详情页已计入缺失 |
> ARCHITECTURE.md §9.3 规划 24 页(含 notifications/settings 共享路由 2 页。portal-shell 当前 1 页已完成22 页缺失1 页为 dashboard 详情页(部分完成)。
---
## 四、关键技术差异 & 迁移注意事项
### 4.1 架构模式差异(最重要)
| 维度 | CICD参考 | portal-shell目标 |
| ------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
| 渲染模式 | Server Component 为主 | 当前仪表盘是 Client Component需确定后续页面模式 |
| 数据获取 | Server Action + Drizzle 直查 DB | **必须改为** BFF / 微服务 API`teacher-bff` / `data-ana` 等) |
| 权限校验 | `requirePermission` + `getAuthContext` + dataScope | 需通过 Gateway + BFF 鉴权,前端用 `usePermission().hasPermission()` |
| 国际化 | `next-intl` `getTranslations` | portal-shell 当前仪表盘硬编码中文,需统一策略 |
| 共享 UI | `@/shared/components/ui/*` | `@/shared/components/ui/*`(已迁移部分) |
| 模块组件 | `@/modules/<domain>/components/*` | 需在 portal-shell 重建或下沉到 BFF |
| 多子女布局 | `ParentChildrenDataPage` + `ParentNoChildrenPage` 通用容器 | 需在 portal-shell 重建(家长域核心模式) |
| 单/多子女分支 | error-book / practice 中 `isSingleChild` 分支 | 需保留此模式(家长域特色) |
### 4.2 路由路径对齐
| CICD 路径 | ARCHITECTURE.md §9.3 目标路径 | 备注 |
| ----------------------------------- | ------------------------------------------ | ----------------------------------------------- |
| `parent/dashboard` | `/shell/parent`(家长仪表盘) | 提升到 `/shell/parent/page.tsx` |
| `parent/dashboard/trend` | `/shell/parent/trend` | CICD 无独立页,需新建 |
| `parent/dashboard/weakness` | `/shell/parent/weakness` | CICD 无独立页,需新建 |
| `parent/children/[studentId]` | `/shell/parent/children/[studentId]` | 路径一致 |
| `parent/grades` | `/shell/parent/grades` | 路径一致 |
| `parent/grades/report-card` | `/shell/parent/grades/report-card` | 路径一致 |
| `(无)` | `/shell/parent/exams` | 全新设计 |
| `(无)` | `/shell/parent/exams/[id]/result` | 全新设计 |
| `(无)` | `/shell/parent/homework` | 全新设计 |
| `parent/attendance` | `/shell/parent/attendance` | 路径一致 |
| `(无)` | `/shell/parent/classes` | 全新设计 |
| `parent/course-plans` | `/shell/parent/course-plans` | 路径一致 |
| `parent/course-plans/[id]` | `/shell/parent/course-plans/[id]` | 路径一致 |
| `parent/lesson-plans` | `/shell/parent/lesson-plans` | 路径一致 |
| `parent/lesson-plans/[planId]/view` | `/shell/parent/lesson-plans/[planId]/view` | 路径一致 |
| `parent/error-book` | `/shell/parent/error-book` | 路径一致 |
| `parent/diagnostic` | `/shell/parent/diagnostic` | 路径一致 |
| `(无)` | `/shell/parent/learning-path` | 全新设计(学生域有 `learning/study-path` 参考) |
| `parent/practice` | `/shell/parent/practice` | 路径一致 |
| `parent/elective` | `/shell/parent/elective` | 路径一致 |
| `parent/leave` | `/shell/parent/leave` | 路径一致 |
| `(无)` | `/shell/parent/preferences` | 全新设计 |
| 共享 | `/shell/notifications` | 共享路由 |
| 共享 | `/shell/settings` | 共享路由 |
> **注**:家长域路由迁移**压力较小**14 个 CICD 页面中有 13 个路径完全一致(仅 dashboard 从子路径提升到 `/shell/parent/page.tsx`)。需新建的 6 个页面exams×2、homework、classes、learning-path、preferences在 CICD 中无源。
### 4.3 权限点映射
CICD 中家长页面用到的权限点(迁移时需在 portal-shell `Permissions` 常量中对应):
- `GRADE_RECORD_READ`grades / report-card
- `ATTENDANCE_READ`attendanceCICD 实际用 `getAuthContext` 而非 `requirePermission`,需统一)
- `COURSE_PLAN_READ`course-plans 列表 + 详情)
- `LESSON_PLAN_READ`lesson-plans 列表 + 详情)
- `ERROR_BOOK_READ`error-book
- `DIAGNOSTIC_READ`diagnostic
- `ADAPTIVE_PRACTICE_READ`practice
- `ELECTIVE_READ`elective
- leave 用 `getAuthContext` 而非 `requirePermission`(需统一)
- children 详情用 `requireAuth` + `verifyParentChildRelationAction` + dataScope 二次校验(**双重校验模式**
- dashboard 用 `getParentDashboardAction`(内部封装权限)
### 4.4 共享组件清单(需在 portal-shell 补齐或确认已有)
**家长域通用容器(核心,必须重建)**
- `ParentChildrenDataPage`(多子女数据页通用容器,含 `renderItem` + `headerExtra` + 空态)
- `ParentNoChildrenPage`(无关联子女空态)
**dashboard 模块**
- `ParentDashboard` / `ChildCard` / `ParentAttentionBanner` / `AiChildSummary` / `ParentNoChildrenPage` / `ParentChildrenDataPage`
**children 模块**
- `ChildDetailHeader` / `ChildDetailPanel` / `SiblingSwitcher`
**grades 模块**
- `StudentGradeSummary` / `GradeTrendCard` / `GrowthArchiveChart` / `ReportCardView` / `ReportCardPrintAction` / `ParentExportButton`
**attendance 模块**
- `ParentAttendanceWarning` / `ParentAttendanceRateCard` / `ParentAttendanceCalendar` / `ParentStudentAttendanceDetail`
**course-plans 模块**
- `CoursePlanList` / `CoursePlanDetail`
**lesson-plans 模块**
- `LessonPlanList` / `LessonPlanReadonlyView` / `LessonPlanProviderSetup` + `PARENT_ROLE_CONFIG`
**error-book 模块**
- `StatsGrid` / `TopWrongQuestions` / `WidgetBoundary` / `Progress`
**diagnostic 模块**
- `StudentDiagnosticView``role="parent"`
**practice 模块**
- `PracticeServiceProvider` / `StatsGrid` / `PracticeHistory` / `WidgetBoundary`
**elective 模块**
- `ParentSelectionView`
**leave 模块**
- `LeaveRequestForm` / `LeaveRequestList` + `ChildOption` 类型
**基础 UI共享**
- `Card` / `CardContent` / `CardHeader` / `CardTitle` / `Button` / `Badge` / `EmptyState` / `Skeleton` / `Progress`
### 4.5 B4 实施优先级建议
按用户价值 + 实现复杂度 + 契约就绪度排序:
1. **P0必做家长域核心闭环**
- `children/[studentId]` 子女详情页(⚠️ 核心,需先补 `myChildren` 契约)
- `dashboard/trend` + `dashboard/weakness` 详情页(契约 ✅ `parentDashboard` 已就绪)
2. **P1高频查看契约 ✅)**
- `error-book`(契约 ✅)
- `diagnostic`(契约 ✅)
3. **P2学习数据查看**
- `grades` 列表 + `grades/report-card`
- `attendance`
- `practice`
4. **P3学习资源**
- `course-plans` 列表 + 详情
- `lesson-plans` 列表 + `lesson-plans/[planId]/view`
- `elective`
5. **P4写操作 + 偏好)**
- `leave`(家长域少有的"写"操作)
- `preferences`
6. **P5全新设计CICD 无源)**
- `exams` 列表 + `exams/[id]/result`
- `homework`
- `classes`
- `learning-path`
7. **共享**`notifications` / `settings`B1/B3 共享路由,非 B4 专属)
---
## 五、CICD 与 ARCHITECTURE.md §9.3 对照表
| ARCH §9.3 源路由 | ARCH §9.3 目标路由 | CICD 是否有源页面 | portal-shell 是否已有 | 契约状态 | 备注 |
| ----------------------------------- | ------------------------------------------ | ---------------------- | --------------------- | -------------------- | -------------------- |
| `parent/dashboard` | `/shell`(家长仪表盘) | ✅ | ✅ | ✅ `parentDashboard` | 已完成基础版 |
| `/trend` | `/shell/parent/trend` | ❌(融合在 dashboard | ❌ | ✅ `parentDashboard` | 需新建详情页 |
| `/weakness` | `/shell/parent/weakness` | ❌(融合在 dashboard | ❌ | ✅ `parentDashboard` | 需新建详情页 |
| `parent/children/[studentId]` | `/shell/parent/children/[studentId]` | ✅ | ❌ | ❌ `myChildren` | ⚠️ 核心,双重校验 |
| `parent/grades` | `/shell/parent/grades` | ✅ | ❌ | ❌ | 多子女对比 |
| `parent/grades/report-card` | `/shell/parent/grades/report-card` | ✅ | ❌ | ❌ | 打印支持 |
| `parent/exams` | `/shell/parent/exams` | ❌ | ❌ | 🟡 | CICD 无,全新设计 |
| `parent/exams/[id]/result` | `/shell/parent/exams/[id]/result` | ❌ | ❌ | 🟡 | CICD 无,全新设计 |
| `parent/homework` | `/shell/parent/homework` | ❌ | ❌ | ❌ | CICD 无,全新设计 |
| `parent/attendance` | `/shell/parent/attendance` | ✅ | ❌ | ❌ | 多子女考勤对比 |
| `parent/classes` | `/shell/parent/classes` | ❌ | ❌ | ❌ | CICD 无,全新设计 |
| `parent/course-plans` | `/shell/parent/course-plans` | ✅ | ❌ | ❌ | 按子女班级过滤 |
| `parent/course-plans/[id]` | `/shell/parent/course-plans/[id]` | ✅ | ❌ | ❌ | scope 校验 |
| `parent/lesson-plans` | `/shell/parent/lesson-plans` | ✅ | ❌ | ❌ | 已发布 + 只读 |
| `parent/lesson-plans/[planId]/view` | `/shell/parent/lesson-plans/[planId]/view` | ✅ | ❌ | ❌ | scope 校验 + 全屏 |
| `parent/error-book` | `/shell/parent/error-book` | ✅ | ❌ | ✅ | 单/多子女分支 |
| `parent/diagnostic` | `/shell/parent/diagnostic` | ✅ | ❌ | ✅ | 多子女 + 错误容错 |
| `parent/learning-path` | `/shell/parent/learning-path` | ❌ | ❌ | ❌ | CICD 无,全新设计 |
| `parent/practice` | `/shell/parent/practice` | ✅ | ❌ | ❌ | 单/多子女分支,只读 |
| `parent/elective` | `/shell/parent/elective` | ✅ | ❌ | ❌ | 多子女选课查看 |
| `parent/leave` | `/shell/parent/leave` | ✅ | ❌ | ❌ | 家长域少有的"写"操作 |
| `parent/preferences` | `/shell/parent/preferences` | ❌ | ❌ | ❌ | CICD 无,全新设计 |
| `notifications`、`settings` | 共享路由 | — | ❌ | ✅ | B1/B3共享路由 |
| `preferences` | `/shell/parent/preferences` | ❌ | ❌ | ❌ | 家长专属 |
---
## 六、风险与建议
### 6.1 高风险点
1. **`myChildren` 契约缺失**:家长域核心契约未就绪,`children/[studentId]` 子女详情页无法启动。**必须在 B4 首位补齐**,建议参考 CICD `getChildDashboardDataAction` 的数据结构定义契约
2. **6 个页面 CICD 无源**`exams`×2、`homework`、`classes`、`learning-path`、`preferences` 需从零设计,参考学生域同构页面(如 `exams` 参考学生域 `exams`、`homework` 参考学生域 `learning/assignments`、`learning-path` 参考学生域 `learning/study-path`
3. **架构模式转变**portal-shell 已转向 BFF + 微服务,**不能照搬 CICD 的 Server Action + Drizzle 直查模式**每个页面都需重新设计数据获取链路BFF → data-ana / core-edu 等服务)
4. **多子女布局是家长域核心模式**`ParentChildrenDataPage` + `ParentNoChildrenPage` 通用容器需优先重建13 个页面复用此模式
5. **单/多子女分支渲染**error-book / practice 中的 `isSingleChild` 分支需保留(单子女家庭体验更紧凑)
6. **双重权限校验**children 详情页的 `verifyParentChildRelationAction` + dataScope 二次校验是防跨家庭信息泄露的关键,迁移时必须保留
7. **国际化缺失**当前仪表盘硬编码中文22 页全部完成后需补 i18n建议从一开始就用 `next-intl`
8. **权限校验缺失**:当前仪表盘无 `requirePermission`,需建立 portal-shell 家长域权限校验中间件/装饰器
9. **leave 是少有的"写"操作**:家长域多为只读视图,`leave` 页面涉及表单提交,需重点设计 BFF 写接口 + 事务保证
### 6.2 建议
1. **先建契约**:开工前先在 `shared-proto` 中补齐 §9.3 标 ❌ / 🟡 的契约,特别是 `myChildren`(核心)、`parentDashboard` 扩展trend/weakness 子契约、exams 系列、homework、classes、learning-path、preferences
2. **统一渲染模式**:建议家长页面统一采用 Server Component + BFF 数据获取(与 CICD 一致),避免当前仪表盘的 Client Component 模式
3. **统一权限模型**:建立 portal-shell 家长域 `RequirePermission` 装饰器/中间件,覆盖所有页面;统一 `getAuthContext` 与 `requirePermission` 的使用CICD 中 attendance / leave 用 `getAuthContext`,需统一)
4. **共享容器下沉**`ParentChildrenDataPage` / `ParentNoChildrenPage` 是家长域核心容器,建议下沉到 `@/shared/components/parent/` 或 BFF 返回结构化数据
5. **复用学生域组件**:家长域多为只读视图,大量组件(`StudentGradeSummary` / `GradeTrendCard` / `ReportCardView` / `StudentDiagnosticView` / `PracticeHistory` / `CoursePlanList` / `LessonPlanReadonlyView`)可复用学生域实现,仅需传入 `role="parent"` 标识
6. **CICD 无源页面决策**6 个 CICD 无源页面需产品确认功能范围,建议参考学生域同构页面 + 家长域多子女布局组合设计
7. **分批推进**:按本报告 §4.5 的 P0→P5 优先级分批实施,每批完成后跑 `pnpm run arch:scan` 同步 arch.db
---
**分析完成。共发现 23 个待完成页面(含 2 个 dashboard 详情页 + 1 个 preferences 家长专属页 + 2 个共享路由页 + 18 个其他模块页),其中 22 个完全缺失、1 个为 dashboard 详情页缺失。最关键的 `children/[studentId]` 子女详情页需 `myChildren` 契约先行。6 个页面exams×2、homework、classes、learning-path、preferences在 CICD 中无源,需从零设计。**

View File

@@ -0,0 +1,374 @@
# 共享路由Shared待完成功能分析
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\`
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\`
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.1 / §9.2 / §9.3 / §9.5(共享 ~7 页)
> 分析时间2026-07-24
> 分析范围:仅分析,不写代码
---
## 一、页面完成度总览
| 状态 | 路径 | 说明 |
| ---- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ✅ | `/login` | 登录页 |
| ✅ | `/shell` | 仪表盘根(按角色分发到 `/shell/{teacher\|student\|parent\|admin}`)。**CICD 参考**无单一分发器——CICD 根 `src/app/page.tsx` 直接 `redirect("/dashboard")`(教师仪表盘);各角色仪表盘分散于 `(dashboard)/dashboard/``(dashboard)/student/dashboard/``(dashboard)/parent/dashboard/``(dashboard)/admin/dashboard/`。portal-shell 实现于 `src/app/shell/[[...route]]/page.tsx`catch-all空路由 → `redirect("/shell/{role}")`+ `src/app/shell/layout.tsx`AppFrame 框架fail-closed 读 `x-user-role` 头) |
| ✅ | `/shell/forbidden` | 403 页(`PUBLIC_ROUTES` 白名单) |
| ❌ | `/shell/notifications` | 通知中心(缺失) |
| ❌ | `/shell/settings` | 设置(缺失) |
| ❌ | `/shell/settings/security` | 安全中心 / 改密码(缺失) |
| ❌ | `/shell/announcements` | 公告列表(缺失) |
| ❌ | `/shell/announcements/[id]` | 公告详情(缺失) |
| ❌ | `/shell/messages` | 消息(缺失) |
| ❌ | `/shell/messages/compose` | 撰写消息(缺失) |
| ❌ | `/shell/messages/[id]` | 消息详情(缺失) |
| ❌ | `/shell/profile` | 个人资料(缺失) |
> 备注 1portal-shell 现存 `widgets/topbar/notification-bell`(顶栏铃铛下拉)与 `widgets/universal/notifications-widget`(仪表盘内嵌通知卡片),但**无独立的通知中心整页**。
> 备注 2`widgets/universal/announcements-widget` 已在 P1-2 下线(数据源迁移到 `adminDashboard` 聚合查询),**无用户端公告列表/详情页**。
> 备注 3`shared/lib/navigation.ts` 与 `shared/lib/route-permissions.ts` **均未登记**上述任一共享路由,按 `route-permissions.ts` §5 fail-closed 规则,未登记的 `/shell/*` 路由会被默认拒绝 → 即便加了 page.tsx 也访问不到,必须同步登记权限配置。
---
## 二、缺失页面详情
### 2.1 `/shell/notifications`(通知中心)
- **CICD 参考**:无独立 `notifications/page.tsx`。CICD 中通知集成在三处:
- `widgets/topbar/notification-bell` 的下拉(顶栏铃铛)
- `messages/page.tsx` 内嵌的 `NotificationListSection`(与消息列表合并展示)
- 通知偏好散落在 `settings` 页 + `notifications/preferences.ts`
- **功能描述**
- 用户视角的"通知中心"整页:分页列出当前用户的站内通知(`message_notifications` 表),支持未读/已读筛选、归档、标记已读、全部已读、按优先级low/normal/high/urgent筛选
- 通知类型包括 `message` / `announcement` / `homework` / `grade` / `diagnostic` 五类
- 通知带 `link` 字段可点击跳转到来源页(如作业/成绩/公告详情)
- 顶栏铃铛仅展示最近 N 条 + 未读角标,整页才是完整列表
- **技术栈**
- Server Component + `Suspense` + `SectionErrorBoundary`(局部失败不影响其他区块)
- `notifications/data-access.ts``getNotifications` / `markNotificationAsRead` / `markAllNotificationsAsRead` / `archiveNotification` / `getUnreadNotificationCount`(均 `cacheFn` 包装tag=`notifications`TTL=60s
- Server Actions`notifications/actions.ts`384 行,标记已读/归档/批量操作)
- 客户端实时流:`use-notification-stream`SSE+ `use-desktop-notifications`(浏览器桌面通知)
- 表:`message_notifications`(站内通知本体)、`notification_preferences`(偏好)、`notification_logs`(发送日志)
- **ARCHITECTURE.md 契约**:✅ `notifications(userId)`§9.1 line 645B1 共享)
- portal-shell 已有 `GET_NOTIFICATIONS_LIST_DOC` GraphQL document`universal.graphql.ts`),但形状待 P1-7 修正(后端返回平铺数组,前端按 `{items, total}` 解析)
- `useNotifications` hook 已就绪(`lib/api/universal.ts`),可直接复用
- **多角色复用**:教师 / 学生 / 家长 / 管理员(全角色,权限点复用 `MESSAGE_READ`,无独立 `NOTIFICATION_READ`
- **优先级****B1**§9.1 line 645
---
### 2.2 `/shell/settings`(设置)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\settings\page.tsx` + `error.tsx` + `loading.tsx`
- **功能描述**
- 个人资料编辑表单(姓名/性别/年龄/手机/地址)+ 头像上传
- 通知偏好表单(`smsEnabled` / `emailEnabled` / `pushEnabled` + `homeworkNotifications` / `gradeNotifications` 等业务开关)
- 安全中心入口密码修改、最近登录记录、2FA、当前会话 User-Agent 标记)
- **角色差异化**:通过 `resolveRoleSettingsConfig(roles)` 注入 `descriptionKey` / `backHref` / `generalExtra`(如管理员额外展示学校信息/品牌配置/AI 提供商配置卡片)
- 子页 `/settings/security`:独立密码修改页 + 安全提示
- **技术栈**
- Server Component`requireAuth`,不要求特定权限点)+ `SettingsServiceProvider`Context 注入 `SettingsService` 接口)
- Server Actions`actions-service.ts``updateProfileAction` / `updateNotificationPreferencesAction` / `actions-avatar.ts` / `actions-password.ts` / `actions-security.ts` / `actions-brand.ts` / `actions-system-settings.ts`
- 数据源:`getUserProfile`users 模块)+ `getNotificationPreferences`notifications 模块)
- 角色配置:`modules/settings/config/role-settings-config.tsx`(按角色注入额外卡片)
- 客户端组件:`settings-view` / `profile-settings-form` / `notification-preferences-form` / `password-change-form` / `security-center-card` / `avatar-upload` / `theme-preferences-card` 等 22 个组件
- 工具:`totp.ts`2FA`security-utils.ts`(密码强度校验)
- **ARCHITECTURE.md 契约**§9.1 line 646B1 共享,契约字段未细化)
- portal-shell 当前无 settings 模块、无 `/shell/settings` 路由、无 `navigation.ts` 入口、无 `route-permissions.ts` 登记
- **多角色复用**:教师 / 学生 / 家长 / 管理员(全角色,按 `roles` 动态注入角色专属卡片)
- **优先级****B1**§9.1 line 646
---
### 2.3 `/shell/settings/security`(安全中心子页)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\settings\security\page.tsx` + `error.tsx` + `loading.tsx`
- **功能描述**
- 独立的密码修改页(`PasswordChangeForm`+ 安全提示卡片
-`settings` 主页的"安全中心卡片"互补,提供专注的改密体验
- **技术栈**
- `requireAuth` + `PasswordChangeForm`(客户端组件,调 `updatePasswordAction`
- 复用 `modules/settings` 模块
- **ARCHITECTURE.md 契约**:🟡 未显式列出,但属于 `settings` 共享子页§9.1 隐含)
- **多角色复用**:全角色
- **优先级**B1跟随 settings
---
### 2.4 `/shell/announcements`(公告列表)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\announcements\page.tsx`
- **功能描述**
- 用户端公告列表(非管理端),分页展示当前用户**可见**的公告school / grade / class 三类,按受众 `gradeIds[]` / `classIds[]` 数组过滤P0-2 多受众修复)
- 支持状态筛选(`published` / `draft` / `archived` / `all`),通过 `searchParams.status` 传递
- `AnnouncementList` + `AnnouncementPagination`(客户端构建分页 URL不传函数 prop
- 点击进入 `/announcements/[id]` 详情
- 管理端在 `/admin/announcements`(不在共享范围)
- **技术栈**
- Server Component + `requirePermission(ANNOUNCEMENT_READ)` + `AnnouncementsServiceProvider`Context
- `announcements/data-access.ts`603 行server-only`getUserAnnouncementsPageData(userId, dataScope, page, pageSize)` 受众过滤 + 分页
- `isAnnouncementVisibleToAudience` 纯函数(单测覆盖三类矩阵)
- 表:`announcements` + `announcement_reads`(已读回执独立表)
- 缓存:`cacheFn` + `invalidateFor` 双路径失效(列表 + 详情)
- **ARCHITECTURE.md 契约**§9.2 line 668"列表"契约未就绪B3
- portal-shell 的 `announcements-widget` 已下线P1-2数据源迁到 `adminDashboard` 聚合,**用户端列表契约完全缺失**
- **多角色复用**:教师 / 学生 / 家长(查看自己受众范围内的公告);管理员走 `/shell/admin/announcements` 管理端,不走共享列表
- **优先级****B3**§9.2 line 668
---
### 2.5 `/shell/announcements/[id]`(公告详情)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\announcements\[id]\page.tsx`
- **功能描述**
- 公告详情页,展示标题/内容/类型/发布时间/作者
- **P0-1 越权修复**`getAnnouncementByIdForUser(id, userId, dataScope)` 在 data-access 层结合受众与 status 过滤,不可见时统一返回 404不暴露公告存在性
- `canManage={false}`(用户端只读,管理操作在 `/admin/announcements/[id]/edit`
- 进入详情后可触发已读回执 `markAnnouncementAsReadAction`
- **技术栈**
- Server Component + `requirePermission(ANNOUNCEMENT_READ)` + `notFound()` 守卫
- `getAnnouncementByIdForUser` 受众过滤查询
- `AnnouncementDetail` 客户端组件 + `AnnouncementsServiceProvider`
- **ARCHITECTURE.md 契约**:🟡 `announcement(id)` 已存在但形状/可见性语义待对齐§9.2 line 668B3
- **多角色复用**:教师 / 学生 / 家长
- **优先级****B3**§9.2 line 668
---
### 2.6 `/shell/messages`(消息列表)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\messages\page.tsx` + `error.tsx` + `loading.tsx`
- **功能描述**
- **CICD 把"消息列表"和"通知列表"合并展示在同一页**(消息在上、通知在下,各自独立 `Suspense` + `SectionErrorBoundary`,局部失败互不影响)
- 消息区:`MessageListSection`(按 `dataScope.type === "class_taught"` 决定是否显示"群发"按钮,即教师可群发全班家长)
- 通知区:`NotificationListSection`(调 `getNotifications(userId, {page:1, pageSize:20})`
- 子页 `compose`:撰写新消息 + 草稿列表(`getRecipients` + `getMessageDrafts`
- 子页 `[id]`:消息详情(`getMessageDetailPageData`,含撤回、星标、举报、附件)
- 子页 `group-compose`教师群发fan-out on write
- **技术栈**
- Server Component + `requirePermission(MESSAGE_READ)` + 双 `Suspense` 流式加载
- `messaging/data-access.ts`barrel拆分为 core/bulk/group/templates/reports 5 子文件)
- 表:`messages` / `message_drafts` / `message_templates` / `message_reports` / `user_blocks`
- Server Actions`sendMessageAction` / `recallMessageAction`2 分钟窗口服务端校验,不乐观更新)/ `sendGroupMessageAction`fan-out on write/ `reportMessageAction` / `blockUserAction`
- 乐观更新:`useOptimistic` 仅用于星标切换
- 收件人二次校验P0-1`isReceiverAllowed` 防 UI 绕过越权
- 通知集成:消息发送后调 `notifications/dispatcher.sendNotification` 多通道分发in-app/sms/email/wechat尊重偏好
- **ARCHITECTURE.md 契约**§9.2 line 669B3 末,契约完全缺失)
- portal-shell 当前无 `messaging` 模块、无 `/shell/messages` 路由
- msg 子图仅暴露 `notifications(userId)`,未暴露 messages 相关查询
- **多角色复用**:教师(可群发)/ 学生 / 家长 / 管理员(全角色私信,权限点 `MESSAGE_READ` / `MESSAGE_SEND`
- **优先级****B3 末**§9.2 line 669
---
### 2.7 `/shell/messages/compose` / `/shell/messages/[id]`(消息子页)
- **CICD 参考**
- `e:\desktop\CICD\src\app\(dashboard)\messages\compose\page.tsx` + `loading.tsx`
- `e:\desktop\CICD\src\app\(dashboard)\messages\[id]\page.tsx` + `loading.tsx`
- `e:\desktop\CICD\src\app\(dashboard)\messages\group-compose\page.tsx` + `loading.tsx`
- **功能描述**
- `compose`:撰写表单(收件人选择 + 主题 + 内容 + 附件)+ 草稿列表(`MessageDraftList` 支持恢复编辑/删除)+ 快捷模板选择器(`message-template-picker`
- `[id]`消息详情含撤回2 分钟窗口)、星标、举报、屏蔽、附件预览
- `group-compose`:教师选择班级 → fan-out 群发全班家长(共享 `groupMessageId`
- **技术栈**:同 §2.6
- **ARCHITECTURE.md 契约**§9.2 line 669 隐含B3 末)
- **多角色复用**:教师(含群发)/ 学生 / 家长 / 管理员
- **优先级**B3 末
---
### 2.8 `/shell/profile`(个人资料)
- **CICD 参考**`e:\desktop\CICD\src\app\(dashboard)\profile\page.tsx` + `error.tsx` + `loading.tsx`
- **功能描述**
- 个人资料展示页(只读,编辑入口跳转到 `/settings`
- 头像上传区(`AvatarUpload`,独立 `SectionErrorBoundary`
- 个人信息卡:姓名 / 性别 / 年龄 / 手机 / 地址
- 账户信息卡:邮箱 / 角色 Badge / 注册时间 / Onboarded 时间
- **角色差异化概览**`resolveProfileOverviewType(roles)` 决定底部展示 `ProfileStudentOverview`(学生学业概览)或 `ProfileTeacherOverview`(教师概览),均带 `Suspense` 骨架屏
- **技术栈**
- Server Component + `requireAuth`(无特定权限点)+ `getUserProfile(userId)` + `redirect("/login")` 守卫
- 客户端组件:`AvatarUpload` / `ProfileStudentOverview` / `ProfileTeacherOverview`
- 角色配置:`modules/settings/config/profile-overview-config.ts`
- UI`PageHeader` + `Card` + `Badge` + `Button` + `lucide-react` 图标
- **ARCHITECTURE.md 契约**:🟡 未在 §9 显式列出独立 `/shell/profile` 行,但 §9.5 计数 ~7 页共享 + CICD 有 `profile/page.tsx`,推断属于共享路由隐含项
- portal-shell 当前无 `/shell/profile` 路由、无 settings 模块profile 组件归属 settings 模块)
- **多角色复用**:教师 / 学生 / 家长 / 管理员(全角色,按 `resolveProfileOverviewType` 切换底部概览)
- **优先级**B1/B3跟随 settings建议 B1 一并实现)
---
### 2.9 /register注册页
- **CICD 参考实现**`e:\desktop\CICD\src\app\(auth)\register\page.tsx`RSC13 行,渲染 `<RegisterForm registerAction={registerAction} />`);布局 `e:\desktop\CICD\src\app\(auth)\layout.tsx` 注入品牌配置(`getBrandConfig()`,失败回退默认)
- **功能描述**:账号注册页。表单字段:姓名 / 邮箱 / 密码 / 出生年月日 / 邀请码(可选,提供时覆盖默认 student 角色)。未成年人保护:根据 `birthDate` 计算 `age``< 18` 触发监护人信息区(监护人姓名 / 电话 / 关系下拉)+ 监护人同意复选框。必勾"同意《隐私政策》与《用户协议》"复选框(链接到 `/privacy``/terms``target="_blank"` 新窗打开)。注册成功 → `router.push("/login")` 跳登录页
- **技术栈**RSC + `RegisterForm` 客户端组件(`modules/auth/components/register-form.tsx`+ `registerAction` Server Action`modules/auth/actions`+ `next-intl``useTranslations("auth.register")`+ shadcn UIButton / Input / Label / Checkbox / Select+ `notify` toast + `lucide-react` Loader2
- **ARCHITECTURE.md 契约**§9.5 line 723 缺口新增B2/B5 末 N。portal-shell 当前无 `/register` 路由、无 auth 模块(仅有 `api/auth/login` + `api/auth/logout` 两个 Route Handler注册走 iam 后端但前端页面与 Server Action 完全缺失
- **多角色复用**:公开页(未登录用户),不涉及角色权限
- **优先级**B5 末
---
### 2.10 /privacy隐私政策
- **CICD 参考实现**`e:\desktop\CICD\src\app\(auth)\privacy\page.tsx`RSC纯静态内容
- **功能描述**隐私政策页K12 教育场景7 个章节:引言 / 信息收集说明(账户 / 身份 / 未成年人保护 / 学习数据 / 设备)/ 信息使用说明 / 信息保护措施bcrypt + AES + RBAC + DataScope + HTTPS/ 用户权利(查询 / 更正 / 删除 / 撤回同意 / 可携带)/ Cookie 政策 / 未成年人保护条款(未满 14 周岁须监护人陪同注册)/ 联系方式。底部"返回注册"链接到 `/register`
- **技术栈**RSC + `next/link` + shadcn UICard / CardHeader / CardTitle / CardDescription / CardContent+ `Metadata`SEO+ 纯静态文案(无 DB / 无 Server Action / 无客户端交互)
- **ARCHITECTURE.md 契约**§9.5 line 723 缺口新增B2/B5 末 N。portal-shell 当前无 `/privacy` 路由;属纯静态页,无后端契约依赖,仅需补页面 + i18n 文案
- **多角色复用**:公开页(未登录用户,注册页链接到它)
- **优先级**B5 末
---
### 2.11 /terms服务条款
- **CICD 参考实现**`e:\desktop\CICD\src\app\(auth)\terms\page.tsx`RSC纯静态内容
- **功能描述**用户服务协议页7 个章节:引言 / 服务说明 / 用户注册(真实邮箱 + 未成年人监护人陪同 + 同意隐私政策与本协议)/ 用户行为规范 / 知识产权 / 免责声明AI 生成内容仅供参考,可能存在偏差)/ 服务变更中断与终止 / 法律适用与争议解决(中华人民共和国法律)。底部"返回注册"链接到 `/register`
- **技术栈**:同 §2.10RSC + next/link + shadcn Card + Metadata + 纯静态文案,无 DB / 无 Server Action
- **ARCHITECTURE.md 契约**§9.5 line 723 缺口新增B2/B5 末 N。portal-shell 当前无 `/terms` 路由;纯静态页,无后端契约依赖
- **多角色复用**:公开页(未登录用户,注册页链接到它)
- **优先级**B5 末
---
### 2.12 /onboarding首次登录引导
- **CICD 参考实现**
- 页面:`e:\desktop\CICD\src\app\(onboarding)\onboarding\page.tsx`RSC + auth guard
- 路由组 loading`e:\desktop\CICD\src\app\(onboarding)\loading.tsx`(骨架屏,匹配 stepper 卡片布局)
- 模块:`e:\desktop\CICD\src\modules\onboarding\`data-access / actions / schema / components / hooks / types
- ⚠️ `e:\desktop\CICD\src\app\api\onboarding\status\route.ts`**@deprecated**(返回 410 Gone已迁移到 Server Action `getOnboardingStatusAction`,保留仅为兼容性指示)
- 注:`(onboarding)` 路由组下有 `loading.tsx`(组根级骨架屏)+ `onboarding/page.tsx`(子目录页),并非"无 page.tsx"
- **功能描述**:首次登录引导页。`getAuthContext()` 鉴权(未登录捕获 `PermissionDeniedError``redirect("/login")`)→ 读 DB `users.onboardedAt` 判断是否需要引导(`getOnboardingStatus`DB 为唯一真相源,废弃 `session.user.onboarded` 冗余检查)→ 不需要则 `redirect("/dashboard")` → 需要则渲染 `OnboardingStepper``Suspense` 包裹,支持 `useSearchParams` URL query 持久化步骤)。按角色差异化步骤:
- 通用:更新基础资料(姓名 / 电话 / 住址)
- 学生:通过邀请码绑定班级(`enrollStudentByInvitationCode`
- 教师:通过邀请码绑定任课科目(`enrollTeacherByInvitationCode`P0-3 多选循环绑定,修复 UI 多选但服务端只取第一个的 bug
- 家长:三因子验证绑定子女(邮箱 + 生日 YYYY-MM-DD + 手机后 4 位,组合空间 365×10000=3.65M 防枚举;支持多子女循环绑定 P1-4
- **技术栈**
- RSC + `getAuthContext()``PermissionDeniedError` → redirect /login+ `Suspense` + `OnboardingStepper` 客户端组件
- data-access`modules/onboarding/data-access.ts`server-only`getOnboardingStatus``cacheFn` 300s`users.onboardedAt` + `usersToRoles` + `roles`/ `getUserOnboardedAt`P0-5 幂等检查)/ `markUserOnboarded` / `updateUserProfile` / `bindParentToChild`(三因子验证 + 幂等)
- Server Actions`modules/onboarding/actions.ts``getOnboardingStatusAction` + `completeOnboardingAction`Zod `OnboardingSchema` 校验 / P0-5 服务端幂等已完成直接返回成功 / P1-2 局部失败收集不回滚整事务 / P0-4 审计日志 `onboarding.complete` / audit-P1-8 家长绑定速率限制每小时 5 次防三因子枚举 / `invalidateFor("onboarding.complete")` 缓存失效)
- 跨模块:`modules/classes/data-access``enrollStudentByInvitationCode` / `enrollTeacherByInvitationCode`,含校验)
- 表:`users.onboardedAt` / `usersToRoles` / `roles` / `parentStudentRelations`
- **ARCHITECTURE.md 契约**§9.5 line 723 缺口新增B2/B5 末 N。portal-shell 当前无 `/onboarding` 路由、无 onboarding 模块iam 子图已有 `userProfile`,但 onboarding 状态查询(`onboardedAt`)与家长绑定子女(`parentStudentRelations`)契约需补;班级邀请码绑定依赖 classes 服务
- **多角色复用**:所有新注册的已登录用户(按角色差异化步骤:学生绑定班级码 / 教师绑定任课科目 / 家长绑定子女);管理员一般预分配 onboarded跳过引导
- **优先级**B5 末
---
## 三、按角色复用矩阵
| 共享页面 | 教师 | 学生 | 家长 | 管理员 | 备注 |
| ------------------------------- | ---- | ---- | ---- | ------ | ------------------------------------------------ |
| `/shell/notifications` | ✅ | ✅ | ✅ | ✅ | 全角色,复用 `MESSAGE_READ` 权限点 |
| `/shell/settings` | ✅ | ✅ | ✅ | ✅ | 全角色,`resolveRoleSettingsConfig` 注入角色卡片 |
| `/shell/settings/security` | ✅ | ✅ | ✅ | ✅ | 全角色,密码修改 + 安全提示 |
| `/shell/announcements` | ✅ | ✅ | ✅ | ⚠️ | 管理员走 `/shell/admin/announcements` 管理端 |
| `/shell/announcements/[id]` | ✅ | ✅ | ✅ | ⚠️ | 同上,管理员走管理端 |
| `/shell/messages` | ✅ | ✅ | ✅ | ✅ | 全角色私信;教师额外有群发入口 |
| `/shell/messages/compose` | ✅ | ✅ | ✅ | ✅ | 全角色可发起私信 |
| `/shell/messages/[id]` | ✅ | ✅ | ✅ | ✅ | 全角色可查看自己参与的消息线程 |
| `/shell/messages/group-compose` | ✅ | ❌ | ❌ | ⚠️ | 仅教师(按所教班级)+ 管理员 |
| `/shell/profile` | ✅ | ✅ | ✅ | ✅ | 全角色,底部概览按角色切换 |
---
## 四、契约就绪情况ARCHITECTURE.md §9.5
| 共享页 | 契约状态 | 后端子图 | 备注 |
| ---------------------------------- | -------- | --------- | --------------------------------------------------------------------------------- |
| `/shell/notifications` | ✅ | msg | `notifications(userId)` 已就绪;形状待 P1-7 修正(平铺数组 vs `{items,total}` |
| `/shell/settings` | ✅ | iam / msg | `userProfile` + `notificationPreferences` 已就绪 |
| `/shell/settings/security` | ✅ | iam | 密码修改走 iam2FA 走 iam |
| `/shell/announcements` | ❌ | content | 列表契约缺失,需 content 子图补 `announcements(userScope)` 查询 |
| `/shell/announcements/[id]` | 🟡 | content | `announcement(id)` 存在,可见性语义需对齐 |
| `/shell/messages` | ❌ | msg | messages 查询契约完全缺失,需 msg 子图补 `messages(userId)` / `messageThread(id)` |
| `/shell/messages/compose` / `[id]` | ❌ | msg | 同上,依赖 messages 契约 |
| `/shell/profile` | ✅ | iam | `userProfile` 已就绪;角色概览查询待补(学生/教师) |
> 节奏原则§9.5):契约就绪页先行;❌ 页用 MSW 先上 UI契约工单跟踪后端补齐后切换真实查询。
---
## 五、portal-shell 现状盘点(已具备 / 待补齐)
### 5.1 已具备(可复用)
| 资产 | 路径 | 说明 |
| ----------------- | -------------------------------------------------- | ---------------------------------- |
| 通知铃铛 widget | `widgets/topbar/notification-bell/index.tsx` | 顶栏下拉,调 `useNotificationBell` |
| 通知列表 widget | `widgets/universal/notifications-widget/index.tsx` | 仪表盘内嵌,调 `useNotifications` |
| 通知 GraphQL 文档 | `lib/api/operations/universal.graphql.ts` | `GET_NOTIFICATIONS_LIST_DOC` |
| 通知 hook | `lib/api/universal.ts``useNotifications` | 形状待 P1-7 修正 |
| 顶栏通知 hook | `lib/api/topbar.ts``useNotificationBell` | 用于铃铛 |
| 403 页 | `app/shell/forbidden/page.tsx` | ✅ 已就绪 |
| 角色仪表盘分发 | `app/shell/page.tsx` | ✅ 已就绪(按角色 redirect |
### 5.2 待补齐(缺失)
| 缺失项 | 影响 |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `app/shell/notifications/page.tsx` + `error.tsx` + `loading.tsx` | 通知中心整页 |
| `app/shell/settings/page.tsx` + `error.tsx` + `loading.tsx` | 设置主页 |
| `app/shell/settings/security/page.tsx` + `error.tsx` + `loading.tsx` | 安全中心子页 |
| `app/shell/announcements/page.tsx` + `error.tsx` + `loading.tsx` | 公告列表 |
| `app/shell/announcements/[id]/page.tsx` + `error.tsx` + `loading.tsx` | 公告详情 |
| `app/shell/messages/page.tsx` + `error.tsx` + `loading.tsx` | 消息列表(含通知区) |
| `app/shell/messages/compose/page.tsx` + `loading.tsx` | 撰写消息 |
| `app/shell/messages/[id]/page.tsx` + `loading.tsx` | 消息详情 |
| `app/shell/messages/group-compose/page.tsx` + `loading.tsx` | 教师群发 |
| `app/shell/profile/page.tsx` + `error.tsx` + `loading.tsx` | 个人资料 |
| `modules/notifications/` 整模块 | 通知 data-access / actions / dispatcher / channels / hooks |
| `modules/messaging/` 整模块 | 消息 data-access / actions / components / hooks |
| `modules/announcements/` 整模块(用户端) | 公告 data-access / actions / components |
| `modules/settings/` 整模块 | 设置 components / actions / config / lib |
| `shared/lib/navigation.ts` 入口登记 | 5 个共享路由均未登记到 `NAVIGATION_ITEMS` |
| `shared/lib/route-permissions.ts` 权限登记 | 5 个共享路由均未登记到 `EXACT_ROUTE_PERMISSIONS` / `PREFIX_ROUTE_PERMISSIONS`,按 fail-closed 规则会被默认拒绝 |
| i18n key`messages/zh-CN.json` / `en.json` | `notifications.*` / `settings.*` / `announcements.*` / `messages.*` / `settings.profile.*` 文案键缺失 |
---
## 六、实施建议(按批次与优先级)
### 6.1 B1 优先(契约就绪,可直接接真实查询)
1. **`/shell/notifications`**:复用现有 `useNotifications` hook新增 page + error + loading同步登记 `EXACT_ROUTE_PERMISSIONS``requiredPermissions: ["MESSAGE_READ"]`,不限角色)+ `navigation.ts`(建议放在顶栏 / 通用区分组,但因 `NAVIGATION_ITEMS` 按角色 group需考虑通用入口的 group 归属——或只在顶栏铃铛加"查看全部"链接跳到 `/shell/notifications`
2. **`/shell/settings`** + **`/shell/settings/security`** + **`/shell/profile`**三者强关联profile 复用 settings 模块的 `AvatarUpload` / 角色概览组件),建议一次性补齐;需新建 `modules/settings/` 模块22+ 客户端组件 + Server Actions
### 6.2 B3契约缺失先用 MSW 上 UI
1. **`/shell/announcements`** + **`/shell/announcements/[id]`**:列表契约缺失,需先用 MSW mock `announcements(userScope)` 查询;`announcement(id)` 已存在但可见性语义需对齐
2. **`/shell/messages`** + 子页messages 契约完全缺失,需 msg 子图补 `messages(userId)` / `messageThread(id)` / `sendMessage` mutation 等;建议 B3 末实施
### 6.3 强制同步项(每次新增路由必做)
- `shared/lib/route-permissions.ts`:新增 `EXACT_ROUTE_PERMISSIONS` 条目(否则 fail-closed 拒绝)
- `shared/lib/navigation.ts`:如需侧边栏入口,新增 `NAVIGATION_ITEMS` 条目
- i18n`messages/zh-CN.json` + `messages/en.json` 补对应文案键
- `pnpm run arch:scan` 更新 arch.db按 project_rules §1 强制)
- 若架构设计意图变化,同步更新 `ARCHITECTURE.md` §9
---
## 七、关键风险与约束
1. **fail-closed 风险**`route-permissions.ts` §5 规定 `/shell/**` 下未登记路由默认拒绝。若只加 page.tsx 不登记权限,访问会被 middleware 重定向到 `/shell/forbidden`
2. **通知形状待修正**`notifications(userId)` 后端返回平铺数组,前端按 `{items, total}` 解析P1-7 待修。B1 实施时需同步修正 `universal.graphql.ts``universal.ts` 的类型,或后端补齐 `{items, total}` 包装。
3. **消息撤回不乐观更新**CICD 的 `recallMessageAction` 必须服务端校验 2 分钟窗口(`MESSAGE_RECALL_WINDOW_MS = 2 * 60 * 1000`),禁止 `useOptimistic` 乐观更新会导致超时撤回误成功。B3 实施时必须遵循此约束。
4. **公告可见性越权P0-1**`getAnnouncementByIdForUser` 必须在 data-access 层结合受众 + status 过滤,不可见时统一返回 404不暴露存在性。B3 实施时必须复刻此守卫。
5. **消息收件人二次校验P0-1**Server Action 必须调 `isReceiverAllowed` 校验 `receiverId` 在 sender 的 `DataScope` 内,防 UI 绕过越权。
6. **群发 fan-out on write**:群发时每条收件人写独立行,共享同一 `groupMessageId`,避免读时聚合。
7. **多 AI 模块边界**:按 project_rules §14.2,通知/消息/公告/设置分属不同模块,需协调 AI 分工;`shared-proto` 契约变更由协调 AI 负责。
8. **profile 归属 settings 模块**CICD 中 `profile` 页复用 `modules/settings/components/avatar-upload``profile-{student,teacher}-overview`,实施时不能跳过 settings 模块直接做 profile。
---
## 八、根路径重定向说明(隐含覆盖页)
| 路径 | 实现位置 | 行为 | CICD 对照 |
| --------- | ------------------------------------ | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `/`(根) | `apps/portal-shell/src/app/page.tsx` | `redirect("/shell")` → 再由 `/shell` catch-all 按角色 redirect 到 `/shell/{role}` | CICD `src/app/page.tsx` 直接 `redirect("/dashboard")`(教师仪表盘,无角色分发层) |
> 说明portal-shell 比 CICD 多一层"角色分发"——根 `/` → `/shell` → `/shell/{role}`,因为 portal-shell 是多角色统一入口(教师 / 学生 / 家长 / 管理员共用一个 app而 CICD 默认进入教师仪表盘。此页属"隐含覆盖"(§一 总览未单列),已在 §一 `/shell` 行标注 CICD 参考路径。portal-shell `src/app/page.tsx` 当前已就绪(`redirect("/shell")`portal-shell spec §8.2 路由前缀 `/shell/*`),无需补齐。

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 中无源页面,需重点设计。**

View File

@@ -0,0 +1,579 @@
# 教师域Teacher待完成功能分析
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\teacher\`53 个 page.tsx
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\teacher\`53 个 page.tsx
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.1(教师域 56 页B1+B2
> 分析日期2026-07-24
> 分析方式:逐页面对比 CICD 实现 vs portal-shell 实现,逐个读取 page.tsx + features/*-client.tsx
---
## 〇、方法论与对比基线
1. CICD 单体Next.js App Router + Server Actions + Drizzle作为**功能基线**,反映"老版单体已实现"的教师功能完整态。
2. portal-shell 作为**目标态**,遵循 ARCH §9.1 的契约与批次规划B1+B2+B2末
3. portal-shell 普遍采用「Server Component page.tsx 仅做 `<Suspense>` 边界包裹 + Client Component `*-client.tsx` 承载业务」的统一架构CICD 多为「Server Component 直接拉数据 + Rich Client Component 渲染」。
4. ARCH §9.1 规划 56 页portal-shell 已实现 53 页,**3 页缺口** = `exams/[id]/proctoring`B2末+ `notifications`B1 共享)+ `settings`B1 共享)。
5. CICD 中 `exams/page.tsx``classes/page.tsx``exams/grading/*``exams/create` 等为**重定向/等价路径**,不算独立功能缺口。
6. CICD 中 `diagnostic/student/[studentId]` 在 ARCH §9.1 中**未规划**ARCH 仅规划 `diagnostic` + `diagnostic/class/[classId]` 共 2 页),属 CICD 历史功能portal-shell 缺失但**不违反 ARCH**。
---
## 一、页面完成度总览
| 状态 | 数量 | 说明 |
| --------------------- | ---- | ---------------------------------------------------------------------------------------------------- |
| ✅ 已完成且功能完整 | 33 | 功能与 CICD 持平或为 ARCH 新建且实现完整(含 MSW 兜底契约页) |
| 🟡 已完成但功能不完整 | 14 | 页面存在但子功能/技术栈与 CICD 有差距 |
| ❌ 完全缺失 | 4 | `exams/[id]/proctoring``diagnostic/student/[studentId]``/shell/notifications``/shell/settings` |
### ✅ 已完成且功能完整33 页)
| 模块 | 页面 | 备注 |
| ---------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| dashboard | `/shell/teacher`teacher/page.tsx | 仪表盘4 StatCard + 班级概况 + 预警(注:标题用硬编码中文,违反 i18n但功能完整 |
| exams | `/shell/teacher/exams/[id]/analytics` | 汇总卡片 + 分数段分布图 + 每题正确率 + 学生排名,混合契约 ✅ `assignmentAnalysis` |
| homework | `/shell/teacher/homework/submissions/[submissionId]` | 单份提交批改,含题目分数/教师评语/AI 建议应用/上下导航/总反馈 |
| homework | `/shell/teacher/homework/submissions/[submissionId]/scan-grading` | 扫描批改 |
| homework | `/shell/teacher/homework/submissions` | 提交列表 |
| homework | `/shell/teacher/homework/assignments/[id]/submissions` | 作业子提交列表 |
| homework | `/shell/teacher/homework` | 作业列表 |
| grades | `/shell/teacher/grades` | 成绩列表 |
| grades | `/shell/teacher/grades/analytics` | 成绩分析,汇总 + 分布 + 排名,混合契约 |
| grades | `/shell/teacher/grades/stats` | 成绩统计 |
| grades | `/shell/teacher/grades/report-card` | 报告卡 |
| attendance | `/shell/teacher/attendance` | 考勤列表 |
| attendance | `/shell/teacher/attendance/report` | 考勤报告 |
| attendance | `/shell/teacher/attendance/stats` | 考勤统计 |
| classes | `/shell/teacher/classes` | 班级列表(含 gradeId/subjectId/q 筛选) |
| classes | `/shell/teacher/classes/schedule` | 班级课表 |
| students | (见 🟡 学生列表,因缺科目成绩) | — |
| course-plans | `/shell/teacher/course-plans``/[id]` | 2 页 |
| elective | `/shell/teacher/elective``/create``/[id]/edit` | 3 页 |
| error-book | `/shell/teacher/error-book` | ✅ `errorBookItems/Stats` 真实契约 |
| diagnostic | `/shell/teacher/diagnostic` | 诊断列表 |
| analytics | `/shell/teacher/analytics``/[studentId]` | ARCH 新建 2 页MSW 兜底 |
| ai | `/shell/teacher/ai/ai-assist``/ai-lesson-plan``/ai-report` | ARCH 新建 B2末 3 页,全 MSW 兜底,工作台布局完整 |
| knowledge-graph | `/shell/teacher/knowledge-graph` | ARCH 新建 B2末三栏工作台`knowledgePoint(id)` ✅ 真实 + 列表 MSW |
| questions | `/shell/teacher/questions` | 题库列表,🟡 `question(id)` |
| textbooks | `/shell/teacher/textbooks``/[id]` | 2 页,🟡 `textbook(id)` |
| lesson-plans | `/shell/teacher/lesson-plans``/new``/calendar``/heatmap``/library` | 5 页edit 见 🟡) |
| practice | `/shell/teacher/practice` | 练习 |
| leave | `/shell/teacher/leave` | 请假 |
| schedule-changes | `/shell/teacher/schedule-changes` | 调课 |
### 🟡 已完成但功能不完整14 页)
详见 §三。
### ❌ 完全缺失4 页)
详见 §二。
---
## 二、缺失页面清单
### 2.1 `/shell/teacher/exams/[id]/proctoring`(监考工作台)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\teacher\exams\[id]\proctoring\page.tsx`
- **功能描述**:实时监考面板,包含 `ProctoringDashboard` 组件,并行拉取:
- `getExamForProctoring(id)` — 考试基础信息
- `getExamProctoringSummary(id)` — 监考汇总(应到/实到/异常数)
- `getStudentProctoringStatuses(id)` — 学生实时状态列表
- `getRecentProctoringEvents(id, 20)` — 最近 20 条监考事件
- **技术栈**Server Component + `requirePermission(Permissions.EXAM_PROCTOR)` + `ProctoringDashboard` 客户端组件(实时 WS 推送)
- **ARCHITECTURE.md 契约**schema 无,需 WS 契约工单 + MSW 先行)
- **批次**B2 末二期WS
- **建议**:✅ 必须补做。需先定 WS 协议契约push-gateway再写 MSW mock最后实现 `ProctoringDashboard` 客户端组件。这是 B2末 明确规划页。
### 2.2 `/shell/teacher/diagnostic/student/[studentId]`(学生诊断详情)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\teacher\diagnostic\student\[studentId]\page.tsx`
- **功能描述**:单个学生的学情诊断页,包含:
- `StudentDiagnosticView` 组件,渲染 `MasteryRadarPoint` 雷达图(学生 vs 班级平均对比)
- `getStudentMasterySummary(studentId)` — 学生掌握度汇总
- `getDiagnosticReports({ studentId }, ctx.dataScope)` — 诊断报告列表(教师可看草稿)
- `getKnowledgePointStats(studentClassId)` — 班级平均掌握度(雷达图对比)
- DataScope 二次校验:`class_taught` scope 验师生关系,`class_members`/`children` scope 限制访问
- **技术栈**Server Component + `requirePermission(Permissions.DIAGNOSTIC_READ)` + `WidgetBoundary` + `StudentDiagnosticView` 客户端组件
- **ARCHITECTURE.md 契约**:🟡(`diagnosticReports` ✅,但单生维度查询未明确)— **注意ARCH §9.1 仅规划 2 页(`/diagnostic` + `/diagnostic/class/[classId]`),未含学生维度**
- **批次**B2CICD 历史功能ARCH 未规划)
- **建议**:⚠️ 建议补做。虽 ARCH §9.1 未明确规划,但 CICD 已有完整实现且属于教师核心诊断场景(查看单个学生的掌握度雷达图 + 班级对比)。补做时需同步更新 ARCH §9.1 + 004。
### 2.3 `/shell/notifications`通知中心B1 共享)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\teacher\notifications\page.tsx`(教师角色入口)
- **功能描述**:通知中心,按 `userId` 拉取通知列表
- **技术栈**Server Component + `notifications(userId)` 查询
- **ARCHITECTURE.md 契约**:✅ `notifications(userId)` schema 已就绪
- **批次**B1共享所有角色共用
- **建议**:✅ 必须补做。B1 共享页schema 已就绪,缺页面入口。需在 `/shell/notifications/page.tsx` 实现,供教师/学生/家长/管理员共用。
### 2.4 `/shell/settings`设置B1 共享)
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\teacher\settings\page.tsx`
- **功能描述**:用户设置页(个人资料、偏好、安全等)
- **技术栈**Server Component
- **ARCHITECTURE.md 契约**:✅
- **批次**B1共享
- **建议**:✅ 必须补做。B1 共享页,需在 `/shell/settings/page.tsx` 实现。
### 2.5 不算缺失的"伪缺失"(路径合并/重定向)
以下 CICD 路径在 portal-shell 中通过路径合并或等价入口覆盖,**不算缺失**
| CICD 路径 | portal-shell 等价 | 说明 |
| --------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `exams/page.tsx` | `/exams/page.tsx`(直接列表) | CICD 是 redirect 到 `/exams/all`portal-shell 直接渲染列表 |
| `exams/all/page.tsx` | `/exams/page.tsx`ExamsListClient | 同上,功能等价(但筛选/计数/分页有差距,见 §3.1 |
| `exams/create/page.tsx` | `/exams/new/page.tsx`NewExamClient | CICD 有 `/create`ExamForm 简单表单)+ `/new`ExamRichForm 富文本portal-shell 合并为 `/new`(简单表单),富文本编辑只在 `/[id]/edit`(见 §3.4 |
| `exams/grading/page.tsx` | 无CICD 自身是 redirect | CICD `grading/page.tsx``redirect("/teacher/homework/submissions")`,无实际功能 |
| `exams/grading/[submissionId]/page.tsx` | 无CICD 自身是 redirect | 同上,仅 redirect |
| `classes/page.tsx` | `/classes/page.tsx`ClassesListClient | CICD 是 redirect 到 `/classes/my`portal-shell 直接渲染列表 |
| `classes/my/page.tsx` | `/classes/page.tsx`ClassesListClient | 路径合并MyClassesGrid 改为 ClassesTable视觉差异功能等价 |
| `classes/my/[id]/page.tsx` | `/classes/[id]/page.tsx`ClassDetailClient | 路径合并,但功能差距大(见 §3.5 |
| `classes/students/page.tsx` | `/students/page.tsx`StudentsListClient | 路径从 `/classes/students` 迁到顶级 `/students`ARCH §9.1 规划),功能差距见 §3.6 |
---
## 三、功能不完整的页面
### 3.1 exams - 考试列表页 `/shell/teacher/exams`
- **路径**`/shell/teacher/exams`
- **portal-shell 实现**`exams/page.tsx` + `features/teacher/exams/exams-list-client.tsx`
- **CICD 实现**`exams/all/page.tsx` + `exam-filters.tsx` + `ExamDataTable` + `ExamFilters`
- **已有功能**
- 关键词搜索q、状态筛选status、客户端二次筛选
- 简单表格展示title/status/examDate/duration/totalScore/actions
- 状态徽章、新建按钮、EmptyState、ListPageShell 骨架
- ✅ 真实查询 `useExams(classId)`classId 默认 `cls-001`MSW 兜底)
- **缺失功能**
-**难度筛选**difficulty filter— CICD 有 difficulty 维度筛选
-**状态计数徽章**draft/published/archived 三个 Badge + 数字)— CICD 顶部统计栏
-**真正的分页控件** — portal-shell 仅显示 `total: N`无分页器CICD `ExamDataTable` 内置分页
-**高级 DataTable 组件** — CICD 用 `ExamDataTable`(含排序/列控制/分页portal-shell 是手写 `<table>`
-**EmptyState "清除筛选" action** — CICD 在有筛选时显示"清除筛选"按钮回到 `/exams/all`portal-shell 无
-**服务端筛选** — CICD 在 Server Component 内调 `getExams({ q, status, difficulty, scope })`portal-shell 是客户端二次过滤
-**DataScope 过滤** — CICD 用 `ctx.dataScope` 做权限过滤portal-shell 无
- **技术差距**
- CICDServer Component + Drizzle `getExams` + `ExamDataTable` + `ExamFilters` 组件
- portal-shell 需补:分页器组件、状态计数 Badge 行、difficulty 筛选 select、EmptyState clearFilters action、后续契约就绪后改服务端筛选
### 3.2 exams - 考试详情页 `/shell/teacher/exams/[id]`
- **路径**`/shell/teacher/exams/[id]`
- **portal-shell 实现**`exams/[id]/page.tsx` + `features/teacher/exams/exam-detail-client.tsx`
- **CICD 实现****CICD 无此页面**CICD `/exams/[id]/` 目录下只有 analytics/build/edit-rich/proctoring 子路由,无 `page.tsx`
- **已有功能**
- ✅ 真实查询 `useExam(examId)`schema 已就绪)
- 基本信息区title/description/examDate/duration/totalScore/classId/subjectId
- 状态信息区currentStatus/statusChangedAt/statusChangedBy/createdBy/createdAt/updatedAt
- 编辑按钮status 可编辑时显示,但 onClick 无路由跳转,**按钮无实际行为**
- **缺失功能**
- ⚠️ **编辑按钮未接路由**`<Button variant="outline">{t("detail.edit")}</Button>``asChild` / `href` / `onClick`,点击无反应,应跳 `/shell/teacher/exams/[id]/edit`
- 💡 **无提交列表预览** — CICD 在 homework 详情有提交列表exam 详情可考虑加 submissions 摘要CICD 无此页,属增量)
- 💡 **无分析入口卡片** — 可加"查看分析"快捷入口跳 `/analytics`
- **技术差距**
- 编辑按钮需补 `asChild` + `<Link href=".../edit">`
- ARCH §9.1 规划此页为 M 详情页portal-shell 已满足基本契约,但交互不完整
### 3.3 exams - 新建考试表单 `/shell/teacher/exams/new`
- **路径**`/shell/teacher/exams/new`
- **portal-shell 实现**`exams/new/page.tsx` + `features/teacher/exams/new-exam-client.tsx`
- **CICD 实现**`exams/new/page.tsx`ExamRichForm 富文本)+ `exams/create/page.tsx`ExamForm 简单)
- **已有功能**
- 简单表单classId/subjectId/title/description/examDate/duration/totalScore
- 内联校验classId/title/examDate 必填)
- ✅ mutation `useCreateExam` MSW 兜底
- 成功后 `router.push` 回列表
- **缺失功能**
-**富文本编辑器** — CICD `/exams/new``ExamRichForm`(含 Tiptap 富文本 + 题目结构portal-shell 是纯字段表单
-**班级/科目下拉选择器** — portal-shell 用 `<input type="text">`CICD 应为 selectCICD ExamRichForm 内部)
- 💡 **题目结构编辑** — CICD 富文本编辑器支持插入题目占位、分值、排序
- **技术差距**
- CICD`ExamRichForm` 客户端组件Tiptap + 结构化节点)
- portal-shell 把 CICD 的 `/exams/new`rich+ `/exams/create`basic合并为 `/exams/new`basic富文本能力下沉到 `/[id]/edit`
- **是否补做**:取决于产品决策。若新建时即需富文本,需补 Tiptap若新建只创建元数据、富文本在编辑页做则当前实现可接受
### 3.4 exams - 富文本试卷编辑 `/shell/teacher/exams/[id]/edit`
- **路径**`/shell/teacher/exams/[id]/edit`(对应 CICD `/exams/[id]/edit-rich`
- **portal-shell 实现**`exams/[id]/edit/page.tsx` + `features/teacher/exams/exam-edit-client.tsx`
- **CICD 实现**`exams/[id]/edit-rich/page.tsx` + `ExamRichForm` + `examNodesToEditorDoc` + `structureToEditorDoc`
- **已有功能**
- 三栏工作台:中栏富文本编辑器 + 右栏属性面板examId/totalScore/questionCount/updatedAt
- 工具栏B/I/U/H1/H2/H3/列表/P
- `useExamRichEditor` + `useSaveExamRichContent` MSW 兜底
- 保存为 HTML`editorRef.current.innerHTML`
- **缺失功能**
-**真正的 Tiptap 编辑器** — portal-shell 用 `contentEditable` + `document.execCommand`**deprecated**CICD 用 Tiptap JSON 结构化文档
-**题目结构加载** — CICD 在 Server Component 内调 `getExamById` + `getQuestions`,把 `exam.structure` + `questions` 转换为 `EditorDoc``examNodesToEditorDoc` + `structureToEditorDoc`portal-shell 完全无此逻辑
-**题目占位符插入** — CICD 富文本支持插入题目节点(含 order/options/contentportal-shell 仅纯文本 HTML
-**服务端初始数据预加载** — CICD Server Component 预加载 exam + questionsportal-shell 纯客户端 MSW
-**编辑模式 (mode="edit")** — CICD `ExamRichForm` 支持 create/edit 双模式portal-shell 仅 edit
- ⚠️ **dangerouslySetInnerHTML** — portal-shell 用 `dangerouslySetInnerHTML` 注入 HTML违反 §4 安全规范(如必须使用,先用 DOMPurify 清洗)
- **技术差距**
- CICDServer Component + Tiptap + `examNodesToEditorDoc`/`structureToEditorDoc` 转换层 + `ExamRichForm`
- portal-shell 需补:引入 Tiptap或 Lexical、编写 structure→EditorDoc 转换、Server Component 预加载、题目节点类型、DOMPurify 清洗
### 3.5 classes - 班级详情页 `/shell/teacher/classes/[id]`
- **路径**`/shell/teacher/classes/[id]`(对应 CICD `/classes/my/[id]`
- **portal-shell 实现**`classes/[id]/page.tsx` + `features/teacher/classes/class-detail-client.tsx`
- **CICD 实现**`classes/my/[id]/page.tsx` + 5 个 widget 组件
- **已有功能**
- ✅ 真实查询 `useClassInfo(classId)`schema 已就绪)
- 基本信息区name/gradeId/headTeacherId/description/createdAt/updatedAt
- 学生名单区studentNo/name/enrolledAtMSW 兜底)
- 任课老师区name/subjectName/roleMSW 兜底)
- **缺失功能**
-**ClassHeader** — CICD 有独立头部组件classId/name/grade/homeroom/room/schoolName/studentCount
-**ClassOverviewStats 概览统计** — 平均分、提交率、待批改数papersToGrade、逾期数overdueCount
-**ClassTrendsWidget 趋势 widget** — 作业趋势列表id/title/status/subject/isActive/isOverdue/dueAt/submittedCount/targetCount/avgScore/medianScore
-**ClassScheduleWidget 课表 widget** — 班级课表展示
-**ClassAssignmentsWidget 作业 widget** — 班级作业列表
-**学生科目成绩展示** — CICD 通过 `getClassStudentSubjectScoresV2` 拉取每个学生的科目成绩portal-shell 学生表只有 studentNo/name/enrolledAt
-**三栏布局** — CICD 是 2/3 主区 + 1/3 侧栏portal-shell 是单栏 DetailSection 堆叠
-**服务端并行数据预加载** — CICD `Promise.all([getClassHomeworkInsights, getClassStudents, getClassSchedule, getClassStudentSubjectScoresV2])`
- **技术差距**
- CICDServer Component + 4 个并行 data-access 查询 + 5 个 widget 组件 + `SectionErrorBoundary`
- portal-shell 需补5 个 widget 组件、4 个 data-access 查询(含 MSW 兜底)、三栏布局、概览统计计算逻辑
### 3.6 students - 学生列表页 `/shell/teacher/students`
- **路径**`/shell/teacher/students`(对应 CICD `/classes/students`
- **portal-shell 实现**`students/page.tsx` + `features/teacher/students/students-list-client.tsx`
- **CICD 实现**`classes/students/page.tsx` + `StudentsFilters` + `StudentsTable` + `students-filters.tsx`
- **已有功能**
- 关键词搜索q、classId/gradeId 筛选(**纯 text input**
- 表格studentNo/name/gender/className/gradeId/enrolledAt
- 性别徽章、EmptyState、ListPageShell
- **缺失功能**
-**班级下拉选择器** — CICD 用 `StudentsFilters` 组件(含 `<select>` 班级列表portal-shell 用 `<input type="text">` 让用户手输 classId
-**默认班级选择逻辑** — CICD 默认选中教师第一个班级(`defaultClassId = classes[0].id`portal-shell 无默认
-**学生科目成绩展示** — CICD 调 `getStudentsSubjectScores(studentIds)` 拉取每个学生科目成绩,`StudentsTable` 有科目成绩列portal-shell 无
-**服务端学生列表预加载** — CICD Server Component 调 `getClassStudents` + `getStudentsSubjectScores`portal-shell 纯客户端 MSW
-**状态筛选** — CICD 有 status 维度筛选active/inactive 等portal-shell 无
- **技术差距**
- CICDServer Component + `getTeacherClasses` + `getClassStudents` + `getStudentsSubjectScores` + `StudentsFilters`/`StudentsTable` 组件
- portal-shell 需补:班级 select 组件、默认班级逻辑、科目成绩列、status 筛选
### 3.7 homework - 作业详情页 `/shell/teacher/homework/[id]`
- **路径**`/shell/teacher/homework/[id]`(对应 CICD `/homework/assignments/[id]`
- **portal-shell 实现**`homework/[id]/page.tsx` + `features/teacher/homework/homework-detail-client.tsx`
- **CICD 实现**`homework/assignments/[id]/page.tsx` + `HomeworkAssignmentQuestionErrorOverviewCard` + `HomeworkAssignmentExamContentCard`
- **已有功能**
- ✅ 真实查询 `useHomework(homeworkId)`schema 已就绪)
- 基本信息区title/description/dueDate/gracePeriod/status/classId/subjectId/createdBy/updatedAt
- 提交列表区studentName/studentNo/status/submittedAt/totalScore/maxScore/actionsMSW 兜底)
- 内联批改表单studentId/score/feedbackMSW 兜底)
- 跳转"查看所有提交"按钮 → `/homework/assignments/[id]/submissions`
- **缺失功能**
-**头部统计行** — CICD 头部有 dueDate/targetCount/submissionCount/gradedCount 四个统计指标portal-shell 无
-**HomeworkAssignmentQuestionErrorOverviewCard 题目错误概览卡片** — CICD 用 `getHomeworkAssignmentAnalytics(id)` 拉取每题错误率,渲染分析卡片(带 Suspense + SectionErrorBoundary
-**HomeworkAssignmentExamContentCard 作业内容卡片** — CICD 渲染 `assignment.structure` + `questions`(含 gradedSampleCountportal-shell 无作业内容展示
-**服务端 analytics 预加载** — CICD 调 `getHomeworkAssignmentAnalytics(id)` 返回 `{ assignment, questions, gradedSampleCount }`
-**流式渲染** — CICD 用 `<Suspense fallback={<AnalyticsSkeleton />}>` 包裹 analytics 卡片portal-shell 无
- **技术差距**
- CICDServer Component + `getHomeworkAssignmentAnalytics` + 2 个分析卡片组件 + `SectionErrorBoundary` + Suspense 流式
- portal-shell 需补题目错误概览卡片、作业内容卡片structure + questions 渲染、头部统计、analytics 查询
### 3.8 homework - 布置作业表单 `/shell/teacher/homework/new`
- **路径**`/shell/teacher/homework/new`(对应 CICD `/homework/assignments/create`
- **portal-shell 实现**`homework/new/page.tsx` + `features/teacher/homework/new-homework-client.tsx`
- **CICD 实现**`homework/assignments/create/page.tsx` + `HomeworkAssignmentForm`
- **已有功能**
- 简单表单classId/subjectId/title/description/dueDate/gracePeriod
- 内联校验、MSW 兜底 mutation `useAssignHomework`
- 成功后回列表
- **缺失功能**
-**关联考试选择** — CICD Server Component 调 `getExams({ scope })` 拉取考试列表,传给 `HomeworkAssignmentForm` 作下拉选项作业可基于试卷结构生成portal-shell 无 examId 字段
-**班级下拉选择器** — CICD 调 `getTeacherClasses()` 拉取班级portal-shell 用 `<input type="text">` 手输 classId
-**EmptyState when no classes** — CICD 在 `classes.length === 0` 时显示 EmptyState 提示"无可用班级,去班级管理"portal-shell 无
-**HomeworkAssignmentForm 富表单** — CICD 用专门组件(含 exam 关联、班级 select、可能含题目结构选择
- **技术差距**
- CICDServer Component + `getExams` + `getTeacherClasses` + `HomeworkAssignmentForm` 组件 + EmptyState
- portal-shell 需补examId 字段 + 考试下拉、班级下拉、无班级 EmptyState
### 3.9 lesson-plans - 教案编辑工作台 `/shell/teacher/lesson-plans/[planId]/edit`
- **路径**`/shell/teacher/lesson-plans/[planId]/edit`
- **portal-shell 实现**`lesson-plans/[planId]/edit/page.tsx` + `features/teacher/lesson-plans/lesson-plan-edit-client.tsx`
- **CICD 实现**`lesson-plans/[planId]/edit/page.tsx` + `LessonPlanEditor` + `LessonPlanProviderSetup` + `AiClientProvider`
- **已有功能**
- 三栏工作台左栏大纲树LessonPlanOutlineTree+ 中栏富文本编辑器 + 右栏属性面板id/grade/subject/duration/status/updatedAt/resources
- 工具栏B/I/U/H1/H2/H3/列表/P
- `useLessonPlan` + `useUpdateLessonPlan` MSW 兜底
- 保存为 HTML
- **缺失功能**
-**真正的 Tiptap 编辑器** — portal-shell 用 `contentEditable` + `document.execCommand`**deprecated**CICD 用 `LessonPlanEditor`Tiptap
-**AI 集成** — CICD 包 `<AiClientProvider service={createCoreAiClientService()}>`,支持 AI 辅助生成/润色portal-shell 无
-**教材/章节上下文** — CICD Server Component 调 `getTextbookById` + `getChaptersByTextbookId` + `findChapterById`,把 textbookTitle/chapterTitle 传给编辑器工具栏显示portal-shell 无
-**LessonPlanProviderSetup** — CICD 有专门的 Provider 设置组件注入编辑器所需上下文portal-shell 无
-**服务端初始数据预加载** — CICD Server Component 预加载 plan + teacherClasses + textbook + chapterportal-shell 纯客户端 MSW
-**班级关联** — CICD 加载 `getTeacherClasses({ teacherId })` 传给编辑器教案可关联班级portal-shell 无
- ⚠️ **dangerouslySetInnerHTML** — 同 §3.4,违反 §4 安全规范
- **技术差距**
- CICDServer Component + Tiptap + AiClientProvider + LessonPlanProviderSetup + 4 个 data-access 查询
- portal-shell 需补Tiptap、AiClientProvider 接入、教材/章节加载、Provider 设置、班级关联、DOMPurify 清洗
### 3.10 grades - 成绩录入页 `/shell/teacher/grades/entry`
- **路径**`/shell/teacher/grades/entry`
- **portal-shell 实现**`grades/entry/page.tsx` + `features/teacher/grades/grade-entry-client.tsx`
- **CICD 实现**`grades/entry/page.tsx` + `BatchGradeEntryByExam` 组件
- **已有功能**
- **单学生单次录入**表单classId/studentId/examId/homeworkId/score/totalScore/feedback
- 内联校验、MSW 兜底 mutation `useCreateGrade`
- **缺失功能**
-**批量录入** — CICD 用 `BatchGradeEntryByExam` 组件,按考试批量录入全班成绩(一次选 exam + class列出所有学生逐题/逐生录入portal-shell 是单生单次
-**考试下拉选择器** — CICD 调 `getExamsForGradeEntry(ctx.dataScope)` 拉取可录入成绩的考试列表portal-shell 用 `<input type="text">` 手输 examId
-**班级下拉选择器** — CICD 调 `getTeacherClasses()` + scope 过滤 + `getClassGradeIdsByClassIds` 构建 classGradeMap用于按试卷年级过滤班级portal-shell 手输 classId
-**学生列表预加载** — CICD 调 `getClassStudentsForEntry(classId, ctx.dataScope)` 拉取班级学生portal-shell 手输 studentId
-**考试题目列表** — CICD 调 `getExamForGradeEntry(examId, ctx.dataScope)` 返回试卷详情含题目,支持按题录入
-**DataScope 过滤** — CICD 用 `ctx.dataScope.type === "class_taught"` 过滤班级portal-shell 无权限过滤
-**EmptyState when no exams** — CICD 在 `exams.length === 0` 时显示 EmptyState
-**按试卷年级过滤班级** — CICD 用 classGradeMap 客户端过滤
- **技术差距**
- CICDServer Component + 4 个 data-access 查询 + `BatchGradeEntryByExam` 客户端组件 + DataScope
- portal-shell 需补:批量录入组件、考试/班级下拉、学生列表、题目列表、scope 过滤、EmptyState
### 3.11 attendance - 考勤点名表 `/shell/teacher/attendance/sheet`
- **路径**`/shell/teacher/attendance/sheet`
- **portal-shell 实现**`attendance/sheet/page.tsx` + `features/teacher/attendance/attendance-sheet-client.tsx`
- **CICD 实现**`attendance/sheet/page.tsx` + `AttendanceSheet` 组件
- **已有功能**
- classId**text input**+ date 选择
- 学生表格studentName/status 按钮组/remark
- 4 种状态PRESENT/ABSENT/LATE/LEAVE点击切换
- MSW 兜底 `useAttendanceSheet` + `useSaveAttendanceSheet`
- 保存后回列表
- **缺失功能**
-**班级下拉选择器** — CICD Server Component 调 `getTeacherClasses()` 拉取班级,传给 `AttendanceSheet` 组件作下拉portal-shell 用 `<input type="text">` 手输 classId
-**服务端学生列表预加载** — CICD 调 `getClassStudentsForAttendance(defaultClassId)` 服务端预加载学生portal-shell 完全依赖 MSW mock
-**AttendanceSheet 组件** — CICD 有专门的客户端组件含班级切换、日期选择、批量标记、统计汇总portal-shell 是手写表格
- **技术差距**
- CICDServer Component + `getTeacherClasses` + `getClassStudentsForAttendance` + `AttendanceSheet` 客户端组件
- portal-shell 需补:班级 select、服务端学生预加载、AttendanceSheet 组件
### 3.12 diagnostic - 班级诊断详情 `/shell/teacher/diagnostic/class/[classId]`
- **路径**`/shell/teacher/diagnostic/class/[classId]`
- **portal-shell 实现**`diagnostic/class/[classId]/page.tsx` + `features/teacher/diagnostic/diagnostic-class-detail-client.tsx`
- **CICD 实现**`diagnostic/class/[classId]/page.tsx` + `ClassDiagnosticView` + `DiagnosticServiceProvider` + `DiagnosticMonitorProvider`
- **已有功能**
- 基本信息区title/reportType/status/className/studentCount/avgScore/summary/generatedAt
- 薄弱知识点表格kpTitle/mastery/errorCount
- 教学建议列表recommendations
- MSW 兜底 `useDiagnosticReport(classId)`
- **缺失功能**
-**ClassDiagnosticView 富视图组件** — CICD 用专门的 `ClassDiagnosticView`(含掌握度雷达图、知识点 drill-down、学生分布等丰富交互portal-shell 是基础详情页
-**DiagnosticServiceProvider** — CICD 注入诊断服务上下文(支持客户端 drill-down 查询portal-shell 无
-**DiagnosticMonitorProvider + 监控埋点** — CICD 用 `createMonitoredDiagnosticService(defaultDiagnosticService, noopDiagnosticMonitor)` 包装服务支持生产环境监控注入portal-shell 无
-**服务端数据预加载** — CICD Server Component 调 `getClassMasterySummary(classId)` 预加载portal-shell 纯客户端 MSW
-**DataScope 校验** — CICD 校验 `class_taught` scope教师只能查所教班级拒绝 `class_members`/`children` scopeportal-shell 无
-**WidgetBoundary** — CICD 用 `WidgetBoundary` 包裹局部降级portal-shell 用 DetailPageShell
- **技术差距**
- CICDServer Component + `getClassMasterySummary` + `ClassDiagnosticView` + Diagnostic Service Provider/Monitor + DataScope
- portal-shell 需补ClassDiagnosticView 组件、DiagnosticServiceProvider/MonitorProvider、服务端预加载、DataScope 校验、WidgetBoundary
### 3.13 dashboard - 教师仪表盘 `/shell/teacher`teacher/page.tsx
- **路径**`/shell/teacher`(根教师页)
- **portal-shell 实现**`shell/teacher/page.tsx`"use client"
- **CICD 实现**`dashboard/page.tsx`Server Component+ `TeacherDashboardView`
- **已有功能**
- 4 StatCardtotal_classes/total_students/class_avg_score/pending_homework_count
- 班级概况区class_name/student_count/average_score
- 近期预警区target_name/severity/warning_type/current_value/threshold
- ✅ 真实查询 `useTeacherDashboard`data-ana 聚合查询)
- loading/error 三态处理
- **缺失功能**
- ⚠️ **i18n 硬编码** — portal-shell 标题/描述用硬编码中文(`"教师仪表盘"``"今日教学概览"``"班级总数"` 等),违反 §3.10 设计令牌规范的 i18n 要求,应改 `useTranslations("dashboard")`CICD 用 `getTranslations("dashboard")`
-**流式渲染** — CICD Server Component 用 `getTeacherDashboardAction()` 返回未解析 Promise + `<TeacherDashboardView dataPromise={...}>`,视图内 `use()` 消费 + Suspense 流式渲染portal-shell 是客户端整体 loading
-**TeacherDashboardView 富视图** — CICD 用专门的 `TeacherDashboardView` 组件(可能含更多 widget课表/作业/考试/学生预警等portal-shell 仅 4 StatCard + 2 简单列表
-**更多 widget** — CICD 可能含课表、作业待批、考试安排、学生预警等 widget需读 TeacherDashboardView 确认portal-shell 仅基础展示
- **技术差距**
- CICDServer Component + Server Action + Suspense 流式 + `TeacherDashboardView` 组件
- portal-shell 需补i18n 化、TeacherDashboardView 组件、流式渲染、更多 widget待确认
### 3.14 classes - 班级列表页 `/shell/teacher/classes`(轻量不完整)
- **路径**`/shell/teacher/classes`
- **portal-shell 实现**`classes/page.tsx` + `features/teacher/classes/classes-list-client.tsx`
- **CICD 实现**`classes/my/page.tsx` + `MyClassesGrid` 组件
- **已有功能**
- ✅ 真实查询 `useClasses({ gradeId, subjectId, q })`MSW 兜底)
- 三筛选q/gradeId/subjectId、表格name/grade/headTeacher/studentCount/subjectCount/description/updatedAt/actions
- EmptyState、ListPageShell
- **缺失功能**
- ⚠️ **MyClassesGrid 卡片网格视图** — CICD 用 `MyClassesGrid` 组件卡片式展示portal-shell 用表格,视觉差异(功能等价,不算重大缺失)
- ⚠️ **subjectOptions 预加载** — CICD 调 `getClassSubjects()` 拉取科目选项作下拉portal-shell 用 text input
- ⚠️ **SectionErrorBoundary** — CICD 用 `SectionErrorBoundary` 包裹portal-shell 用 errorNode
- **技术差距**:较小,主要是视觉/组件差异,功能基本等价
### 3.15 exams - 试卷组卷工作台 `/shell/teacher/exams/[id]/build`
- **路径**`/shell/teacher/exams/[id]/build`
- **CICD 参考实现**`e:\desktop\CICD\src\app\(dashboard)\teacher\exams\[id]\build\page.tsx` + 同目录 `error.tsx` + `loading.tsx`
- **功能描述**试卷组卷工作台CICD 为 Server Component 入口(`export const dynamic = "force-dynamic"`
- `requirePermission(Permissions.EXAM_READ)` 权限校验,拿到 `ctx.dataScope`
- `getExamById(id, ctx.dataScope)` 拉取考试元数据(含 `questions` + `structure`),不存在则 `notFound()`
- `Promise.all` 并行拉取题库前 20 条(`getQuestions({ pageSize: 20 })`+ 已选题目详情(`getQuestions({ ids: selectedQuestionIds })`
- `questionOptionsById` Map 去重合并 bank + selected 题目为 `questionOptions`
- `normalizeStructure(exam.structure)``exam.structure` 规范化为 `ExamNode[]` 树(支持嵌套分组节点)
- structure 空但 `initialSelected` 非空时回退为扁平节点列表(用 `@paralleldrive/cuid2` `createId()` 生成节点 ID
- `createCoreAiClientService()` 创建 AI 服务,`<AiClientProvider>` 注入 AI 上下文
- 渲染 `<ExamAssembly>` 富组件(位于 `modules/exams/components/exam-assembly`),透传 examId/title/subject/grade/difficulty/totalScore/durationMin + initialSelected + initialStructure + questionOptions
- 配套 `error.tsx``useErrorReport` 错误上报 + `EmptyState` + retry 按钮)+ `loading.tsx`3 栏 Skeleton`lg:grid-cols-3` 2+1 布局)
- **技术栈**Next.js App Router Server Component`force-dynamic`+ next-intl `getTranslations("examHomework.exam.build")` + `requirePermission` + `Permissions.EXAM_READ` + `ctx.dataScope` 数据范围过滤 + Drizzle data-access`getExamById` / `getQuestions`+ `normalizeStructure` 转换层 + `@paralleldrive/cuid2` + AI 集成(`AiClientProvider` + `createCoreAiClientService`+ `ExamAssembly` 客户端组件 + 专用 error/loading 边界
- **ARCHITECTURE.md 契约**B2— portal-shell `page.tsx` 注释明确 `@contract-pending``examBuild(examId)` / `questionsLibrary(filter)` / `saveExamBuild(input)` 三个根字段均不存在,全 MSW 兜底
- **portal-shell 现状**
- `page.tsx``<Suspense fallback={<WorkbenchPageSkeleton />}>` 包裹 `ExamBuildClient`**无服务端预加载、无权限守卫、无 notFound**
- `ExamBuildClient` 纯客户端,使用 `useExamBuild` / `useQuestionsLibrary` / `useSaveExamBuild` 三个 Apollo hook全 MSW 兜底)
- `WorkbenchPageShell` 三栏布局:左 `QuestionsLibraryPanel` / 中 `SelectedQuestionsPanel` / 右 `PreviewPanel`
- 左栏q 关键词 + type 下拉 + difficulty 下拉筛选 + 题目列表 + 添加按钮(`Plus` 图标)+ 总数显示
- 中栏:已选题目列表,支持上移/下移(`↑`/`↓`/编辑分值(`Input type="number"`/移除(`Trash2`
- 右栏:总分(含 baseline 差值 ± 提示amber/destructive 配色)+ 及格分 + 题型分布(`countByType`
- 本地 `useState<ExamBuildNode[] | null>` 管理 selected`useEffect` 在 data 首次就绪时初始化(`sortBySortOrder`
- 保存 mutation `useSaveExamBuild` + `notify.success/error` 反馈
- 三态合并到 `errorNode`error + empty 共用降级节点)
- **无专用 error.tsx / loading.tsx 文件**
- **缺失功能**
-**Server Component 数据预加载** — CICD 在服务端并行 `Promise.all` 拉取 exam + bank questions + selected questionsportal-shell 完全依赖客户端 MSW
-**DataScope 权限过滤** — CICD 用 `ctx.dataScope` 过滤考试可见性portal-shell 无
-**`requirePermission(Permissions.EXAM_READ)` 权限守卫** — portal-shell 无
-**`notFound()` 404 处理** — CICD 考试不存在时调 `notFound()`portal-shell 显示空态节点
-**AI 集成** — CICD 包 `<AiClientProvider service={createCoreAiClientService()}>` 支持 AI 辅助组卷portal-shell 无 AI 接入(与 §3.9 教案编辑同类型缺失,见 §六 共性技术债 6
-**`normalizeStructure` 转换层** — CICD 把 `exam.structure` 规范化为 `ExamNode[]`支持嵌套分组节点portal-shell 用扁平 `ExamBuildNode[]`,无结构化分组能力
-**structure 空时回退逻辑** — CICD 在 structure 空但 `initialSelected` 非空时回退为扁平节点列表cuid2 生成 IDportal-shell 无此兼容
-**`ExamAssembly` 富组件** — CICD 用专门的 `ExamAssembly` 组件(`modules/exams/components/exam-assembly`portal-shell 是手写 3 个 Panel 函数
-**questionOptions 去重合并** — CICD 把 bank + selected 题目通过 Map 去重合并为 `questionOptions` 传给组件portal-shell 左右栏分别查询、无合并
-**专用 error.tsx** — CICD 有 `error.tsx``useErrorReport` 上报 + `EmptyState` + retry 按钮portal-shell 用 `WorkbenchPageShell.errorNode` 整页降级,无错误上报
-**专用 loading.tsx** — CICD 有 3 栏 Skeleton`lg:grid-cols-3` 2+1 布局portal-shell 用通用 `WorkbenchPageSkeleton`
-**`dynamic = "force-dynamic"`** — CICD 强制动态渲染权限相关数据不能静态化portal-shell 默认
- ⚠️ **i18n 命名空间差异** — CICD 用 `examHomework.exam.build`portal-shell 用 `exams`,需对齐
- ⚠️ **exam 元数据透传** — CICD 把 subject/grade/difficulty/durationMin 传给 `ExamAssembly` 展示portal-shell 不展示这些上下文
- **建议**
1. **短期(契约未就绪)**:保留 MSW 兜底;补 `error.tsx``useErrorReport` + EmptyState + retry+ `loading.tsx`3 栏 Skeletoni18n 命名空间与 CICD 对齐(或保留 `exams` 但与 ARCH §11 一致)
2. **中期(契约就绪后)**
- 改造为 Server Component并行预加载 exam + questionsbank + selected
- 引入 `requirePermission` + `ctx.dataScope` 过滤
- 实现 `normalizeStructure` 转换层,支持嵌套 `ExamNode` 树(与 CICD 对齐)
- 引入 `AiClientProvider` + `createCoreAiClientService`,接入 AI 辅助组卷
- 抽取 `ExamAssembly` 富组件,统一 3 栏 UI 与 CICD 对齐
- questionOptions 去重合并,减少客户端重复查询
3. **长期**:与 §3.4exams/[id]/edit 富文本)+ §3.9lesson-plans edit统一引入 Tiptap + AI 集成方案(见 §六 共性技术债 1/2/6
---
## 四、按模块汇总
| 模块 | ARCH 规划 | portal-shell 已实现 | ✅ 完整 | 🟡 不完整 | ❌ 缺失 | 备注 |
| ---------------- | --------- | ------------------- | -------------- | --------------------- | --------------- | ----------------------------------------------------------------------------------- |
| dashboard | 1 | 1 | 0 | 1§3.13 | 0 | i18n 硬编码 + 缺流式 + 缺富视图 |
| exams | 7 | 6 | 1analytics | 4§3.1/3.2/3.3/3.4 | 1proctoring | build 列入🟡但功能基本完整,缺 AI |
| homework | 7 | 7 | 5 | 2§3.7/3.8 | 0 | 详情缺分析卡片,新建缺考试关联 |
| grades | 5 | 5 | 4 | 1§3.10 | 0 | entry 缺批量录入 |
| lesson-plans | 6 | 6 | 5 | 1§3.9 | 0 | edit 缺 Tiptap + AI + 教材章节 |
| questions | 1 | 1 | 1 | 0 | 0 | — |
| textbooks | 2 | 2 | 2 | 0 | 0 | — |
| attendance | 4 | 4 | 3 | 1§3.11 | 0 | sheet 缺班级下拉 + 学生预加载 |
| classes | 3 | 3 | 1schedule | 2§3.5/3.14 | 0 | [id] 缺 5 个 widgetlist 轻量差异 |
| students | 1 | 1 | 0 | 1§3.6 | 0 | 缺下拉 + 科目成绩 |
| course-plans | 2 | 2 | 2 | 0 | 0 | — |
| elective | 3 | 3 | 3 | 0 | 0 | — |
| error-book | 1 | 1 | 1 | 0 | 0 | — |
| diagnostic | 2 | 2 | 1list | 1§3.12 | 0 | class/[classId] 缺富视图 + 服务上下文 |
| analytics | 2 | 2 | 2 | 0 | 0 | ARCH 新建MSW 兜底 |
| ai | 3 | 3 | 3 | 0 | 0 | ARCH 新建 B2末全 MSW |
| knowledge-graph | 1 | 1 | 1 | 0 | 0 | ARCH 新建 B2末 |
| practice | 1 | 1 | 1 | 0 | 0 | — |
| schedule-changes | 1 | 1 | 1 | 0 | 0 | — |
| leave | 1 | 1 | 1 | 0 | 0 | — |
| notifications | 1共享 | 0 | 0 | 0 | 1§2.3 | B1 共享schema ✅ |
| settings | 1共享 | 0 | 0 | 0 | 1§2.4 | B1 共享 |
| **合计** | **56** | **53** | **33** | **14** | **4** | 另CICD 历史功能 `diagnostic/student/[studentId]` 未在 ARCH 规划建议补做§2.2 |
---
## 五、优先级建议
### P0B1 共享必须补schema 已就绪)
1. `/shell/notifications` — B1 共享,`notifications(userId)`
2. `/shell/settings` — B1 共享
### P1B2末 规划页,必须补)
3. `/shell/teacher/exams/[id]/proctoring` — B2末WS 二期,需先定 WS 契约
### P2CICD 历史功能,建议补,需同步更新 ARCH §9.1
4. `/shell/teacher/diagnostic/student/[studentId]` — 学生诊断雷达图CICD 已有完整实现
### P3功能不完整页按业务优先级补
**高优先级(核心教学场景,差距大)**5. `classes/[id]` 班级详情 — 缺 5 个 widget概览/趋势/课表/作业/学生科目成绩班级是教师核心工作台6. `homework/[id]` 作业详情 — 缺分析卡片 + 内容卡片,作业是高频场景 7. `grades/entry` 成绩录入 — 缺批量录入,单生录入效率太低 8. `lesson-plans/[planId]/edit` 教案编辑 — 缺 Tiptap + AIcontentEditable deprecated 不可长期维护
**中优先级(交互不完整,影响体验)**9. `students` 学生列表 — 缺下拉 + 科目成绩 10. `homework/new` 布置作业 — 缺考试关联 + 班级下拉 11. `attendance/sheet` 考勤点名 — 缺班级下拉 + 学生预加载 12. `diagnostic/class/[classId]` 班级诊断 — 缺富视图 + 服务上下文 13. `exams/[id]/edit` 富文本编辑 — 缺 Tiptap + 题目结构(与 §3.9 教案编辑同类型问题,建议统一升级 Tiptap14. `exams` 列表 — 缺筛选/计数/分页/DataTable
**低优先级(功能基本可用,细节差距)**15. `exams/[id]` 详情 — 编辑按钮未接路由小修16. `exams/new` 新建考试 — 是否补富文本取决于产品决策 17. `classes` 列表 — 视觉差异,功能等价 18. `dashboard` 仪表盘 — i18n 化 + 流式渲染i18n 应立即修,违反 §3.10
---
## 六、共性技术债
1. **Tiptap 缺失**`exams/[id]/edit``lesson-plans/[planId]/edit` 均用 `contentEditable` + `document.execCommand`deprecatedCICD 用 Tiptap。建议统一引入 Tiptap或 Lexical编写 structure→EditorDoc 转换层。
2. **dangerouslySetInnerHTML 违规**:上述两个富文本页直接 `dangerouslySetInnerHTML`,违反 §4 安全规范,需 DOMPurify 清洗。
3. **下拉选择器缺失**`students`/`homework/new`/`grades/entry`/`attendance/sheet` 等多处用 `<input type="text">` 替代 `<select>`,需补 data-access 查询 + select 组件。
4. **DataScope 校验缺失**portal-shell 普遍无 `ctx.dataScope` 权限过滤CICD 在 Server Component 内做 scope 过滤),需在后端契约就绪后补。
5. **Server Component 预加载缺失**CICD 多个详情页在 Server Component 内并行预加载多个数据源portal-shell 纯客户端 MSW等契约就绪后需改 Server Component 预加载 + Suspense 流式。
6. **AI 集成缺失**`exams/[id]/build``lesson-plans/[planId]/edit` 在 CICD 有 `AiClientProvider` + `createCoreAiClientService`portal-shell 无 AI 接入。
7. **i18n 硬编码**`dashboard` 用硬编码中文,应全面 `useTranslations`
8. **SectionErrorBoundary / WidgetBoundary 缺失**CICD 多处用 `SectionErrorBoundary`/`WidgetBoundary` 做局部降级portal-shell 用 errorNode 整页降级,粒度较粗。
---
## 七、参考文件索引
### CICD 参考实现
- `e:\desktop\CICD\src\app\(dashboard)\teacher\` — 教师域 53 页源
- `e:\desktop\CICD\src\modules\exams\components\exam-rich-form.tsx` — 富文本试卷表单
- `e:\desktop\CICD\src\modules\exams\components\exam-assembly.tsx` — 组卷工作台
- `e:\desktop\CICD\src\modules\classes\components\class-detail\` — 班级详情 5 个 widget
- `e:\desktop\CICD\src\modules\classes\components\my-classes-grid.tsx` — 班级网格
- `e:\desktop\CICD\src\modules\classes\components\students-table.tsx` — 学生表
- `e:\desktop\CICD\src\modules\homework\components\homework-assignment-question-error-overview-card.tsx` — 作业错误概览
- `e:\desktop\CICD\src\modules\homework\components\homework-assignment-exam-content-card.tsx` — 作业内容卡片
- `e:\desktop\CICD\src\modules\grades\components\batch-grade-entry.tsx` — 批量录入
- `e:\desktop\CICD\src\modules\lesson-preparation\components\lesson-plan-editor.tsx` — 教案编辑器
- `e:\desktop\CICD\src\modules\diagnostic\components\class-diagnostic-view.tsx` — 班级诊断视图
- `e:\desktop\CICD\src\modules\diagnostic\components\student-diagnostic-view.tsx` — 学生诊断视图
- `e:\desktop\CICD\src\modules\proctoring\components\proctoring-dashboard.tsx` — 监考面板
- `e:\desktop\CICD\src\modules\dashboard\components\teacher-dashboard\teacher-dashboard-view.tsx` — 仪表盘视图
### portal-shell 当前实现
- `e:\Desktop\Edu\apps\portal-shell\src\app\shell\teacher\` — 教师域 53 页
- `e:\Desktop\Edu\apps\portal-shell\src\features\teacher\` — 51 个 `*-client.tsx` + 辅助组件
- `e:\Desktop\Edu\apps\portal-shell\src\lib\api\` — Apollo hooks含 MSW 兜底)
- `e:\Desktop\Edu\apps\portal-shell\src\shared\components\page-templates\` — ListPageShell/FormPageShell/DetailPageShell/WorkbenchPageShell
- `e:\Desktop\Edu\apps\portal-shell\ARCHITECTURE.md` §9.1 — 教师域 56 页规划
### 契约工单
- `docs/architecture/issues/contracts/core-edu_contract.md` — exams/homework/grades/questions mutation 工单
- `docs/architecture/issues/contracts/classes_contract.md` — classes/students/attendance 工单
- `docs/architecture/issues/contracts/data-ana_contract.md` — diagnostic/analytics 工单
---
**分析完成。下一步建议按 §五 优先级逐项补做,每补一页同步更新 ARCH §9.1 状态 + 004 + arch.db。**

View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -19,10 +19,11 @@ interface Baseline {
categories: Record<string, { pattern: string; min: number; label: string }>; categories: Record<string, { pattern: string; min: number; label: string }>;
} }
// Baseline as of P2 B3 (2026-07-22, error-book + diagnostic + analytics modules added). // Baseline as of B2 末 教师域 (2026-07-24, proctoring + student-diagnostic
// added: 2 new pages).
// Update when adding pages. // Update when adding pages.
const BASELINE: Baseline = { const BASELINE: Baseline = {
total: 58, total: 69,
categories: { categories: {
dashboards: { dashboards: {
pattern: "shell/{admin,teacher,student,parent}/page.tsx", pattern: "shell/{admin,teacher,student,parent}/page.tsx",

View File

@@ -206,7 +206,7 @@ describe("E2E: 三层安全边界", () => {
it("teacher 有 QUESTION_READ → 访问题库放行OR 语义)", () => { it("teacher 有 QUESTION_READ → 访问题库放行OR 语义)", () => {
const result = checkRoutePermission( const result = checkRoutePermission(
"/shell/teacher/question-bank", "/shell/teacher/questions",
TEACHER_USER.bitmap, TEACHER_USER.bitmap,
TEACHER_USER.role, TEACHER_USER.role,
); );
@@ -250,7 +250,7 @@ describe("E2E: 三层安全边界", () => {
it("student 有 ELECTIVE_SELECT → 访问选修课选择放行OR 语义)", () => { it("student 有 ELECTIVE_SELECT → 访问选修课选择放行OR 语义)", () => {
const result = checkRoutePermission( const result = checkRoutePermission(
"/shell/student/electives", "/shell/student/elective",
STUDENT_USER.bitmap, STUDENT_USER.bitmap,
STUDENT_USER.role, STUDENT_USER.role,
); );
@@ -263,7 +263,7 @@ describe("E2E: 三层安全边界", () => {
"DASHBOARD_READ", "DASHBOARD_READ",
]); ]);
const result = checkRoutePermission( const result = checkRoutePermission(
"/shell/student/electives", "/shell/student/elective",
noElective, noElective,
"student", "student",
); );
@@ -313,7 +313,7 @@ describe("E2E: 三层安全边界", () => {
const teacherRoutes = [ const teacherRoutes = [
"/shell/teacher", "/shell/teacher",
"/shell/teacher/lesson-plans", "/shell/teacher/lesson-plans",
"/shell/teacher/question-bank", "/shell/teacher/questions",
]; ];
const results = batchCheckRoutePermission( const results = batchCheckRoutePermission(
teacherRoutes, teacherRoutes,

View File

@@ -0,0 +1,38 @@
"use client";
/**
* AI 配置路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AiSettingsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.aiSettings");
useEffect(() => {
console.error("[portal-shell] ai-settings route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* AI 配置路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AiSettingsLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,24 @@
import { Suspense } from "react";
import { AiSettingsClient } from "@/features/admin/ai-settings/ai-settings-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* AI Provider 配置与用量仪表盘ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 AiSettingsClientclient component中。
*
* 数据契约:
* - aiProviders(scope) ❌ schema 未就绪 → MSW 兜底(@contract-pending
* - aiUsageDashboard(range) ❌ schema 未就绪 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AiSettingsPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<AiSettingsClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 公告编辑路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AnnouncementEditError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.announcements");
useEffect(() => {
console.error("[portal-shell] announcement edit route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 公告编辑路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AnnouncementEditLoading(): React.ReactElement {
return <FormPageSkeleton />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AnnouncementEditClient } from "@/features/admin/announcements/announcement-edit-client";
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 公告编辑表单页ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useParams 要求)。
* 业务逻辑在 AnnouncementEditClientclient component中。
*
* 数据契约adminAnnouncement(id) + updateAnnouncement(id, input) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AnnouncementEditPage(): React.ReactElement {
return (
<Suspense fallback={<FormPageSkeleton />}>
<AnnouncementEditClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 公告详情路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AnnouncementDetailError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.announcements");
useEffect(() => {
console.error("[portal-shell] announcement detail route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 公告详情路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AnnouncementDetailLoading(): React.ReactElement {
return <DetailPageSkeleton />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AnnouncementDetailClient } from "@/features/admin/announcements/announcement-detail-client";
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 公告详情页ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 AnnouncementDetailClientclient component中。
*
* 数据契约adminAnnouncement(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
*/
export default function AnnouncementDetailPage(): React.ReactElement {
return (
<Suspense fallback={<DetailPageSkeleton />}>
<AnnouncementDetailClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 公告路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AnnouncementsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.announcements");
useEffect(() => {
console.error("[portal-shell] announcements route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 公告路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AnnouncementsLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AnnouncementsListClient } from "@/features/admin/announcements/announcements-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 公告管理列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 AnnouncementsListClientclient component中。
*
* 数据契约adminAnnouncements(status) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AnnouncementsListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<AnnouncementsListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,36 @@
"use client";
/**
* 考勤管理路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AdminAttendanceError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.attendance.error");
useEffect(() => {
console.error("[portal-shell] admin attendance route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 考勤管理路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AdminAttendanceLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,23 @@
import { Suspense } from "react";
import { AdminAttendanceClient } from "@/features/admin/attendance/admin-attendance-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 考勤管理页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 AdminAttendanceClientclient component中。
*
* 数据契约adminAttendanceStats / attendanceGradeCorrelation / adminClasses
* ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AdminAttendancePage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<AdminAttendanceClient />
</Suspense>
);
}

View File

@@ -0,0 +1,39 @@
"use client";
/**
* 数据变更日志路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function DataChangesError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.auditLogs.error");
useEffect(() => {
console.error(
"[portal-shell] admin audit-logs/data-changes route error:",
error,
);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 数据变更日志路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function DataChangesLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { DataChangesClient } from "@/features/admin/audit-logs/data-changes-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 数据变更日志页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 DataChangesClientclient component中。
*
* 数据契约dataChangeLogs(filter, pagination) / dataChangeTableOptions / dataChangeStats ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function DataChangesPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<DataChangesClient />
</Suspense>
);
}

View File

@@ -0,0 +1,36 @@
"use client";
/**
* 审计日志路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AuditLogsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.auditLogs.error");
useEffect(() => {
console.error("[portal-shell] admin audit-logs route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 审计日志路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AuditLogsLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,39 @@
"use client";
/**
* 登录日志路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function LoginLogsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.auditLogs.error");
useEffect(() => {
console.error(
"[portal-shell] admin audit-logs/login-logs route error:",
error,
);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 登录日志路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function LoginLogsLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { LoginLogsClient } from "@/features/admin/audit-logs/login-logs-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 登录日志页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 LoginLogsClientclient component中。
*
* 数据契约loginLogs(filter, pagination) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function LoginLogsPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<LoginLogsClient />
</Suspense>
);
}

View File

@@ -0,0 +1,39 @@
"use client";
/**
* 审计概览路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AuditOverviewError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.auditLogs.error");
useEffect(() => {
console.error(
"[portal-shell] admin audit-logs/overview route error:",
error,
);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 审计概览路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AuditOverviewLoading(): React.ReactElement {
return <DetailPageSkeleton />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AuditOverviewClient } from "@/features/admin/audit-logs/audit-overview-client";
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 审计概览页ARCHITECTURE.md §7.3 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 AuditOverviewClientclient component中。
*
* 数据契约auditOverviewStats / auditTrend / dataChangeActionStats ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AuditOverviewPage(): React.ReactElement {
return (
<Suspense fallback={<DetailPageSkeleton />}>
<AuditOverviewClient />
</Suspense>
);
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AuditLogsListClient } from "@/features/admin/audit-logs/audit-logs-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 审计日志列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 AuditLogsListClientclient component中。
*
* 数据契约auditLogs(filter, pagination) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AuditLogsListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<AuditLogsListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 管理端课程计划编辑路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function CoursePlanEditError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.coursePlans");
useEffect(() => {
console.error("[portal-shell] admin course-plan edit route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 管理端课程计划编辑路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function CoursePlanEditLoading(): React.ReactElement {
return <FormPageSkeleton />;
}

View File

@@ -0,0 +1,21 @@
import { Suspense } from "react";
import { CoursePlanEditClient } from "@/features/admin/course-plans/course-plan-edit-client";
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 管理端课程计划编辑表单页ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useParams 要求)。
*
* 数据契约adminCoursePlan(id) + updateCoursePlan(input) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function CoursePlanEditPage(): React.ReactElement {
return (
<Suspense fallback={<FormPageSkeleton />}>
<CoursePlanEditClient />
</Suspense>
);
}

View File

@@ -0,0 +1,41 @@
"use client";
/**
* 管理端课程计划详情路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function CoursePlanDetailError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.coursePlans");
useEffect(() => {
console.error(
"[portal-shell] admin course-plan detail route error:",
error,
);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 管理端课程计划详情路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*
* 子页面(编辑)的 Skeleton 由 server page 的 <Suspense> 兜底,
* 本文件仅在 /shell/admin/course-plans/[id] 期间显示。
*/
export default function CoursePlanDetailLoading(): React.ReactElement {
return <DetailPageSkeleton />;
}

View File

@@ -0,0 +1,21 @@
import { Suspense } from "react";
import { CoursePlanDetailClient } from "@/features/admin/course-plans/course-plan-detail-client";
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 管理端课程计划详情页ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹。
*
* 数据契约adminCoursePlan(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function CoursePlanDetailPage(): React.ReactElement {
return (
<Suspense fallback={<DetailPageSkeleton />}>
<CoursePlanDetailClient />
</Suspense>
);
}

View File

@@ -0,0 +1,41 @@
"use client";
/**
* 管理端课程计划新建路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function CoursePlanCreateError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.coursePlans");
useEffect(() => {
console.error(
"[portal-shell] admin course-plan create route error:",
error,
);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 管理端课程计划新建路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function CoursePlanCreateLoading(): React.ReactElement {
return <FormPageSkeleton />;
}

View File

@@ -0,0 +1,21 @@
import { Suspense } from "react";
import { CoursePlanCreateClient } from "@/features/admin/course-plans/course-plan-create-client";
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 管理端课程计划新建表单页ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹。
*
* 数据契约mutation createCoursePlan(input) ❌ schema 无 Mutation 类型 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function CoursePlanCreatePage(): React.ReactElement {
return (
<Suspense fallback={<FormPageSkeleton />}>
<CoursePlanCreateClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 管理端课程计划路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function CoursePlansError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.coursePlans");
useEffect(() => {
console.error("[portal-shell] admin course-plans route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 管理端课程计划路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*
* 子页面(详情/编辑/新建)的 Skeleton 由各自 server page 的 <Suspense> 兜底,
* 本文件仅在 /shell/admin/course-plans 列表/重定向期间显示。
*/
export default function CoursePlansLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { CoursePlansListClient } from "@/features/admin/course-plans/course-plans-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 管理端课程计划列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 CoursePlansListClientclient component中。
*
* 数据契约adminCoursePlans(status) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function CoursePlansListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<CoursePlansListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 课程地图路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function CurriculumMapError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.curriculumMap");
useEffect(() => {
console.error("[portal-shell] admin curriculum-map route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 课程地图路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function CurriculumMapLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { CurriculumMapClient } from "@/features/admin/curriculum-map/curriculum-map-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 课程地图页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 CurriculumMapClientclient component中。
*
* 数据契约standardsCoverageHeatmap / globalLessonPlanStats ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function CurriculumMapPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<CurriculumMapClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 选修课编辑路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function ElectiveEditError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.elective");
useEffect(() => {
console.error("[portal-shell] admin elective edit route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 选修课编辑路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function ElectiveEditLoading(): React.ReactElement {
return <FormPageSkeleton />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { ElectiveEditClient } from "@/features/admin/elective/elective-edit-client";
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 选修课编辑表单页ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5 / admin-NeedTodo §四)
*
* Server Component 入口:仅负责 Suspense 边界包裹useParams 要求)。
* 业务逻辑在 ElectiveEditClientclient component中。
*
* 数据契约adminElective(id) + updateElective(id, input) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function ElectiveEditPage(): React.ReactElement {
return (
<Suspense fallback={<FormPageSkeleton />}>
<ElectiveEditClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 选修课详情路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function ElectiveDetailError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.elective");
useEffect(() => {
console.error("[portal-shell] admin elective detail route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 选修课详情路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function ElectiveDetailLoading(): React.ReactElement {
return <DetailPageSkeleton />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { ElectiveDetailClient } from "@/features/admin/elective/elective-detail-client";
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 选修课详情页ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5 / admin-NeedTodo §四)
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 ElectiveDetailClientclient component中。
*
* 数据契约adminElective(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
*/
export default function ElectiveDetailPage(): React.ReactElement {
return (
<Suspense fallback={<DetailPageSkeleton />}>
<ElectiveDetailClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 选修课新建路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function ElectiveCreateError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.elective");
useEffect(() => {
console.error("[portal-shell] admin elective create route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 选修课新建路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function ElectiveCreateLoading(): React.ReactElement {
return <FormPageSkeleton />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { ElectiveCreateClient } from "@/features/admin/elective/elective-create-client";
import { FormPageSkeleton } from "@/shared/components/page-templates";
/**
* 选修课新建表单页ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5 / admin-NeedTodo §四)
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 ElectiveCreateClientclient component中。
*
* 数据契约createElective(input) ❌ schema 无 Mutation 类型 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function ElectiveCreatePage(): React.ReactElement {
return (
<Suspense fallback={<FormPageSkeleton />}>
<ElectiveCreateClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 选修课管理路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function ElectiveListError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.elective");
useEffect(() => {
console.error("[portal-shell] admin elective list route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 选修课管理路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function ElectiveListLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { ElectiveListClient } from "@/features/admin/elective/elective-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 选修课管理列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 ElectiveListClientclient component中。
*
* 数据契约adminElectives ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function ElectiveListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<ElectiveListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,36 @@
"use client";
/**
* 错题本分析路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AdminErrorBookError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.errorBook.error");
useEffect(() => {
console.error("[portal-shell] admin error-book route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 错题本分析路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AdminErrorBookLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { ErrorBookClient } from "@/features/admin/error-book/error-book-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 错题本分析页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 ErrorBookClientclient component中。
*
* 数据契约adminErrorBookStats ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AdminErrorBookPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<ErrorBookClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 文件路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function FilesError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.files");
useEffect(() => {
console.error("[portal-shell] files route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 文件路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function FilesLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { FilesListClient } from "@/features/admin/files/files-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 文件管理列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 FilesListClientclient component中。
*
* 数据契约fileAttachments(filter) + fileStats ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function FilesListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<FilesListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,36 @@
"use client";
/**
* 邀请码管理路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function InvitationCodesError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.invitationCodes.error");
useEffect(() => {
console.error("[portal-shell] admin invitation-codes route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 邀请码管理路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function InvitationCodesLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { InvitationCodesListClient } from "@/features/admin/invitation-codes/invitation-codes-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 邀请码管理列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 InvitationCodesListClientclient component中。
*
* 数据契约invitationCodes(status) / createInvitationCode / revokeInvitationCode ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function InvitationCodesListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<InvitationCodesListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 教案详情只读路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/shared/components/ui/button";
export default function AdminLessonPlanViewError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.lessonPlans");
useEffect(() => {
console.error("[portal-shell] admin lesson plan view route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 教案详情只读路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AdminLessonPlanViewLoading(): React.ReactElement {
return <DetailPageSkeleton />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AdminLessonPlanViewClient } from "@/features/admin/lesson-plans/lesson-plan-view-client";
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 教案详情只读页ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 AdminLessonPlanViewClientclient component中。
*
* 数据契约adminLessonPlan(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
*/
export default function AdminLessonPlanViewPage(): React.ReactElement {
return (
<Suspense fallback={<DetailPageSkeleton />}>
<AdminLessonPlanViewClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 教案管理路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/shared/components/ui/button";
export default function AdminLessonPlansError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.lessonPlans");
useEffect(() => {
console.error("[portal-shell] admin lesson plans route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 教案管理路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AdminLessonPlansLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AdminLessonPlansListClient } from "@/features/admin/lesson-plans/lesson-plans-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 教案管理列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 AdminLessonPlansListClientclient component中。
*
* 数据契约adminLessonPlans ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AdminLessonPlansListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<AdminLessonPlansListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 组织管理路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function OrganizationError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.organization");
useEffect(() => {
console.error("[portal-shell] admin organization route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 组织管理路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function OrganizationLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { OrganizationTreeClient } from "@/features/admin/organization/organization-tree-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 组织管理树视图页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 OrganizationTreeClientclient component中。
*
* 数据契约organizationTree() ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function OrganizationTreePage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<OrganizationTreeClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 权限目录路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function PermissionsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.permissions");
useEffect(() => {
console.error("[portal-shell] admin permissions route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 权限目录路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function PermissionsLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,23 @@
import { Suspense } from "react";
import { PermissionsListClient } from "@/features/admin/permissions/permissions-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 权限目录列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 PermissionsListClientclient component中。
*
* 数据契约permissions / permissionRoleCounts ❌ schema 无 → MSW 兜底(@contract-pending
* 契约工单docs/architecture/issues/contracts/iam_contract.md#permissions
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function PermissionsListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<PermissionsListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 插件管理路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function PluginsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.plugins");
useEffect(() => {
console.error("[portal-shell] plugins route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 插件管理路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function PluginsLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,24 @@
import { Suspense } from "react";
import { PluginsClient } from "@/features/admin/plugins/plugins-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 插件管理ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / 004 §5.4
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 PluginsClientclient component中。
*
* 数据契约:
* - pluginRegistry ✅ schema 已就绪config-service
* - updatePluginRegistry(pluginId, input) ✅ schema 已就绪
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function PluginsPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<PluginsClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 题库管理路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/shared/components/ui/button";
export default function AdminQuestionsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.questions");
useEffect(() => {
console.error("[portal-shell] admin questions route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 题库管理路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AdminQuestionsLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AdminQuestionsListClient } from "@/features/admin/questions/questions-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 题库管理列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 AdminQuestionsListClientclient component中。
*
* 数据契约adminQuestions(filter) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AdminQuestionsListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<AdminQuestionsListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 角色详情路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function RoleDetailError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.roles");
useEffect(() => {
console.error("[portal-shell] admin role detail route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 角色详情路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function RoleDetailLoading(): React.ReactElement {
return <DetailPageSkeleton />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { RoleDetailClient } from "@/features/admin/roles/role-detail-client";
import { DetailPageSkeleton } from "@/shared/components/page-templates";
/**
* 角色详情页ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 RoleDetailClientclient component中。
*
* 数据契约role(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
*/
export default function RoleDetailPage(): React.ReactElement {
return (
<Suspense fallback={<DetailPageSkeleton />}>
<RoleDetailClient />
</Suspense>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
/**
* 角色管理路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function RolesError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.roles");
useEffect(() => {
console.error("[portal-shell] admin roles route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">
{t("error.title")}
</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("error.unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("error.retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 角色管理路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function RolesLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,23 @@
import { Suspense } from "react";
import { RolesListClient } from "@/features/admin/roles/roles-list-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 角色管理列表页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5
*
* Server Component 入口:仅负责 Suspense 边界包裹useSearchParams 要求)。
* 业务逻辑在 RolesListClientclient component中。
*
* 数据契约roles ❌ schema 无 → MSW 兜底(@contract-pending
* 契约工单docs/architecture/issues/contracts/iam_contract.md#roles
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function RolesListPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<RolesListClient />
</Suspense>
);
}

View File

@@ -0,0 +1,36 @@
"use client";
/**
* 自动排课路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AdminSchedulingAutoError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.scheduling.error");
useEffect(() => {
console.error("[portal-shell] admin scheduling/auto route error:", error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

View File

@@ -0,0 +1,9 @@
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 自动排课路由段加载骨架ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment loading.tsx自动包裹页面渲染期间。
*/
export default function AdminSchedulingAutoLoading(): React.ReactElement {
return <ListPageSkeleton rows={5} />;
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import { AutoScheduleClient } from "@/features/admin/scheduling/auto-schedule-client";
import { ListPageSkeleton } from "@/shared/components/page-templates";
/**
* 自动排课页ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
*
* Server Component 入口:仅负责 Suspense 边界包裹。
* 业务逻辑在 AutoScheduleClientclient component中。
*
* 数据契约adminClasses / autoSchedule ❌ schema 无 → MSW 兜底(@contract-pending
*
* 关联ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
*/
export default function AdminSchedulingAutoPage(): React.ReactElement {
return (
<Suspense fallback={<ListPageSkeleton rows={5} />}>
<AutoScheduleClient />
</Suspense>
);
}

View File

@@ -0,0 +1,39 @@
"use client";
/**
* 排课变更审批路由错误边界ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD
* Next.js Route Segment error.tsx捕获子树未处理异常。
*/
import { useEffect } from "react";
import { Button } from "@/shared/components/ui/button";
import { useTranslations } from "next-intl";
export default function AdminSchedulingChangesError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.ReactElement {
const t = useTranslations("admin.scheduling.error");
useEffect(() => {
console.error(
"[portal-shell] admin scheduling/changes route error:",
error,
);
}, [error]);
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
<p className="text-sm text-muted-foreground">
{error.message || t("unknown")}
</p>
<Button onClick={reset} variant="outline">
{t("retry")}
</Button>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More