diff --git a/apps/portal-shell/docs/needtodo/admin-NeedTodo.md b/apps/portal-shell/docs/needtodo/admin-NeedTodo.md new file mode 100644 index 0000000..8bab4bc --- /dev/null +++ b/apps/portal-shell/docs/needtodo/admin-NeedTodo.md @@ -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 的契约与批次规划(B5,24 页)。 +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 关键代码片段**:通过 `` 客户端组件触发导入流程,模板字段固定 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 + ``` + +#### 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 + ``` + +> **附注**: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 + ``` + +### 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 | 6(schools/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/insights(1 页) + +已在 §2.6 附注中提及,不重复。 + +--- + +## 五、迁移注意事项 + +### 5.1 架构迁移要点 + +1. **管理员权限校验**:所有 23 缺失页面均需 `requirePermission(Permissions.XXX)` 在 Server Component 入口校验。CICD 模式可直接复用,但需对齐 portal-shell 的 auth-guard(004 §5 鉴权链路)。 +2. **DataScope 上下文**:`school/grades/insights`、`error-book`、`attendance` 等页面依赖 `ctx.dataScope`(all / class_taught / class_members / children),portal-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. **优先级 4(ARCH 新建,需独立设计)**: + - `/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-logs(4 页 + 导出)、school(6 子页 + 跨表关联)、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 页。 diff --git a/apps/portal-shell/docs/needtodo/management-grade-NeedTodo.md b/apps/portal-shell/docs/needtodo/management-grade-NeedTodo.md new file mode 100644 index 0000000..3c0656e --- /dev/null +++ b/apps/portal-shell/docs/needtodo/management-grade-NeedTodo.md @@ -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` 包裹两个 Table(v4-P1-11) +- **技术栈**: + - Server Component(`force-dynamic`)+ `generateMetadata`(i18n `school.grades.gradeInsights.*`) + - `formatDate` / `formatNumber` 工具(`@/shared/lib/utils`) + - 共享 UI:`FilterBar` / `ChipNav` / `EmptyState` / `StatCard` / `Card` / `Badge` / `Table` 全套 + - 数据访问:`getGradeHomeworkInsights({ gradeId, limit: 50 })`(`modules/classes/data-access`,limit=50) + - 辅助查询:`getTeacherIdForMutations` + `getGradesForStaff` + - 图标:`BarChart3`(lucide-react) +- **权限**:`GRADE_RECORD_READ`(`grade_record:read`)— 只读权限 +- **ARCHITECTURE.md 契约**:❌(Drizzle 直查,无 GraphQL) +- **批次**:B2/B5 末(N) + +### 2.5 年级组练习概览(practice) + +- **CICD 路径**:`e:\desktop\CICD\src\app\(dashboard)\management\grade\practice\page.tsx` +- **建议目标路由**:`/shell/teacher/management/grade/practice` +- **功能描述**:年级组自适应练习概览页,三段式数据可视化: + - **年级筛选器**(FilterBar + ChipNav):`getGradesForStaff(teacherId)` + `?gradeId=xxx` 切换 + - **5 个统计卡(StatsGrid)**: + - 班级总数(`overview.totalClasses`,BookOpen 图标) + - 练习会话总数(`overview.totalSessions`,Activity 图标) + - 答题总数(`overview.totalQuestionsAnswered`,Target 图标) + - 平均正确率(`overview.averageAccuracy * 100`%,CheckCircle2 图标) + - 参与率(`overview.participationRate * 100`%,Users 图标) + - **班级练习对比表**:`ClassPracticeComparisonTable`(`modules/adaptive-practice/components`)渲染 `classComparison` + - **练习类型分布图**:`PracticeTypeBreakdownChart`(同模块)渲染 `typeBreakdown` + - **懒加载**:仅 `selected` 存在时执行 `Promise.all` 并行 3 查询 + - **三段空状态**:无年级 / 未选 / 数据为 null 各自 EmptyState +- **技术栈**: + - Server Component(`force-dynamic`)+ `generateMetadata`(i18n `practice.grade.*`) + - 双 i18n namespace:`practice`(主)+ `school`(年级筛选器复用 `grades.gradeInsights.selectGrade`) + - 数据访问:`modules/adaptive-practice/data-access-analytics` 的 3 个查询: + - `getGradePracticeOverview(gradeId)` + - `getGradeClassPracticeComparison(gradeId)` + - `getPracticeTypeBreakdown(studentIds)` — 需要先调 `getUserIdsByGradeId(gradeId)` 取学生 ID 列表 + - 共享 UI:`StatsGrid`(5 列)、`FilterBar`、`ChipNav`、`EmptyState` + - 业务组件:`ClassPracticeComparisonTable` + `PracticeTypeBreakdownChart` + - 6 个 lucide-react 图标:`Activity` / `BarChart3` / `BookOpen` / `CheckCircle2` / `Target` / `Users` +- **权限**:`ADAPTIVE_PRACTICE_READ`(`adaptive_practice:read`)— 自适应练习读权限 +- **ARCHITECTURE.md 契约**:❌(Drizzle 直查,无 GraphQL) +- **批次**:B2/B5 末(N) +- **迁移注意**:CICD 的 `navigation.ts` 中 `teacher.gradeManagement` 菜单组**只列了 classes/dashboard/insights 3 项**,未列 practice 子菜单。但 page.tsx 实际存在,应作为隐藏/直接 URL 访问入口保留。 + +--- + +## 三、角色归属分析 + +### 3.1 CICD 中的权限校验 + +| 页面 | requirePermission | 权限语义 | +| --------------- | ------------------------------------ | ---------------- | +| page.tsx (入口) | `Permissions.GRADE_MANAGE` | 年级组管理(写) | +| classes | `Permissions.GRADE_MANAGE` | 年级组管理(写) | +| dashboard | `Permissions.GRADE_RECORD_READ` | 成绩记录读 | +| insights | `Permissions.GRADE_RECORD_READ` | 成绩记录读 | +| practice | `Permissions.ADAPTIVE_PRACTICE_READ` | 自适应练习读 | + +> 5 个页面共用 3 类权限,无 `admin:*` 权限校验。**年级组页面是"年级组长"维度,不属于"管理员"维度**。 + +### 3.2 CICD 导航归属 + +`e:\desktop\CICD\src\modules\layout\config\navigation.ts` 中: + +- `teacher` 角色菜单组:包含 `teacher.gradeManagement`(permission=`GRADE_MANAGE`),子项含 classes/dashboard/insights +- `grade_head` 角色菜单组:同样包含 `teacher.gradeManagement`,子项相同 +- 两个角色都把年级组放在**与"teacher.grades"(成绩录入)平级**的位置,不在 `admin.*` 之下 + +### 3.3 portal-shell 角色与目录约定 + +- `apps\portal-shell\ARCHITECTURE.md` §9.1 教师域规划到 `/shell/teacher/*`,§9.4 管理域规划到 `/shell/admin/*` +- portal-shell 现有目录仅 `/shell/{teacher|admin|student|parent|dev|forbidden}/*`,无独立 `grade-head` 段 +- `src/middleware.ts` 默认合成身份为 `role: "teacher"`,未对 `grade_head` 角色做特殊路由分发 +- `src/shared/lib/route-permissions.ts` 中无 `management` / `grade` 路由条目(需新增) + +### 3.4 建议归属 + +**推荐方案:归入教师域子路径** `/shell/teacher/management/grade/*` + +理由: + +1. CICD 在 `teacher` 与 `grade_head` 两角色菜单中都把"年级组管理"挂在教师主菜单下,语义上是"教师的扩展职责" +2. portal-shell 已有 `/shell/teacher/*` 体系,复用其布局壳、middleware 身份合成、route-permissions 表 +3. 避免新建 `/shell/grade-head/*` 顶层段(会带来 sidebar 配置、middleware 分发、layout 重复实现等成本) +4. 路由权限按 `GRADE_MANAGE` / `GRADE_RECORD_READ` / `ADAPTIVE_PRACTICE_READ` 精确声明,与角色解耦——拥有这些权限位的教师(无论 role 字段是 `teacher` 还是 `grade_head`)都可访问 + +**备选方案**:若后续 portal-shell 引入 `grade_head` 独立角色段,可平滑迁移到 `/shell/grade-head/*`(page.tsx 实现完全可复用,仅需调整路由前缀)。 + +**目标路由建议**: + +| CICD 源路径 | 建议目标路由 | 权限 | +| ----------------------------- | ------------------------------------------- | ------------------------ | +| `/management/grade` | `/shell/teacher/management/grade` | `GRADE_MANAGE` | +| `/management/grade/classes` | `/shell/teacher/management/grade/classes` | `GRADE_MANAGE` | +| `/management/grade/dashboard` | `/shell/teacher/management/grade/dashboard` | `GRADE_RECORD_READ` | +| `/management/grade/insights` | `/shell/teacher/management/grade/insights` | `GRADE_RECORD_READ` | +| `/management/grade/practice` | `/shell/teacher/management/grade/practice` | `ADAPTIVE_PRACTICE_READ` | + +--- + +## 四、迁移注意事项 + +### 4.1 契约缺口(5 页全部 ❌) + +5 个页面的数据访问在 CICD 中**全部走 Drizzle ORM 直查 DB**,未通过 GraphQL schema。迁移到 portal-shell 时按 ARCH §9.5 "节奏原则"处理: + +- **MSW 先行**:用 Mock Service Worker 提供假数据,先把 5 个页面的 UI 跑起来 +- **契约工单**:向后端开 5 类查询的 GraphQL schema 工单 + - `gradeManagedClasses(userId)` + `managedGrades(userId)` + `teacherOptions()` + - `gradesForStaff(teacherId)`(年级列表) + - `gradeDistributionByGradeId(gradeId, scope)` + `gradeHomeworkInsights(gradeId, limit)` + `examsByGradeId(gradeId, scope)` + `gradeCoursePlanProgress(gradeId)` + - `gradePracticeOverview(gradeId)` + `gradeClassPracticeComparison(gradeId)` + `practiceTypeBreakdown(studentIds)` + `userIdsByGradeId(gradeId)` +- **切换策略**:契约就绪后,只改 hook 的 fetcher 指向(从 MSW 切到 Apollo Client),页面不动 + +### 4.2 跨模块依赖(5 个领域模块) + +年级组页面是**跨模块聚合页**,依赖 5 个领域模块的 data-access: + +| 依赖模块 | 用到的查询 | +| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `classes` | `getGradeManagedClasses` / `getManagedGrades` / `getTeacherIdForMutations` / `getGradeHomeworkInsights` / `getTeacherOptions` | +| `school` | `getGradesForStaff` | +| `grades` | `getGradeDistributionByGradeId` | +| `exams` | `getExamsByGradeId` | +| `course-plans` | `getGradeCoursePlanProgress` | +| `adaptive-practice` | `getGradePracticeOverview` / `getGradeClassPracticeComparison` / `getPracticeTypeBreakdown` | +| `users` | `getUserIdsByGradeId` | + +迁移时需确保 portal-shell 对应的 7 个领域模块契约齐备或 MSW 兜底覆盖。 + +### 4.3 DataScope 二次过滤 + +CICD 的 `requirePermission` 返回 `ctx.dataScope`,传入 `getGradeDistributionByGradeId` 和 `getExamsByGradeId` 进行**数据范围二次过滤**(按教师可见的年级/班级范围裁剪结果)。 + +portal-shell 中需在 hook 层保留 dataScope 参数,避免越权读取非管辖年级数据。 + +### 4.4 i18n 命名空间 + +5 个页面用到 3 个 i18n namespace: + +- `school`(dashboard / classes / insights 主命名空间) +- `practice`(practice 页主命名空间) +- `school.grades.gradeInsights.selectGrade`(practice 页复用) + +迁移时需将 CICD 的 `messages/zh-CN/school.json` / `practice.json` 中相关键同步到 portal-shell 的 i18n 资源。 + +### 4.5 业务组件迁移 + +以下业务组件需要从 CICD 迁移到 portal-shell(按页面分组): + +| 页面 | 组件 | 来源模块 | +| --------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| dashboard | `GradeDistributionPanel` / `GradeHomeworkPanel` / `GradeExamsPanel` / `GradeProgressPanel` | `modules/school/components/grade-dashboard` | +| classes | `GradeClassesClient` + 4 个子组件(`ClassDeleteDialog` / `ClassFormDialog` / `ClassListTable` / `ClassListToolbar`) | `modules/classes/components` | +| practice | `ClassPracticeComparisonTable` / `PracticeTypeBreakdownChart` | `modules/adaptive-practice/components` | +| insights | 仅用共享 UI(StatCard / Card / Table),无业务组件 | — | + +### 4.6 route-permissions.ts 同步 + +迁移完成后需在 `apps/portal-shell/src/shared/lib/route-permissions.ts` 新增 5 条路由权限声明: + +```ts +// 示例(实际实现时按真实 schema 编写) +"/shell/teacher/management/grade": { permission: "grade:manage", ... }, +"/shell/teacher/management/grade/classes": { permission: "grade:manage", ... }, +"/shell/teacher/management/grade/dashboard": { permission: "grade_record:read", ... }, +"/shell/teacher/management/grade/insights": { permission: "grade_record:read", ... }, +"/shell/teacher/management/grade/practice": { permission: "adaptive_practice:read", ... }, +``` + +### 4.7 ARCHITECTURE.md 与 arch.db 同步 + +按 `e:\Desktop\Edu\.trae\rules\project_rules.md` §1.3「改码必同步图」要求: + +- 完成 5 页迁移后运行 `pnpm run arch:scan` 更新 arch.db +- 在 `apps/portal-shell/ARCHITECTURE.md` §9.5 表格中将"缺口新增"行的"契约已就绪"列从 `0` 更新为实际就绪数(按契约工单进度) +- 在 `docs/troubleshooting/known-issues.md` 对应模块分区追加"场景→技术"映射(索引式一行,不标 AI 身份) + +### 4.8 批次与顺序 + +按 ARCH §9.5 规划,5 页归入 **B2/B5 末** 批次: + +- **B2 末**(教师域二期,与教师主功能一起补齐):classes / dashboard / insights — 因 CICD navigation 中挂在 `teacher.gradeManagement` 菜单下 +- **B5 末**(管理域,与管理员功能一起补齐):practice — 因 CICD navigation 未列 practice 子项,按独立入口处理 + +**推荐实施顺序**(依赖从轻到重): + +1. `page.tsx`(入口重定向,最简单,0 数据访问) +2. `insights`(仅查 1 个 data-access + 共享 UI) +3. `classes`(3 个 data-access + 1 客户端组件 + 4 子组件) +4. `dashboard`(4 个 data-access + 4 业务面板组件) +5. `practice`(3 个 data-access + 2 业务组件 + 1 辅助查询) + +--- + +## 五、附录:CICD 文件清单 + +``` +e:\desktop\CICD\src\app\(dashboard)\management\grade\ +├─ page.tsx # 入口重定向 → /classes +├─ error.tsx # 错误边界 +├─ loading.tsx # 加载骨架 +├─ classes\ +│ ├─ page.tsx # 年级组班级列表 +│ ├─ error.tsx +│ └─ loading.tsx +├─ dashboard\ +│ ├─ page.tsx # 年级组仪表盘(4 Tab) +│ └─ loading.tsx +├─ insights\ +│ ├─ page.tsx # 年级组作业洞察 +│ ├─ error.tsx +│ └─ loading.tsx +└─ practice\ + ├─ page.tsx # 年级组练习概览 + ├─ error.tsx + └─ loading.tsx +``` + +> portal-shell 已有统一的 `loading.tsx` / `error.tsx` 模板(见 `apps/portal-shell/src/app/shell/teacher/*` 各目录),迁移时复用模板即可,无需单独迁 error/loading。 diff --git a/apps/portal-shell/docs/needtodo/parent-NeedTodo.md b/apps/portal-shell/docs/needtodo/parent-NeedTodo.md new file mode 100644 index 0000000..0a2434e --- /dev/null +++ b/apps/portal-shell/docs/needtodo/parent-NeedTodo.md @@ -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.3(24 页,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 | +| 数据来源 | 直查 DB(Drizzle) | 走 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 } + ``` + +> ⚠️ **此页是家长域最核心页面**,是其他模块(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//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`(attendance,CICD 实际用 `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 中无源,需从零设计。** diff --git a/apps/portal-shell/docs/needtodo/shared-NeedTodo.md b/apps/portal-shell/docs/needtodo/shared-NeedTodo.md new file mode 100644 index 0000000..a1deee7 --- /dev/null +++ b/apps/portal-shell/docs/needtodo/shared-NeedTodo.md @@ -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` | 个人资料(缺失) | + +> 备注 1:portal-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 645,B1 共享) + - 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 646,B1 共享,契约字段未细化) + - 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 668,B3) +- **多角色复用**:教师 / 学生 / 家长 +- **优先级**:**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 669,B3 末,契约完全缺失) + - 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`(RSC,13 行,渲染 ``);布局 `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 UI(Button / 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 UI(Card / 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.10(RSC + 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 | 密码修改走 iam,2FA 走 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/*`),无需补齐。 diff --git a/apps/portal-shell/docs/needtodo/student-NeedTodo.md b/apps/portal-shell/docs/needtodo/student-NeedTodo.md new file mode 100644 index 0000000..c97184b --- /dev/null +++ b/apps/portal-shell/docs/needtodo/student-NeedTodo.md @@ -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.2(36 页,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 | +| 数据来源 | 直查 DB(Drizzle) | 走 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 共享路由 | + +> **加上 diagnostic(CICD 额外页)共 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//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`(diagnostic,CICD 额外) +- 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. **P5(B3 末)**: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//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 中无源页面,需重点设计。** diff --git a/apps/portal-shell/docs/needtodo/teacher-NeedTodo.md b/apps/portal-shell/docs/needtodo/teacher-NeedTodo.md new file mode 100644 index 0000000..4bb135b --- /dev/null +++ b/apps/portal-shell/docs/needtodo/teacher-NeedTodo.md @@ -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 仅做 `` 边界包裹 + 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]`),未含学生维度** +- **批次**:B2(CICD 历史功能,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 是手写 `` + - ❌ **EmptyState "清除筛选" action** — CICD 在有筛选时显示"清除筛选"按钮回到 `/exams/all`,portal-shell 无 + - ❌ **服务端筛选** — CICD 在 Server Component 内调 `getExams({ q, status, difficulty, scope })`,portal-shell 是客户端二次过滤 + - ❌ **DataScope 过滤** — CICD 用 `ctx.dataScope` 做权限过滤,portal-shell 无 +- **技术差距**: + - CICD:Server 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 无路由跳转,**按钮无实际行为**) +- **缺失功能**: + - ⚠️ **编辑按钮未接路由** — `` 无 `asChild` / `href` / `onClick`,点击无反应,应跳 `/shell/teacher/exams/[id]/edit` + - 💡 **无提交列表预览** — CICD 在 homework 详情有提交列表,exam 详情可考虑加 submissions 摘要(CICD 无此页,属增量) + - 💡 **无分析入口卡片** — 可加"查看分析"快捷入口跳 `/analytics` +- **技术差距**: + - 编辑按钮需补 `asChild` + `` + - 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 用 ``,CICD 应为 select(CICD 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/content),portal-shell 仅纯文本 HTML + - ❌ **服务端初始数据预加载** — CICD Server Component 预加载 exam + questions,portal-shell 纯客户端 MSW + - ❌ **编辑模式 (mode="edit")** — CICD `ExamRichForm` 支持 create/edit 双模式,portal-shell 仅 edit + - ⚠️ **dangerouslySetInnerHTML** — portal-shell 用 `dangerouslySetInnerHTML` 注入 HTML,违反 §4 安全规范(如必须使用,先用 DOMPurify 清洗) +- **技术差距**: + - CICD:Server 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/enrolledAt,MSW 兜底) + - 任课老师区(name/subjectName/role,MSW 兜底) +- **缺失功能**: + - ❌ **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])` +- **技术差距**: + - CICD:Server 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` 组件(含 `` 让用户手输 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 无 +- **技术差距**: + - CICD:Server 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/actions,MSW 兜底) + - 内联批改表单(studentId/score/feedback,MSW 兜底) + - 跳转"查看所有提交"按钮 → `/homework/assignments/[id]/submissions` +- **缺失功能**: + - ❌ **头部统计行** — CICD 头部有 dueDate/targetCount/submissionCount/gradedCount 四个统计指标,portal-shell 无 + - ❌ **HomeworkAssignmentQuestionErrorOverviewCard 题目错误概览卡片** — CICD 用 `getHomeworkAssignmentAnalytics(id)` 拉取每题错误率,渲染分析卡片(带 Suspense + SectionErrorBoundary) + - ❌ **HomeworkAssignmentExamContentCard 作业内容卡片** — CICD 渲染 `assignment.structure` + `questions`(含 gradedSampleCount),portal-shell 无作业内容展示 + - ❌ **服务端 analytics 预加载** — CICD 调 `getHomeworkAssignmentAnalytics(id)` 返回 `{ assignment, questions, gradedSampleCount }` + - ❌ **流式渲染** — CICD 用 `}>` 包裹 analytics 卡片,portal-shell 无 +- **技术差距**: + - CICD:Server 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 用 `` 手输 classId + - ❌ **EmptyState when no classes** — CICD 在 `classes.length === 0` 时显示 EmptyState 提示"无可用班级,去班级管理",portal-shell 无 + - ❌ **HomeworkAssignmentForm 富表单** — CICD 用专门组件(含 exam 关联、班级 select、可能含题目结构选择) +- **技术差距**: + - CICD:Server 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 包 ``,支持 AI 辅助生成/润色,portal-shell 无 + - ❌ **教材/章节上下文** — CICD Server Component 调 `getTextbookById` + `getChaptersByTextbookId` + `findChapterById`,把 textbookTitle/chapterTitle 传给编辑器工具栏显示,portal-shell 无 + - ❌ **LessonPlanProviderSetup** — CICD 有专门的 Provider 设置组件(注入编辑器所需上下文),portal-shell 无 + - ❌ **服务端初始数据预加载** — CICD Server Component 预加载 plan + teacherClasses + textbook + chapter,portal-shell 纯客户端 MSW + - ❌ **班级关联** — CICD 加载 `getTeacherClasses({ teacherId })` 传给编辑器(教案可关联班级),portal-shell 无 + - ⚠️ **dangerouslySetInnerHTML** — 同 §3.4,违反 §4 安全规范 +- **技术差距**: + - CICD:Server 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 用 `` 手输 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 客户端过滤 +- **技术差距**: + - CICD:Server 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 用 `` 手输 classId + - ❌ **服务端学生列表预加载** — CICD 调 `getClassStudentsForAttendance(defaultClassId)` 服务端预加载学生,portal-shell 完全依赖 MSW mock + - ❌ **AttendanceSheet 组件** — CICD 有专门的客户端组件(含班级切换、日期选择、批量标记、统计汇总),portal-shell 是手写表格 +- **技术差距**: + - CICD:Server 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` scope,portal-shell 无 + - ❌ **WidgetBoundary** — CICD 用 `WidgetBoundary` 包裹(局部降级),portal-shell 用 DetailPageShell +- **技术差距**: + - CICD:Server 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 StatCard(total_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 + ``,视图内 `use()` 消费 + Suspense 流式渲染,portal-shell 是客户端整体 loading + - ❌ **TeacherDashboardView 富视图** — CICD 用专门的 `TeacherDashboardView` 组件(可能含更多 widget:课表/作业/考试/学生预警等),portal-shell 仅 4 StatCard + 2 简单列表 + - ❌ **更多 widget** — CICD 可能含课表、作业待批、考试安排、学生预警等 widget(需读 TeacherDashboardView 确认),portal-shell 仅基础展示 +- **技术差距**: + - CICD:Server 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 服务,`` 注入 AI 上下文 + - 渲染 `` 富组件(位于 `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` 仅 `}>` 包裹 `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` 管理 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 questions,portal-shell 完全依赖客户端 MSW + - ❌ **DataScope 权限过滤** — CICD 用 `ctx.dataScope` 过滤考试可见性,portal-shell 无 + - ❌ **`requirePermission(Permissions.EXAM_READ)` 权限守卫** — portal-shell 无 + - ❌ **`notFound()` 404 处理** — CICD 考试不存在时调 `notFound()`,portal-shell 显示空态节点 + - ❌ **AI 集成** — CICD 包 `` 支持 AI 辅助组卷,portal-shell 无 AI 接入(与 §3.9 教案编辑同类型缺失,见 §六 共性技术债 6) + - ❌ **`normalizeStructure` 转换层** — CICD 把 `exam.structure` 规范化为 `ExamNode[]` 树(支持嵌套分组节点),portal-shell 用扁平 `ExamBuildNode[]`,无结构化分组能力 + - ❌ **structure 空时回退逻辑** — CICD 在 structure 空但 `initialSelected` 非空时回退为扁平节点列表(cuid2 生成 ID),portal-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 栏 Skeleton);i18n 命名空间与 CICD 对齐(或保留 `exams` 但与 ARCH §11 一致) + 2. **中期(契约就绪后)**: + - 改造为 Server Component,并行预加载 exam + questions(bank + selected) + - 引入 `requirePermission` + `ctx.dataScope` 过滤 + - 实现 `normalizeStructure` 转换层,支持嵌套 `ExamNode` 树(与 CICD 对齐) + - 引入 `AiClientProvider` + `createCoreAiClientService`,接入 AI 辅助组卷 + - 抽取 `ExamAssembly` 富组件,统一 3 栏 UI 与 CICD 对齐 + - questionOptions 去重合并,减少客户端重复查询 + 3. **长期**:与 §3.4(exams/[id]/edit 富文本)+ §3.9(lesson-plans edit)统一引入 Tiptap + AI 集成方案(见 §六 共性技术债 1/2/6) + +--- + +## 四、按模块汇总 + +| 模块 | ARCH 规划 | portal-shell 已实现 | ✅ 完整 | 🟡 不完整 | ❌ 缺失 | 备注 | +| ---------------- | --------- | ------------------- | -------------- | --------------------- | --------------- | ----------------------------------------------------------------------------------- | +| dashboard | 1 | 1 | 0 | 1(§3.13) | 0 | i18n 硬编码 + 缺流式 + 缺富视图 | +| exams | 7 | 6 | 1(analytics) | 4(§3.1/3.2/3.3/3.4) | 1(proctoring) | 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 | 1(schedule) | 2(§3.5/3.14) | 0 | [id] 缺 5 个 widget,list 轻量差异 | +| 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 | 1(list) | 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) | + +--- + +## 五、优先级建议 + +### P0(B1 共享,必须补,schema 已就绪) + +1. `/shell/notifications` — B1 共享,`notifications(userId)` ✅ +2. `/shell/settings` — B1 共享 + +### P1(B2末 规划页,必须补) + +3. `/shell/teacher/exams/[id]/proctoring` — B2末,WS 二期,需先定 WS 契约 + +### P2(CICD 历史功能,建议补,需同步更新 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 + AI,contentEditable deprecated 不可长期维护 + +**中优先级(交互不完整,影响体验)**:9. `students` 学生列表 — 缺下拉 + 科目成绩 10. `homework/new` 布置作业 — 缺考试关联 + 班级下拉 11. `attendance/sheet` 考勤点名 — 缺班级下拉 + 学生预加载 12. `diagnostic/class/[classId]` 班级诊断 — 缺富视图 + 服务上下文 13. `exams/[id]/edit` 富文本编辑 — 缺 Tiptap + 题目结构(与 §3.9 教案编辑同类型问题,建议统一升级 Tiptap)14. `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`(deprecated),CICD 用 Tiptap。建议统一引入 Tiptap(或 Lexical),编写 structure→EditorDoc 转换层。 +2. **dangerouslySetInnerHTML 违规**:上述两个富文本页直接 `dangerouslySetInnerHTML`,违反 §4 安全规范,需 DOMPurify 清洗。 +3. **下拉选择器缺失**:`students`/`homework/new`/`grades/entry`/`attendance/sheet` 等多处用 `` 替代 ` updateQuery("scope", e.target.value)} + placeholder="scope filter (e.g. global / school)" + className="h-9 w-64" + aria-label="scope filter" + /> + updateQuery("range", e.target.value)} + placeholder="range (e.g. 7d / 30d)" + className="h-9 w-32" + aria-label="range filter" + /> + + } + loading={loading} + loadingNode={} + empty={providers.length === 0 && !loading} + emptyNode={emptyNode} + errorNode={errorNode} + > + + +
+
+ + + + + + + + + + + + {providers.map((p) => ( + + + + + + + + + ))} + +
{t("colName")}{t("colType")}{t("colApiKey")}{t("colBaseUrl")}{t("colIsActive")}{t("colActions")}
+
{p.name}
+
+ {p.model} +
+
+ {formatProviderType(p.type)} + + {maskApiKey(extractApiKey(p.config))} + + {truncateBaseUrl(p.apiBase)} + + + +
+ + + +
+
+ + +

{t("mswNotice")}

+ + ); +} + +/** + * AI 用量仪表盘分区(StatCard 总览 + 按 Provider 明细)。 + */ +function UsageDashboardSection({ + loading, + totalRequests, + totalTokens, + totalCostCents, + byProvider, +}: { + loading: boolean; + totalRequests: number; + totalTokens: number; + totalCostCents: number; + byProvider: Array<{ + providerId: string; + providerName: string; + requests: number; + tokens: number; + costCents: number; + }>; +}): React.ReactElement { + const t = useTranslations("admin.aiSettings.list"); + + return ( +
+

{t("usageTitle")}

+
+ + + +
+ + + + {t("usageByProvider")} + + + {byProvider.length === 0 ? ( +

{t("emptyUsage")}

+ ) : ( +
+ + + + + + + + + + + {byProvider.map((row) => ( + + + + + + + ))} + +
+ {t("colProviderName")} + + {t("colRequests")} + + {t("colTokens")} + + {t("colCostCents")} +
{row.providerName} + {formatNumber(row.requests)} + + {formatNumber(row.tokens)} + + {formatCostCents(row.costCents)} +
+
+ )} +
+
+
+ ); +} + +/** + * 状态徽章(按 isActive 渲染启用/停用徽章)。 + */ +function ActiveBadge({ isActive }: { isActive: boolean }): React.ReactElement { + const t = useTranslations("admin.aiSettings.list"); + const label = t(formatActiveLabel(isActive)); + const cls = activeToBadgeClass(isActive); + return ( + + {label} + + ); +} diff --git a/apps/portal-shell/src/features/admin/ai-settings/transformations.ts b/apps/portal-shell/src/features/admin/ai-settings/transformations.ts new file mode 100644 index 0000000..f4745c8 --- /dev/null +++ b/apps/portal-shell/src/features/admin/ai-settings/transformations.ts @@ -0,0 +1,111 @@ +/** + * AI Settings 数据变换工具(ARCHITECTURE.md §11.3 DoD - 纯函数单测) + * + * 所有格式化/映射函数均为纯函数,便于 vitest 单测。 + * 关联:ARCHITECTURE.md §11.3 DoD "数据变换/权限判断等纯函数有 vitest 单测" + */ + +/** AI Provider 类型标签映射 */ +export const PROVIDER_TYPE_LABEL: Record = { + openai: "OpenAI", + anthropic: "Anthropic", + azure: "Azure OpenAI", + local: "本地模型", +}; + +/** AI Provider 类型支持的取值列表 */ +export const PROVIDER_TYPES: readonly string[] = [ + "openai", + "anthropic", + "azure", + "local", +] as const; + +/** + * 将 Provider 类型代码映射为展示标签。未知值回退为原始值。 + */ +export function formatProviderType(type: string): string { + return PROVIDER_TYPE_LABEL[type] ?? type; +} + +/** + * 判断字符串是否为受支持的 Provider 类型。 + */ +export function isValidProviderType(type: string): boolean { + return PROVIDER_TYPES.includes(type); +} + +/** + * 根据 isActive 返回 Tailwind 徽章语义类名。 + */ +export function activeToBadgeClass(isActive: boolean): string { + return isActive + ? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400" + : "bg-muted text-muted-foreground"; +} + +/** + * 根据 isActive 返回展示标签。 + */ +export function formatActiveLabel(isActive: boolean): string { + return isActive ? "active" : "inactive"; +} + +/** + * 格式化 AI 用量费用(分)为元展示字符串。 + * 输入无效返回 "¥0.00"。 + */ +export function formatCostCents(costCents: number): string { + if (!Number.isFinite(costCents) || costCents < 0) return "¥0.00"; + const yuan = costCents / 100; + return `¥${yuan.toFixed(2)}`; +} + +/** + * 格式化数字为千分位展示字符串。 + * 输入无效返回 "0"。 + */ +export function formatNumber(value: number): string { + if (!Number.isFinite(value) || value < 0) return "0"; + return value.toLocaleString("zh-CN"); +} + +/** + * 截断 API Key 用于列表展示(仅保留前 4 + 后 4 字符,中间以 ... 占位)。 + * 输入为空返回占位符 "--"。 + */ +export function maskApiKey(apiKey: string | null | undefined): string { + if (!apiKey || apiKey.trim().length === 0) return "--"; + const text = apiKey.trim(); + if (text.length <= 8) return text; + return `${text.slice(0, 4)}...${text.slice(-4)}`; +} + +/** + * 截断 BaseUrl 用于列表展示(超过 maxLen 字符时截断并加省略号)。 + * maxLen 默认 40。 + */ +export function truncateBaseUrl( + url: string | null | undefined, + maxLen = 40, +): string { + if (!url) return "--"; + const text = url.trim(); + if (text.length <= maxLen) return text; + return `${text.slice(0, maxLen)}...`; +} + +/** + * 格式化 ISO 日期字符串为本地化展示(zh-CN,仅年月日)。 + * 输入无效时返回占位符。 + */ +export function formatAiDate(isoDate: string | null | undefined): string { + if (!isoDate) return "--"; + const d = new Date(isoDate); + if (Number.isNaN(d.getTime())) return "--"; + return d.toLocaleDateString("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); +} diff --git a/apps/portal-shell/src/features/admin/announcements/__tests__/transformations.test.ts b/apps/portal-shell/src/features/admin/announcements/__tests__/transformations.test.ts new file mode 100644 index 0000000..1cd3b15 --- /dev/null +++ b/apps/portal-shell/src/features/admin/announcements/__tests__/transformations.test.ts @@ -0,0 +1,132 @@ +/** + * Announcements 数据变换工具单测(ARCHITECTURE.md §11.3 DoD) + * + * 关联:ARCHITECTURE.md §11.3 "数据变换/权限判断等纯函数有 vitest 单测" + */ +import { describe, expect, it } from "vitest"; + +import { + AUDIENCE_LABEL, + STATUS_LABEL, + announcementStatusToBadgeClass, + formatAnnouncementDate, + formatAnnouncementStatus, + formatAudience, + isAnnouncementArchivable, + isAnnouncementEditable, + isAnnouncementPinned, +} from "../transformations"; + +describe("announcementStatusToBadgeClass", () => { + it("returns muted class for draft", () => { + expect(announcementStatusToBadgeClass("draft")).toBe( + "bg-muted text-muted-foreground", + ); + }); + + it("returns emerald class for published", () => { + expect(announcementStatusToBadgeClass("published")).toContain("emerald"); + }); + + it("returns amber class for archived", () => { + expect(announcementStatusToBadgeClass("archived")).toContain("amber"); + }); + + it("returns muted class for unknown status", () => { + expect(announcementStatusToBadgeClass("unknown")).toBe( + "bg-muted text-muted-foreground", + ); + expect(announcementStatusToBadgeClass("")).toBe( + "bg-muted text-muted-foreground", + ); + }); +}); + +describe("formatAnnouncementDate", () => { + it("formats valid ISO date string", () => { + const result = formatAnnouncementDate("2026-07-22T10:30:00Z"); + expect(result).toContain("2026"); + expect(result).toContain("07"); + }); + + it("returns placeholder for null/undefined/empty", () => { + expect(formatAnnouncementDate(null)).toBe("--"); + expect(formatAnnouncementDate(undefined)).toBe("--"); + expect(formatAnnouncementDate("")).toBe("--"); + }); + + it("returns placeholder for invalid date", () => { + expect(formatAnnouncementDate("not-a-date")).toBe("--"); + }); +}); + +describe("isAnnouncementPinned", () => { + it("returns true for non-empty pinnedAt", () => { + expect(isAnnouncementPinned("2026-07-22T10:30:00Z")).toBe(true); + }); + + it("returns false for null/undefined/empty/whitespace", () => { + expect(isAnnouncementPinned(null)).toBe(false); + expect(isAnnouncementPinned(undefined)).toBe(false); + expect(isAnnouncementPinned("")).toBe(false); + expect(isAnnouncementPinned(" ")).toBe(false); + }); +}); + +describe("isAnnouncementEditable", () => { + it("returns true for draft and published", () => { + expect(isAnnouncementEditable("draft")).toBe(true); + expect(isAnnouncementEditable("published")).toBe(true); + }); + + it("returns false for archived and unknown", () => { + expect(isAnnouncementEditable("archived")).toBe(false); + expect(isAnnouncementEditable("unknown")).toBe(false); + }); +}); + +describe("isAnnouncementArchivable", () => { + it("returns true for draft and published", () => { + expect(isAnnouncementArchivable("draft")).toBe(true); + expect(isAnnouncementArchivable("published")).toBe(true); + }); + + it("returns false for archived", () => { + expect(isAnnouncementArchivable("archived")).toBe(false); + }); +}); + +describe("formatAudience", () => { + it("maps known audiences to Chinese labels", () => { + expect(formatAudience("all")).toBe("全校"); + expect(formatAudience("teachers")).toBe("教师"); + expect(formatAudience("students")).toBe("学生"); + expect(formatAudience("parents")).toBe("家长"); + }); + + it("returns original value for unknown audience", () => { + expect(formatAudience("staff")).toBe("staff"); + expect(formatAudience("")).toBe(""); + }); + + it("AUDIENCE_LABEL covers 4 standard audiences", () => { + expect(Object.keys(AUDIENCE_LABEL)).toHaveLength(4); + }); +}); + +describe("formatAnnouncementStatus", () => { + it("maps known statuses to Chinese labels", () => { + expect(formatAnnouncementStatus("draft")).toBe("草稿"); + expect(formatAnnouncementStatus("published")).toBe("已发布"); + expect(formatAnnouncementStatus("archived")).toBe("已归档"); + }); + + it("returns original value for unknown status", () => { + expect(formatAnnouncementStatus("other")).toBe("other"); + expect(formatAnnouncementStatus("")).toBe(""); + }); + + it("STATUS_LABEL covers 3 standard statuses", () => { + expect(Object.keys(STATUS_LABEL)).toHaveLength(3); + }); +}); diff --git a/apps/portal-shell/src/features/admin/announcements/announcement-detail-client.tsx b/apps/portal-shell/src/features/admin/announcements/announcement-detail-client.tsx new file mode 100644 index 0000000..5e861d5 --- /dev/null +++ b/apps/portal-shell/src/features/admin/announcements/announcement-detail-client.tsx @@ -0,0 +1,233 @@ +"use client"; + +/** + * 公告详情页 - 客户端组件(ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5) + * + * 数据契约: + * - 单查 adminAnnouncement(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending) + * - mutation archive / pin / delete ❌ → MSW 兜底 + * + * 三态规范(§11.3 DoD): + * - loading:DetailPageSkeleton + * - error:errorNode 局部降级 + * - notFound:data 为 null 时显示空态节点 + * + * 关联:ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3 + */ +import { Megaphone } from "lucide-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useTranslations } from "next-intl"; + +import { + useAdminAnnouncement, + useArchiveAnnouncement, + useDeleteAnnouncement, + usePinAnnouncement, + type AnnouncementDetail, +} from "@/lib/api"; +import { Button } from "@/shared/components/ui/button"; +import { + DetailPageShell, + DetailPageSkeleton, + DetailSection, + DetailField, +} from "@/shared/components/page-templates"; +import { notify } from "@/shared/lib/notify"; +import { + announcementStatusToBadgeClass, + formatAnnouncementDate, + formatAnnouncementStatus, + formatAudience, + isAnnouncementArchivable, + isAnnouncementEditable, + isAnnouncementPinned, +} from "@/features/admin/announcements/transformations"; + +/** + * 详情客户端主体。需由 server page 包裹在 中。 + */ +export function AnnouncementDetailClient(): React.ReactElement { + const t = useTranslations("admin.announcements.detail"); + const tCommon = useTranslations("common"); + const params = useParams<{ id: string }>(); + const announcementId = params?.id ?? ""; + + // @contract-pending:MSW 兜底 + const { data, loading, error } = useAdminAnnouncement(announcementId); + + // @contract-pending:MSW 兜底 + const { run: archiveAnnouncement } = useArchiveAnnouncement(); + const { run: pinAnnouncement } = usePinAnnouncement(); + const { run: deleteAnnouncement } = useDeleteAnnouncement(); + + const handleArchive = async (): Promise => { + try { + await archiveAnnouncement(announcementId); + notify.success(t("archive")); + } catch (err) { + notify.error(tCommon("error.loadFailed", { message: String(err) })); + } + }; + + const handlePinToggle = async (): Promise => { + try { + await pinAnnouncement(announcementId); + notify.success( + isAnnouncementPinned(data?.pinnedAt) ? t("unpin") : t("pin"), + ); + } catch (err) { + notify.error(tCommon("error.loadFailed", { message: String(err) })); + } + }; + + const handleDelete = async (): Promise => { + if (!window.confirm(t("deleteConfirm"))) return; + try { + await deleteAnnouncement(announcementId); + notify.success(t("delete")); + } catch (err) { + notify.error(tCommon("error.loadFailed", { message: String(err) })); + } + }; + + const errorNode = error ? ( +
+

+ {tCommon("error.loadFailed", { message: String(error) })} +

+
+ ) : undefined; + + const pinned = isAnnouncementPinned(data?.pinnedAt); + + return ( + } + backHref="/shell/admin/announcements" + actions={ + data && isAnnouncementEditable(data.status) ? ( +
+ + + {isAnnouncementArchivable(data.status) ? ( + + ) : null} + +
+ ) : null + } + loading={loading} + loadingNode={} + errorNode={errorNode} + emptyNode={ + !loading && !error && !data ? ( +
+ {t("notFound")} +
+ ) : undefined + } + > + {data ? : null} +
+ ); +} + +/** + * 详情内容区(基本信息 + 公告内容 + 发布范围)。 + */ +function AnnouncementDetailBody({ + announcement, +}: { + announcement: AnnouncementDetail; +}): React.ReactElement { + const t = useTranslations("admin.announcements.detail"); + return ( + <> + + + } + /> + + + + + + + + + +
+ {announcement.content} +
+
+ + + + 0 + ? announcement.grades.join(", ") + : "-" + } + /> + 0 + ? announcement.classes.join(", ") + : "-" + } + /> + + + ); +} + +/** + * 状态徽章(按状态色阶展示)。 + */ +function StatusBadge({ status }: { status: string }): React.ReactElement { + const label = formatAnnouncementStatus(status); + const cls = announcementStatusToBadgeClass(status); + return ( + + {label} + + ); +} diff --git a/apps/portal-shell/src/features/admin/announcements/announcement-edit-client.tsx b/apps/portal-shell/src/features/admin/announcements/announcement-edit-client.tsx new file mode 100644 index 0000000..591a439 --- /dev/null +++ b/apps/portal-shell/src/features/admin/announcements/announcement-edit-client.tsx @@ -0,0 +1,252 @@ +"use client"; + +/** + * 公告编辑表单页 - 客户端组件(ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5) + * + * 数据契约: + * - 单查 adminAnnouncement(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending) + * - mutation updateAnnouncement(id, input) ❌ → MSW 兜底 + * + * 三态规范(§11.3 DoD): + * - loading:FormPageSkeleton(初始数据加载) + * - error:errorSummary 表单级错误 + * - success:notify.success + router.push 回详情页 + * + * 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4 + */ +import { Megaphone } from "lucide-react"; +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useState, useTransition } from "react"; +import { useTranslations } from "next-intl"; + +import { + useAdminAnnouncement, + usePinAnnouncement, + useUpdateAnnouncement, + type AnnouncementInput, +} from "@/lib/api"; +import { FormPageShell } from "@/shared/components/page-templates"; +import { notify } from "@/shared/lib/notify"; +import { isAnnouncementPinned } from "@/features/admin/announcements/transformations"; + +/** 可选状态枚举(与列表筛选对齐) */ +const STATUS_OPTIONS = ["draft", "published", "archived"] as const; +/** 可选受众枚举 */ +const AUDIENCE_OPTIONS = ["all", "teachers", "students", "parents"] as const; + +/** + * 编辑表单客户端主体。需由 server page 包裹在 中。 + */ +export function AnnouncementEditClient(): React.ReactElement { + const t = useTranslations("admin.announcements.edit"); + const tCommon = useTranslations("common"); + const router = useRouter(); + const params = useParams<{ id: string }>(); + const announcementId = params?.id ?? ""; + const [, startTransition] = useTransition(); + + // @contract-pending:MSW 兜底 + const { data, loading, error } = useAdminAnnouncement(announcementId); + // @contract-pending:MSW 兜底 + const { run: updateAnnouncement, loading: submitting } = + useUpdateAnnouncement(); + const { run: pinAnnouncement } = usePinAnnouncement(); + + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [status, setStatus] = useState("draft"); + const [audience, setAudience] = useState("all"); + const [pinned, setPinned] = useState(false); + const [originalPinned, setOriginalPinned] = useState(false); + const [formError, setFormError] = useState(null); + const [initialized, setInitialized] = useState(false); + + // 数据到达后预填表单 + useEffect(() => { + if (data && !initialized) { + setTitle(data.title); + setContent(data.content); + setStatus(data.status); + setAudience(data.audience); + const isPinned = isAnnouncementPinned(data.pinnedAt); + setPinned(isPinned); + setOriginalPinned(isPinned); + setInitialized(true); + } + }, [data, initialized]); + + const handleFormSubmit = async (): Promise => { + setFormError(null); + + if (!title.trim()) { + setFormError(t("errorTitleRequired")); + return; + } + if (!content.trim()) { + setFormError(t("errorContentRequired")); + return; + } + + const input: AnnouncementInput = { + title: title.trim(), + content: content.trim(), + status, + audience, + }; + + try { + await updateAnnouncement(announcementId, input); + // 若置顶状态变更,同步调用 pin(后端按 toggle 处理) + if (pinned !== originalPinned) { + try { + await pinAnnouncement(announcementId); + } catch (pinErr) { + notify.error( + tCommon("error.loadFailed", { message: String(pinErr) }), + ); + } + } + notify.success(t("success")); + startTransition(() => { + router.push(`/shell/admin/announcements/${announcementId}`); + }); + } catch (err) { + setFormError(`${t("error")}: ${String(err)}`); + } + }; + + if (loading) { + return ( + } + backHref={`/shell/admin/announcements/${announcementId}`} + loading + /> + ); + } + + if (error || (!data && !loading)) { + return ( + } + backHref={`/shell/admin/announcements/${announcementId}`} + errorSummary={ +

+ {tCommon("error.loadFailed", { message: String(error ?? "") })} +

+ } + /> + ); + } + + return ( + } + backHref={`/shell/admin/announcements/${announcementId}`} + onSubmit={handleFormSubmit} + submitting={submitting} + submitLabel={t("submit")} + cancelLabel={t("cancel")} + errorSummary={ + formError ? ( +

{formError}

+ ) : undefined + } + > + {/* 标题 */} + + setTitle(e.target.value)} + className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm" + required + /> + + + {/* 内容 */} + +