feat(portal-shell): 管理域 §9.4 B5 全量迁移(24 + 21 补充批次共 44 页 + 42 features)
按 ARCHITECTURE.md §9.4 规划口径 + admin-NeedTodo.md §四补充批次完成管理域全量页面迁移: 【§9.4 规划 24 页(B5)】 - users(2) + roles(1) + permissions(1) + audit-logs(4) + invitation-codes(1) - school(6: redirect/schools/classes/departments/academic-year/grades) - announcements(1) + files(1) + ai-settings(1) + system(1) + viewports(1) - students(1) + teachers(1) + organization(1) + plugins(1, config-service) - 仪表盘已存在(/shell/admin/page.tsx) 【§四补充批次 21 页】 - course-plans(4) + elective(4) + questions(1) + lesson-plans(2) + error-book(1) - scheduling(3: auto/changes/rules) + attendance(1) + curriculum-map(1) - announcements 详情/编辑(2) + roles/[id] 详情(1) + users/import(1) 【实现要点】 - 全部使用 ListPageShell + loading/error/empty 三态规范(§11.3 DoD) - 走 lib/api hooks;未就绪契约走 MSW + @contract-pending 注释(§11.4) - 文案走 useTranslations(zh-CN + en 两份同步更新) - 42 个 features/<domain>/transformations.ts 纯函数 + 配套 vitest 单测 - catch 块统一 notify.error;无空 catch;lint:tokens 通过 - 路由全部登记到 route-permissions.ts(39 EXACT + 8 PREFIX) 【验收】 - tsc --noEmit: 0 errors - ESLint src: 0 errors (4 generated-files warnings, pre-existing) - lint:tokens: 0 errors - vitest: 1639/1639 passed (含 23 admin 测试文件 671 用例) - check:routes: PASS (143 routes, 4 ghost entries pre-existing) - check:pages: PASS (146 pages) - check:codegen: PASS - arch:scan: 24 modules, 8262 symbols 关联:ARCHITECTURE.md §9.4 / §10 P5 / §11.3 DoD / §11.6
This commit is contained in:
582
apps/portal-shell/docs/needtodo/admin-NeedTodo.md
Normal file
582
apps/portal-shell/docs/needtodo/admin-NeedTodo.md
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
# 管理域(Admin)待完成功能分析
|
||||||
|
|
||||||
|
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\admin\`(41 个 page.tsx)
|
||||||
|
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\admin\`(1 个 page.tsx)
|
||||||
|
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.4(管理域 24 页,B5 批次)
|
||||||
|
> 分析日期:2026-07-24
|
||||||
|
> 分析方式:逐页面对比 CICD 实现 vs portal-shell 实现,逐个读取 CICD page.tsx + ARCH §9.4 契约
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 〇、方法论与对比基线
|
||||||
|
|
||||||
|
1. CICD 单体(Next.js App Router + Server Actions + Drizzle)作为**功能基线**,反映"老版单体已实现"的管理员功能完整态。
|
||||||
|
2. portal-shell 作为**目标态**,遵循 ARCH §9.4 的契约与批次规划(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 关键代码片段**:通过 `<UserImportDialog />` 客户端组件触发导入流程,模板字段固定 5 列。
|
||||||
|
|
||||||
|
### 2.3 roles + permissions 模块(2 页缺失)
|
||||||
|
|
||||||
|
#### 2.3.1 `/shell/admin/roles`(角色管理列表)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\roles\page.tsx`
|
||||||
|
- **功能描述**:角色列表 + 创建/编辑入口
|
||||||
|
- 查询 `getRoles()` 拉取所有角色
|
||||||
|
- 渲染 `RoleManagementView` 客户端组件(含创建按钮、角色卡片/列表)
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.ROLE_READ)` + Drizzle `rbac/data-access`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **CICD 关键代码片段**:
|
||||||
|
```ts
|
||||||
|
await requirePermission(Permissions.ROLE_READ)
|
||||||
|
const roles = await getRoles()
|
||||||
|
return <RoleManagementView roles={roles} />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3.2 `/shell/admin/permissions`(权限目录)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\permissions\page.tsx`
|
||||||
|
- **功能描述**:权限目录页(系统所有权限点列表,按模块分组)
|
||||||
|
- 查询 `getPermissionRoleCounts()` 获取每个权限点关联的角色数
|
||||||
|
- 渲染 `PermissionCatalogView` 客户端组件
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.PERMISSION_READ)` + Drizzle `rbac/data-access-permissions`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **CICD 关键代码片段**:
|
||||||
|
```ts
|
||||||
|
await requirePermission(Permissions.PERMISSION_READ)
|
||||||
|
const roleCountsByPermission = await getPermissionRoleCounts()
|
||||||
|
return <PermissionCatalogView roleCountsByPermission={roleCountsByPermission} />
|
||||||
|
```
|
||||||
|
|
||||||
|
> **附注**:CICD 另有 `admin/roles/[id]/page.tsx`(角色详情,含 `RolePermissionMatrix` 权限矩阵 + `RoleDetailEditButton`),但 ARCH §9.4 仅规划 `/shell/admin/roles` 一页,未列详情子页。可作 B5 后期补强。
|
||||||
|
|
||||||
|
### 2.4 audit-logs 模块(4 页缺失)
|
||||||
|
|
||||||
|
#### 2.4.1 `/shell/admin/audit-logs`(审计日志列表 + 导出)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\audit-logs\page.tsx`
|
||||||
|
- **功能描述**:审计日志列表 + 多维筛选 + CSV 导出
|
||||||
|
- URL 参数:`page`、`module`、`action`、`status`、`userId`、`startDate`、`endDate`
|
||||||
|
- 并行查询:`getAuditLogs({...})` + `getAuditModuleOptions()`
|
||||||
|
- `AuditLogExportButton` 触发 CSV 导出(按筛选条件)
|
||||||
|
- `SectionErrorBoundary` 错误兜底
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.AUDIT_LOG_READ)` + Drizzle `audit/data-access` + 类型守卫 `isValidAuditLogStatus`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **CICD 关键代码片段**:
|
||||||
|
```ts
|
||||||
|
const [result, moduleOptions] = await Promise.all([
|
||||||
|
getAuditLogs({ page, module, action, status, userId, startDate, endDate }),
|
||||||
|
getAuditModuleOptions(),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4.2 `/shell/admin/audit-logs/overview`(审计概览)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\audit-logs\overview\page.tsx`
|
||||||
|
- **功能描述**:审计概览页(统计/趋势/分布三件套)
|
||||||
|
- 并行查询:`getAuditOverviewStats()` + `getAuditTrend(7)` + `getDataChangeActionStats()`
|
||||||
|
- `AuditServiceProvider` 包裹 `adminAuditService` 注入服务上下文
|
||||||
|
- 渲染 `AuditOverviewView` 客户端组件(统计卡 + 7 天趋势图 + 动作分布饼图)
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.AUDIT_LOG_READ)` + 服务注入模式
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **CICD 关键代码片段**:
|
||||||
|
```ts
|
||||||
|
const [stats, trend, distribution] = await Promise.all([
|
||||||
|
getAuditOverviewStats(),
|
||||||
|
getAuditTrend(7),
|
||||||
|
getDataChangeActionStats(),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4.3 `/shell/admin/audit-logs/login-logs`(登录日志)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\audit-logs\login-logs\page.tsx`
|
||||||
|
- **功能描述**:登录日志列表 + 筛选 + 导出
|
||||||
|
- URL 参数:`page`、`action`(signin/signout/signup)、`status`(success/failure)、`userId`、`startDate`、`endDate`
|
||||||
|
- 查询 `getLoginLogs({...})`
|
||||||
|
- `AuditLogExportButton exportType="login"` 触发导出
|
||||||
|
- 渲染 `LoginLogView` 客户端组件
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.AUDIT_LOG_READ)` + 类型守卫 `isValidLoginLogAction` / `isValidLoginLogStatus`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
#### 2.4.4 `/shell/admin/audit-logs/data-changes`(数据变更日志)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\audit-logs\data-changes\page.tsx`
|
||||||
|
- **功能描述**:数据变更日志列表 + 筛选 + 导出 + 统计
|
||||||
|
- URL 参数:`page`、`tableName`、`action`(create/update/delete)、`userId`、`startDate`、`endDate`
|
||||||
|
- 并行查询:`getDataChangeLogs({...})` + `getDataChangeTableOptions()` + `getDataChangeStats()`
|
||||||
|
- 渲染 `DataChangeLogView` 客户端组件(含表选项下拉 + 变更统计卡)
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.AUDIT_LOG_READ)` + 类型守卫 `isValidDataChangeAction`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
### 2.5 invitation-codes 模块(1 页缺失)
|
||||||
|
|
||||||
|
#### 2.5.1 `/shell/admin/invitation-codes`(邀请码管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\invitation-codes\page.tsx`
|
||||||
|
- **功能描述**:邀请码列表 + 生成 + 删除
|
||||||
|
- SSR 查询 `listInvitationCodes(200, true)`(200 条 + 含已用)
|
||||||
|
- 服务端计算 `now = new Date().getTime()` 传给客户端(避免 Client 端 render 阶段调用 `Date.now()`,符合 react-hooks/purity 规则)
|
||||||
|
- 生成/删除通过 Server Action + `router.refresh()` 触发更新
|
||||||
|
- 渲染 `InvitationCodesView` 客户端组件
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.USER_MANAGE)` + Drizzle `invitation-codes/data-access`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **CICD 关键代码片段**:
|
||||||
|
```ts
|
||||||
|
const codes = await listInvitationCodes(200, true)
|
||||||
|
const now = new Date().getTime()
|
||||||
|
return <InvitationCodesView initialCodes={codes} now={now} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.6 school 模块(6 页缺失)
|
||||||
|
|
||||||
|
#### 2.6.1 `/shell/admin/school`(学校首页,redirect)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\school\page.tsx`
|
||||||
|
- **功能描述**:纯重定向页 `redirect("/admin/school/schools")`,无 UI
|
||||||
|
- **技术栈**:`redirect()` from `next/navigation`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
#### 2.6.2 `/shell/admin/school/schools`(学校列表)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\school\schools\page.tsx`
|
||||||
|
- **功能描述**:学校列表 + CRUD
|
||||||
|
- 查询 `getSchools()`
|
||||||
|
- 渲染 `SchoolsClient` 客户端组件
|
||||||
|
- `SectionErrorBoundary` 错误兜底
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle `school/data-access`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
#### 2.6.3 `/shell/admin/school/classes`(班级管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\school\classes\page.tsx`
|
||||||
|
- **功能描述**:班级管理 + 跨数据聚合
|
||||||
|
- 并行查询:`getAdminClasses()` + `getTeacherOptions()` + `getSchools()` + `getGrades()`
|
||||||
|
- 渲染 `AdminClassesClient` 客户端组件(含班级 CRUD + 教师/学校/年级下拉)
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle `classes/data-access` + `school/data-access`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **CICD 关键代码片段**:
|
||||||
|
```ts
|
||||||
|
const [classes, teachers, schools, grades] = await Promise.all([
|
||||||
|
getAdminClasses(),
|
||||||
|
getTeacherOptions(),
|
||||||
|
getSchools(),
|
||||||
|
getGrades(),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注**:ARCH §9.4 第 7 行将 `admin/classes` 单列 1 页(M),与 `school/classes` 实际同源,建议合并到 `/shell/admin/school/classes`。
|
||||||
|
|
||||||
|
#### 2.6.4 `/shell/admin/school/departments`(部门管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\school\departments\page.tsx`
|
||||||
|
- **功能描述**:部门列表 + CRUD
|
||||||
|
- 查询 `getDepartments()`
|
||||||
|
- 渲染 `DepartmentsClient` 客户端组件
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
#### 2.6.5 `/shell/admin/school/academic-year`(学年管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\school\academic-year\page.tsx`
|
||||||
|
- **功能描述**:学年列表 + CRUD
|
||||||
|
- 查询 `getAcademicYears()`
|
||||||
|
- 渲染 `AcademicYearClient` 客户端组件
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
#### 2.6.6 `/shell/admin/school/grades`(年级管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\school\grades\page.tsx`
|
||||||
|
- **功能描述**:年级列表 + CRUD + 统计
|
||||||
|
- 并行查询:`getGrades()` + `getSchools()` + `getStaffOptions()` + `getGradeOverviewStats()`
|
||||||
|
- 渲染 `GradesClient` 客户端组件(年级 CRUD + 学校/教职工下拉 + 统计概览)
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.SCHOOL_MANAGE)` + Drizzle
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
> **附注**:CICD 另有 `school/grades/insights/page.tsx`(年级洞察,含 `SchoolWideSummaryCard` 全校汇总 + ChipNav 年级筛选 + 作业表 + 班级排名表 + `getSchoolWideGradeSummary(ctx.dataScope)`),属管理员数据分析视角,**ARCH §9.4 未单独规划**,可作 B5 后期补强。
|
||||||
|
|
||||||
|
### 2.7 announcements 模块(1 页缺失,ARCH 口径)
|
||||||
|
|
||||||
|
#### 2.7.1 `/shell/admin/announcements`(公告管理列表)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\announcements\page.tsx`
|
||||||
|
- **功能描述**:公告列表 + 状态筛选
|
||||||
|
- URL 参数:`status`(draft/published/archived)
|
||||||
|
- 查询 `getAdminAnnouncementsPageData(status)`(一次性返回 announcements + grades + classes)
|
||||||
|
- 渲染 `AdminAnnouncementsView` 客户端组件
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.ANNOUNCEMENT_MANAGE)` + Drizzle `announcements/data-access` + 类型守卫 `isValidStatus`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
> **附注**:CICD 另有 `announcements/[id]/page.tsx`(公告详情,含发布/归档/删除/置顶操作)+ `announcements/[id]/edit/page.tsx`(编辑表单 `AnnouncementForm mode="edit"` + grades 关联),**ARCH §9.4 仅规划 1 页**,建议合并到详情子页或工作流组件。
|
||||||
|
|
||||||
|
### 2.8 files 模块(1 页缺失)
|
||||||
|
|
||||||
|
#### 2.8.1 `/shell/admin/files`(文件管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\files\page.tsx`
|
||||||
|
- **功能描述**:文件列表 + 统计
|
||||||
|
- 并行查询:`getFileAttachmentsWithFilters({ limit: 200 })` + `getFileStats()`
|
||||||
|
- 渲染 `AdminFilesView` 客户端组件(含统计卡 + 文件列表)
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.FILE_READ)` + Drizzle `files/data-access`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **CICD 关键代码片段**:
|
||||||
|
```ts
|
||||||
|
const [files, stats] = await Promise.all([
|
||||||
|
getFileAttachmentsWithFilters({ limit: 200 }),
|
||||||
|
getFileStats(),
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.9 ai-settings 模块(1 页缺失)
|
||||||
|
|
||||||
|
#### 2.9.1 `/shell/admin/ai-settings`(AI 配置)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\ai-settings\page.tsx`
|
||||||
|
- **功能描述**:AI Provider 配置 + Usage 仪表盘(取代旧 `/settings?tab=ai` 与考试页内嵌 AI 弹窗)
|
||||||
|
- 权限:`AI_CHAT`(普通用户可访问自己的 private provider),管理员额外有 `AI_CONFIGURE`(管理 public provider 与他人 private provider)
|
||||||
|
- 渲染 `AiProviderSettingsCard` 客户端组件(含 isAdmin 开关)
|
||||||
|
- 管理员额外渲染 `AiUsageDashboard`(用量统计)
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.AI_CHAT)` + 通过 `ctx.permissions.includes(Permissions.AI_CONFIGURE)` 区分管理员
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
### 2.10 system 模块(1 页缺失)
|
||||||
|
|
||||||
|
#### 2.10.1 `/shell/admin/system`(系统设置)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\admin\settings\page.tsx`
|
||||||
|
- **功能描述**:系统设置页
|
||||||
|
- 渲染 `AdminSettingsView` 客户端组件(具体配置项由组件内部决定)
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.SETTINGS_ADMIN)` + Drizzle
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5,对应 ARCH §9.4 第 13 行 `admin/system` → `/shell/admin/system`)
|
||||||
|
|
||||||
|
### 2.11 viewports 模块(1 页缺失,ARCH 新建)
|
||||||
|
|
||||||
|
#### 2.11.1 `/shell/admin/viewports`(视口配置)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:**无**(CICD 未实现)
|
||||||
|
- **功能描述**:对齐 004 §5.4 视口配置(BFF/Service 层 viewport 路由策略可视化)
|
||||||
|
- **技术栈**:未知,需依据 004 §5.4 设计
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
|
||||||
|
### 2.12 plugins 模块(1 页,契约已就绪 ✅)
|
||||||
|
|
||||||
|
#### 2.12.1 `/shell/admin/plugins`(插件管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:**无**(CICD 未实现)
|
||||||
|
- **功能描述**:现有 portal-shell `plugin-manager` 升级整页
|
||||||
|
- **技术栈**:依据 portal-shell 已有 plugin-manager 演化
|
||||||
|
- **ARCHITECTURE.md 契约**:✅ `config-service`(已就绪,可优先实现)
|
||||||
|
|
||||||
|
### 2.13 ARCH §9.4 规划但 CICD 无源的"新建"页面(3 页)
|
||||||
|
|
||||||
|
#### 2.13.1 `/shell/admin/students`(学生管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:**无**
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **建议**:参照 CICD 中 `admin/school/classes` 等管理页设计,作学生维度聚合视图。
|
||||||
|
|
||||||
|
#### 2.13.2 `/shell/admin/teachers`(教师管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:**无**
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **建议**:参照 CICD 中 `admin/users` + `getStaffOptions()` 演化。
|
||||||
|
|
||||||
|
#### 2.13.3 `/shell/admin/organization`(组织管理)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:**无**
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B5)
|
||||||
|
- **建议**:合并 `school/departments` + `school/grades` + 师生维度,作组织架构视图。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、按 ARCH §9.4 模块汇总
|
||||||
|
|
||||||
|
| 模块 | ARCH 规划页数 | portal-shell 已完成 | 缺失 | 契约状态 |
|
||||||
|
| ---------------- | ---------------------------------------------------------------- | ------------------- | ------------------ | ------------------- |
|
||||||
|
| dashboard | 1 | 1 | 0 | ✅ `adminDashboard` |
|
||||||
|
| users | 1(+1 import 子页) | 0 | 1(+1 子页) | ❌ |
|
||||||
|
| roles | 1 | 0 | 1 | ❌ |
|
||||||
|
| permissions | 1 | 0 | 1 | ❌ |
|
||||||
|
| audit-logs | 4(含 overview/login/data-changes) | 0 | 4 | ❌ |
|
||||||
|
| invitation-codes | 1 | 0 | 1 | ❌ |
|
||||||
|
| school | 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 页。
|
||||||
332
apps/portal-shell/docs/needtodo/management-grade-NeedTodo.md
Normal file
332
apps/portal-shell/docs/needtodo/management-grade-NeedTodo.md
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
# 年级组域(Management/Grade)待完成功能分析
|
||||||
|
|
||||||
|
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\management\grade\`(5 个 page.tsx)
|
||||||
|
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\`(**无对应页面,全部缺失**)
|
||||||
|
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.5 缺口新增(5 页,B2/B5 末 N)
|
||||||
|
> 分析日期:2026-07-24
|
||||||
|
> 分析方式:逐页面读取 CICD page.tsx 源码 + 检查 portal-shell 对应路由是否存在
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 〇、方法论与对比基线
|
||||||
|
|
||||||
|
1. CICD 单体(Next.js App Router + Server Actions + Drizzle)作为**功能基线**,反映"老版单体已实现"的年级组功能完整态。
|
||||||
|
2. portal-shell 作为**目标态**,遵循 ARCH §9.5 的"缺口新增"规划(B2/B5 末,N=新建)。
|
||||||
|
3. CICD 的 5 个页面位于 `(dashboard)/management/grade/` 顶级路由组下,**不属于 /teacher 也不属于 /admin**,是独立的"年级组管理"维度,按 `GRADE_MANAGE` / `GRADE_RECORD_READ` / `ADAPTIVE_PRACTICE_READ` 三类权限放行。
|
||||||
|
4. CICD 的 `navigation.ts` 显示该菜单组同时出现在 `teacher` 与 `grade_head` 两个角色的导航中(permission 均为 `Permissions.GRADE_MANAGE`),即"年级组长"既可以是带 `grade_head` 角色的独立账号,也可以是普通教师被授予 `GRADE_MANAGE` 权限。
|
||||||
|
5. portal-shell 现有目录结构仅含 `/shell/{teacher|admin|student|parent|dev|forbidden}/*`,**未规划 `/shell/management/*` 独立段**,且 `route-permissions.ts` 中无任何 `management/grade` 条目。
|
||||||
|
6. ARCH §9.5 表格仅以"缺口新增"一行汇总 10 页(5 grade + 5 register/onboarding/privacy/terms),未细化每页的目标路由与契约;本文档补充细化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、页面完成度总览
|
||||||
|
|
||||||
|
| 状态 | 数量 | 说明 |
|
||||||
|
| ------- | ---- | -------------------------------------- |
|
||||||
|
| ❌ 缺失 | 5 | 5 个页面在 portal-shell 中均无对应实现 |
|
||||||
|
| 🟡 部分 | 0 | — |
|
||||||
|
| ✅ 完成 | 0 | — |
|
||||||
|
|
||||||
|
> portal-shell 路径检查:`apps/portal-shell/src/app/shell/**/management/**` 与 `apps/portal-shell/src/app/shell/**/grade/**` Glob 均返回 "No file found"。`route-permissions.ts` 亦无 `management` / `grade` 关键字命中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、缺失页面清单
|
||||||
|
|
||||||
|
### 2.1 年级组入口重定向(page.tsx)
|
||||||
|
|
||||||
|
- **CICD 路径**:`e:\desktop\CICD\src\app\(dashboard)\management\grade\page.tsx`
|
||||||
|
- **建议目标路由**:`/shell/teacher/management/grade`(见 §三角色归属分析)
|
||||||
|
- **功能描述**:空页面入口,仅做权限校验与重定向:
|
||||||
|
- `await requirePermission(Permissions.GRADE_MANAGE)` — 校验年级管理权限
|
||||||
|
- `redirect("/management/grade/classes")` — 重定向到班级列表页
|
||||||
|
- **技术栈**:
|
||||||
|
- Next.js `redirect` (from `next/navigation`)
|
||||||
|
- `requirePermission` 共享鉴权工具(`@/shared/lib/auth-guard`)
|
||||||
|
- `Permissions.GRADE_MANAGE` 权限位(`grade:manage`)
|
||||||
|
- `export const dynamic = "force-dynamic"` — 强制动态渲染(避免静态缓存鉴权信息)
|
||||||
|
- **权限**:`GRADE_MANAGE`(`grade:manage`)— 年级组长专属权限
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(§9.5 缺口新增,无 GraphQL schema)
|
||||||
|
- **批次**:B2/B5 末(N)
|
||||||
|
|
||||||
|
### 2.2 年级组仪表盘(dashboard)
|
||||||
|
|
||||||
|
- **CICD 路径**:`e:\desktop\CICD\src\app\(dashboard)\management\grade\dashboard\page.tsx`
|
||||||
|
- **建议目标路由**:`/shell/teacher/management/grade/dashboard`
|
||||||
|
- **功能描述**:年级组长仪表盘,包含 4 个 Tab 切换的概览面板:
|
||||||
|
- **年级筛选器**(FilterBar + ChipNav):拉取 `getGradesForStaff(teacherId)` 得到该教师可见年级列表,URL `?gradeId=xxx` 即时切换,无整页刷新
|
||||||
|
- **Tab distribution(成绩分布)**:调用 `getGradeDistributionByGradeId({ gradeId, scope: ctx.dataScope })`,渲染 `GradeDistributionPanel`
|
||||||
|
- **Tab homework(作业洞察)**:调用 `getGradeHomeworkInsights({ gradeId, limit: 50 })`,渲染 `GradeHomeworkPanel`
|
||||||
|
- **Tab exams(考试列表)**:调用 `getExamsByGradeId({ gradeId, scope: ctx.dataScope })`,渲染 `GradeExamsPanel`
|
||||||
|
- **Tab progress(课程计划进度)**:调用 `getGradeCoursePlanProgress({ gradeId })`,渲染 `GradeProgressPanel`
|
||||||
|
- **懒加载策略**:仅渲染当前激活 Tab 的数据(按 `tab` 参数选择性 fetch),其他 Tab 不预取
|
||||||
|
- **空状态**:无可见年级 / 未选年级 / 数据为 null 三种 EmptyState 兜底
|
||||||
|
- **错误边界**:`SectionErrorBoundary` 包裹面板区域(namespace="school")
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component(`force-dynamic`)+ `generateMetadata` 动态标题(i18n `school.grades.gradeDashboard.*`)
|
||||||
|
- `next-intl/server` 的 `getTranslations`
|
||||||
|
- `getParam` / `SearchParams` 工具(`@/shared/lib/search-params`)
|
||||||
|
- 跨模块数据访问:`classes` / `school` / `grades` / `exams` / `course-plans` 5 个模块的 `data-access`
|
||||||
|
- 共享 UI:`FilterBar`、`ChipNav`、`EmptyState`、`SectionErrorBoundary`
|
||||||
|
- 业务组件:`modules/school/components/grade-dashboard/grade-{distribution,homework,exams,progress}-panel`
|
||||||
|
- DataScope 二次过滤:`ctx.dataScope` 传入查询,按教师可见范围过滤
|
||||||
|
- **权限**:`GRADE_RECORD_READ`(`grade_record:read`)— 比入口权限宽松,允许只读查看成绩的教师访问
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(5 个 data-access 全部走 Drizzle 直查 DB,未走 GraphQL schema)
|
||||||
|
- **批次**:B2/B5 末(N)
|
||||||
|
|
||||||
|
### 2.3 年级组班级列表(classes)
|
||||||
|
|
||||||
|
- **CICD 路径**:`e:\desktop\CICD\src\app\(dashboard)\management\grade\classes\page.tsx`
|
||||||
|
- **建议目标路由**:`/shell/teacher/management/grade/classes`
|
||||||
|
- **功能描述**:年级组长管辖的班级管理页,并行拉取三组数据:
|
||||||
|
- `getGradeManagedClasses(userId)` — 该用户作为年级组长管辖的班级列表
|
||||||
|
- `getTeacherOptions()` — 教师下拉选项(用于班主任分配)
|
||||||
|
- `getManagedGrades(userId)` — 该用户管辖的年级列表
|
||||||
|
- 渲染 `GradeClassesClient` 客户端组件,承载班级 CRUD(建/删/改)交互
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component(`force-dynamic`)+ `generateMetadata`(i18n `school.classManagement.grade.*`)
|
||||||
|
- `Promise.all` 并行数据获取
|
||||||
|
- 客户端组件 `GradeClassesClient`(`modules/classes/components/grade-classes-view.tsx`)
|
||||||
|
- 客户端组件依赖:
|
||||||
|
- `createGradeClassAction` / `deleteGradeClassAction` / `updateGradeClassAction`(Server Actions,`modules/classes/actions.ts`)
|
||||||
|
- `useClassData` Hook(`modules/classes/hooks/use-class-data.ts`)
|
||||||
|
- `ClassDeleteDialog` / `ClassFormDialog` / `ClassListTable` / `ClassListToolbar`(4 个子组件)
|
||||||
|
- `ctx.userId` 用于 owner 维度过滤
|
||||||
|
- **权限**:`GRADE_MANAGE`(`grade:manage`)— 年级组长写权限
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(5 个 data-access 全走 Drizzle,无 GraphQL)
|
||||||
|
- **批次**:B2/B5 末(N)
|
||||||
|
- **迁移注意**:portal-shell 现有 `/shell/teacher/classes` 是普通教师班级列表(教师授课班级),与年级组长管辖的班级列表**语义不同**(前者按 `teacherId` 关联 `class_teachers`,后者按年级组管辖范围 `grade_head`)。**不可合并**。
|
||||||
|
|
||||||
|
### 2.4 年级组作业洞察(insights)
|
||||||
|
|
||||||
|
- **CICD 路径**:`e:\desktop\CICD\src\app\(dashboard)\management\grade\insights\page.tsx`
|
||||||
|
- **建议目标路由**:`/shell/teacher/management/grade/insights`
|
||||||
|
- **功能描述**:年级组作业洞察页,单年级作业统计与班级排名:
|
||||||
|
- **年级筛选器**(FilterBar + ChipNav):`getGradesForStaff(teacherId)` + `?gradeId=xxx` 切换
|
||||||
|
- **4 个统计卡(StatCard)**:
|
||||||
|
- 班级数(`insights.classCount`)
|
||||||
|
- 学生数(`studentCounts.total` / `active` / `inactive`)
|
||||||
|
- 整体平均分(`overallScores.avg`)
|
||||||
|
- 最近作业平均分(`insights.latest.scoreStats.avg` + 作业标题)
|
||||||
|
- **作业时间线表(Table)**:每个作业一行,列含:作业名 / 状态 / 创建时间 / 应交 / 已交 / 已批 / 平均分 / 中位数分
|
||||||
|
- **班级排名表(Table)**:每个班一行,列含:班级名(含班主任)/ 学生数 / 最近作业平均 / 上次平均 / 涨跌 / 整体平均
|
||||||
|
- **三段空状态**:无年级 / 未选 / 数据为空 / 无 insights 各自 EmptyState
|
||||||
|
- **移动端适配**:`overflow-x-auto` 包裹两个 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。
|
||||||
725
apps/portal-shell/docs/needtodo/parent-NeedTodo.md
Normal file
725
apps/portal-shell/docs/needtodo/parent-NeedTodo.md
Normal file
@@ -0,0 +1,725 @@
|
|||||||
|
# 家长域(Parent)待完成功能分析
|
||||||
|
|
||||||
|
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\parent\`
|
||||||
|
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\parent\`
|
||||||
|
> 规划依据:`apps/portal-shell/ARCHITECTURE.md` §9.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 <EmptyState icon={ShieldAlert} ... /> }
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **此页是家长域最核心页面**,是其他模块(grades/attendance/homework 等)子女切换的"枢纽"。`myChildren` 契约必须在 B4 首位补齐。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 grades 模块(2 页规划,0 已完成,2 缺失)
|
||||||
|
|
||||||
|
#### 2.3.1 ❌ `/shell/parent/grades`(子女成绩列表,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\grades\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- **多子女成绩对比视图**(家长域特色)
|
||||||
|
- 每个子女一块:姓名标题 + **导出按钮**(`ParentExportButton`,按 studentId 导出)
|
||||||
|
- **成绩趋势卡片**(`GradeTrendCard`,含班级平均对比线,v3-P2-8 补齐)
|
||||||
|
- **纵向成长档案图**(`GrowthArchiveChart`,P3-4 新增,跨学年/学期聚合)
|
||||||
|
- 学生成绩汇总(`StudentGradeSummary`)
|
||||||
|
- 使用 `Promise.allSettled` 容错:单个子女查询失败不影响其他子女展示
|
||||||
|
- 用循环 + 类型守卫替代 `as` 断言(P1-8 修复)
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getStudentGradeSummary(id, ctx.dataScope)` / `getClassAverageTrend(id, undefined, undefined, ctx.dataScope)` / `getStudentGrowthArchive(id, ctx.dataScope)`
|
||||||
|
- `requirePermission(Permissions.GRADE_RECORD_READ)`
|
||||||
|
- dataScope 校验:`ctx.dataScope.type === "children"` 且 `childrenIds.length > 0`,否则显示 `ParentNoChildrenPage`
|
||||||
|
- 组件:`ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentExportButton` / `GradeTrendCard` / `GrowthArchiveChart` / `StudentGradeSummary`
|
||||||
|
- 类型:`ClassAverageTrendResult` / `StudentGrowthArchiveResult`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(列表,未定义)
|
||||||
|
|
||||||
|
#### 2.3.2 ❌ `/shell/parent/grades/report-card`(子女成绩报告卡,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\grades\report-card\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- 学年 / 学期可切换的成绩报告卡(家长视角)
|
||||||
|
- **必填查询参数**:`studentId`(必须在家长子女范围内)
|
||||||
|
- 可选参数:`academicYearId` / `semester`("1" | "2")
|
||||||
|
- 缺少 `studentId` 时显示提示空态 + 返回按钮
|
||||||
|
- 报告卡视图(`ReportCardView`,子女维度聚合)
|
||||||
|
- **打印操作**(`ReportCardPrintAction`)
|
||||||
|
- 返回成绩列表的面包屑(`ArrowLeft` + Link)
|
||||||
|
- 学年数量提示
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getReportCardData(studentId, ctx.dataScope, { academicYearId, semester })` + `getAcademicYears()`
|
||||||
|
- `requirePermission(Permissions.GRADE_RECORD_READ)`
|
||||||
|
- 组件:`ReportCardView` / `ReportCardPrintAction` / `Button` / `EmptyState`
|
||||||
|
- 工具:`getParam` / `SearchParams` 类型
|
||||||
|
- searchParams:`studentId` / `academicYearId` / `semester`
|
||||||
|
- dataScope 在 data-access 层校验子女归属(防跨家庭泄露)
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(未单独定义)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 exams 模块(2 页规划,0 已完成,2 缺失)⚠️ CICD 无源
|
||||||
|
|
||||||
|
> **CICD 参考项目无 exams 模块家长页面**。家长考试相关功能需 portal-shell 从零设计。
|
||||||
|
|
||||||
|
#### 2.4.1 ❌ `/shell/parent/exams`(子女考试列表,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:❌ CICD 无对应源页面
|
||||||
|
- **功能描述**:家长视角的子女考试列表,应展示:
|
||||||
|
- 多子女考试汇总(每个子女一块)
|
||||||
|
- 即将到来的考试(含倒计时、考场、座位号)
|
||||||
|
- 已结束考试(含成绩、查看结果入口)
|
||||||
|
- 按学科 / 状态过滤
|
||||||
|
- **技术栈建议**:参考 CICD `parent/grades/page.tsx` 的 `ParentChildrenDataPage` 多子女布局模式 + `parent/practice/page.tsx` 的单/多子女分支渲染
|
||||||
|
- **ARCHITECTURE.md 契约**:🟡(部分,需补充)
|
||||||
|
|
||||||
|
#### 2.4.2 ❌ `/shell/parent/exams/[id]/result`(子女考试结果页,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:❌ CICD 无对应源页面(可参考 `parent/grades/report-card/page.tsx` 的 searchParams + 权限校验模式)
|
||||||
|
- **功能描述**:
|
||||||
|
- 子女考试得分、排名、班级均分对比
|
||||||
|
- 题目作答详情(每题对错、参考答案、子女答案)
|
||||||
|
- 知识点掌握情况
|
||||||
|
- 错题一键加入错题本入口
|
||||||
|
- **技术栈建议**:参考 CICD `parent/grades/report-card/page.tsx` 的 `verifyParentChildRelationAction` + dataScope 双重校验模式
|
||||||
|
- **ARCHITECTURE.md 契约**:🟡(部分,需补充)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 homework 模块(1 页规划,0 已完成,1 缺失)⚠️ CICD 无源
|
||||||
|
|
||||||
|
#### 2.5.1 ❌ `/shell/parent/homework`(子女作业查看,缺失)⚠️ CICD 无源
|
||||||
|
|
||||||
|
- **CICD 参考实现**:❌ CICD 无对应源页面
|
||||||
|
- **功能描述**:家长视角查看子女作业,应展示:
|
||||||
|
- 多子女作业汇总(每个子女一块)
|
||||||
|
- 作业列表(标题、状态徽章、截止时间、剩余尝试次数、最新得分、逾期提示)
|
||||||
|
- 按学科分组 + 状态过滤(all/pending/submitted/graded)
|
||||||
|
- **只读视图**(家长不能提交作业,仅查看)
|
||||||
|
- **技术栈建议**:参考 CICD `parent/practice/page.tsx` 的 `ParentChildrenDataPage` + 单/多子女分支 + `StatsGrid` 统计模式;状态徽章参考学生域 `StatusBadge` + `variantMap`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 attendance 模块(1 页规划,0 已完成,1 缺失)
|
||||||
|
|
||||||
|
#### 2.6.1 ❌ `/shell/parent/attendance`(子女考勤,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\attendance\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- **多子女考勤对比视图**(家长域特色)
|
||||||
|
- 顶部 `headerExtra`:出勤率卡片(`ParentAttendanceRateCard`)+ 考勤预警(`ParentAttendanceWarning`)
|
||||||
|
- 每个子女一块:姓名标题 + **考勤日历**(`ParentAttendanceCalendar`)+ **考勤明细**(`ParentStudentAttendanceDetail`)
|
||||||
|
- 使用 `Promise.allSettled` 容错
|
||||||
|
- **类型映射解耦**:将 attendance 模块的 `StudentAttendanceSummary` 映射为 parent 模块的 `ParentStudentAttendanceSummary`(P1-2 解耦,parent 模块仅消费自身类型)
|
||||||
|
- 通过接口抽象消费 attendance 数据(P1-2 修复:不再直接 import data-access)
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`createAttendanceReadService(ctx.dataScope).getStudentSummary(id)`(接口抽象)
|
||||||
|
- `getAuthContext()`(注意:未用 `requirePermission`,直接取 ctx)
|
||||||
|
- dataScope 校验:`ctx.dataScope.type === "children"` 且 `childrenIds.length > 0`,否则显示 `ParentNoChildrenPage`
|
||||||
|
- 组件:`ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentAttendanceWarning` / `ParentAttendanceRateCard` / `ParentAttendanceCalendar` / `ParentStudentAttendanceDetail`
|
||||||
|
- 类型:`ParentStudentAttendanceSummary` / `AttendanceReadService`
|
||||||
|
- 映射函数:`toParentSummary`(手动字段映射)
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 classes 模块(1 页规划,0 已完成,1 缺失)⚠️ CICD 无源
|
||||||
|
|
||||||
|
#### 2.7.1 ❌ `/shell/parent/classes`(子女班级信息,缺失)⚠️ CICD 无源
|
||||||
|
|
||||||
|
- **CICD 参考实现**:❌ CICD 无对应源页面(CICD 仅在 `course-plans` 中通过 `getStudentActiveClassId` 间接消费班级数据)
|
||||||
|
- **功能描述**:家长视角的子女班级信息,应展示:
|
||||||
|
- 多子女班级列表(每个子女一块)
|
||||||
|
- 班级基本信息(班名、班主任、任课教师、教室)
|
||||||
|
- 班级近期动态(作业、考试、公告)
|
||||||
|
- **技术栈建议**:参考 CICD `parent/course-plans/page.tsx` 的 `getStudentActiveClassId(sid)` 多子女并行查询模式 + `ParentChildrenDataPage` 布局
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8 course-plans 模块(2 页规划,0 已完成,2 缺失)
|
||||||
|
|
||||||
|
#### 2.8.1 ❌ `/shell/parent/course-plans`(课程计划列表,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\course-plans\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- 家长视角:解析**所有孩子的班级 ID**,用于过滤课程计划
|
||||||
|
- 并行查询每个子女的活跃班级 ID(`getStudentActiveClassId`)
|
||||||
|
- 仅查询 `status: "active"` 的课程计划
|
||||||
|
- 使用 `CoursePlanList` 组件渲染(`detailBaseHref="/parent/course-plans"`)
|
||||||
|
- 权限上下文:`{ userId, isAdmin: false, classIds }`(家长非管理员,仅限指定班级)
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getCoursePlans({ status: "active" }, { userId, isAdmin: false, classIds })` + `getStudentActiveClassId(sid)`(并行 `Promise.all`)
|
||||||
|
- `requirePermission(Permissions.COURSE_PLAN_READ)`
|
||||||
|
- dataScope 类型守卫:`ctx.dataScope.type === "children"` 且 `childrenIds.length > 0`
|
||||||
|
- 组件:`CoursePlanList`
|
||||||
|
- 类型守卫:`filter((id): id is string => id !== null)`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
#### 2.8.2 ❌ `/shell/parent/course-plans/[id]`(课程计划详情,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\course-plans\[id]\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- 家长视角:仅允许查看**孩子所在班级**的课程计划
|
||||||
|
- 解析所有孩子的班级 ID,传入权限上下文
|
||||||
|
- 若计划不在子女班级范围内,`notFound()` 兜底
|
||||||
|
- 使用 `CoursePlanDetail` 组件渲染(`backHref` / `successHref` 均指向 `/parent/course-plans`)
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getCoursePlanById(id, { userId, isAdmin: false, classIds })` + `getStudentActiveClassId(sid)`(并行)
|
||||||
|
- `requirePermission(Permissions.COURSE_PLAN_READ)`
|
||||||
|
- 组件:`CoursePlanDetail`
|
||||||
|
- 路由参数:`id`
|
||||||
|
- `notFound()` 兜底
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.9 lesson-plans 模块(2 页规划,0 已完成,2 缺失)
|
||||||
|
|
||||||
|
#### 2.9.1 ❌ `/shell/parent/lesson-plans`(教案列表,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\lesson-plans\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- 家长视角:仅查看**已发布**教案(`status: "published"`)
|
||||||
|
- 学科选项并行加载(`getSubjectOptions`)
|
||||||
|
- **角色配置注入**:`LessonPlanProviderSetup` 包裹,传入 `PARENT_ROLE_CONFIG`,使筛选功能生效(P0-13 修复)
|
||||||
|
- `LessonPlanList` 以 `viewMode="parent"` 渲染(只读模式)
|
||||||
|
- `Suspense` 流式渲染 + Skeleton 占位(6 个 `h-[180px]` 卡片骨架)
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getLessonPlans({ status: "published" }, ctx.dataScope, ctx.userId)` + `getSubjectOptions()`
|
||||||
|
- `requirePermission(Permissions.LESSON_PLAN_READ)`(V4 P0-2 修复:页面层补齐权限校验)
|
||||||
|
- 组件:`LessonPlanList` / `LessonPlanProviderSetup` / `Skeleton`
|
||||||
|
- Provider 配置:`PARENT_ROLE_CONFIG`(来自 `@/modules/lesson-preparation/providers/lesson-plan-provider`)
|
||||||
|
- `viewMode="parent"` 标识家长只读视图
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
#### 2.9.2 ❌ `/shell/parent/lesson-plans/[planId]/view`(教案只读详情,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\lesson-plans\[planId]\view\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- 家长视角:仅可查看**孩子所在年级**的已发布课案(V4 P0-1 修复,防跨年级信息泄露)
|
||||||
|
- **scope 校验**:`assertPlanInScope(plan, ctx)`,失败 `notFound()`
|
||||||
|
- 未发布计划显示提示卡片(`readonly.notPublished`)
|
||||||
|
- 教材标题 + 章节标题并行查询(V4 P2-1 修复,`Promise.all` 并行)
|
||||||
|
- `LessonPlanReadonlyView` 渲染计划内容
|
||||||
|
- 全屏布局(`h-[calc(100vh-4rem)]`)
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getLessonPlanById(planId, ctx.userId)` + `getTextbookById(plan.textbookId)` + `getChaptersByTextbookId(plan.textbookId)` + `findChapterById(chapters, plan.chapterId)`
|
||||||
|
- `requirePermission(Permissions.LESSON_PLAN_READ)`
|
||||||
|
- scope 校验:`assertPlanInScope(plan, ctx)`(lib/scope-check)
|
||||||
|
- 组件:`LessonPlanReadonlyView` / `Skeleton`
|
||||||
|
- 路由参数:`planId`
|
||||||
|
- `notFound()` 兜底(计划不存在或越权)
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.10 error-book 模块(1 页规划,0 已完成,1 缺失)
|
||||||
|
|
||||||
|
#### 2.10.1 ❌ `/shell/parent/error-book`(子女错题本,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\error-book\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- **单/多子女分支渲染**(家长域特色):
|
||||||
|
- **单子女**:直接展示 `StatsGrid` 5 列统计卡片(总数 / 新增 / 学习中 / 已掌握 / 待复习)
|
||||||
|
- **多子女**:每个子女一张 `Card`,含姓名 + 掌握率徽章 + 4 项统计 + `Progress` 进度条
|
||||||
|
- 汇总所有子女的错题:**Top 错题**(`TopWrongQuestions`,前 5)+ **薄弱知识点**(`getKnowledgePointWeakness`,前 5)
|
||||||
|
- 薄弱知识点:每条含知识点名称 + 错误次数 + 掌握率 + `Progress` 进度条
|
||||||
|
- 待复习数 > 0 时高亮(`highlight: stats.dueReviewCount > 0`)
|
||||||
|
- `WidgetBoundary` 降级容错
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getErrorBookStats(id)` + `getStudentNameMap(childrenIds)` + `getTopWrongQuestionsByStudentIds(childrenIds, 5)` + `getKnowledgePointWeakness(childrenIds, 5)`
|
||||||
|
- `requirePermission(Permissions.ERROR_BOOK_READ)`
|
||||||
|
- 组件:`StatsGrid` / `TopWrongQuestions` / `WidgetBoundary` / `Card` / `Badge` / `Progress` / `EmptyState`
|
||||||
|
- 工具:`formatNumber`
|
||||||
|
- icons:`BookX` / `Clock` / `GraduationCap` / `Repeat` / `Sparkles` / `Users`
|
||||||
|
- **ARCHITECTURE.md 契约**:✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.11 diagnostic 模块(1 页规划,0 已完成,1 缺失)
|
||||||
|
|
||||||
|
#### 2.11.1 ❌ `/shell/parent/diagnostic`(子女学情诊断,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\diagnostic\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- **多子女诊断对比视图**(v4-P1-9 容错增强)
|
||||||
|
- 预先查询所有子女姓名(`getUserNamesByIds`),用于错误卡片展示
|
||||||
|
- **allSettled 容错但保留 rejected 项**:rejected 项不再静默丢弃,渲染错误卡片(`AlertCircle` + 学生姓名 + "数据加载失败"提示)
|
||||||
|
- 每个子女:诊断摘要(`getStudentMasterySummary`)+ **已发布诊断报告**(`getDiagnosticReports`,v4-P1-3 修复:家长仅可见 `status: "published"` 报告,避免草稿泄露)
|
||||||
|
- `StudentDiagnosticView` 渲染(`role="parent"`)
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getStudentMasterySummary(id)` + `getDiagnosticReports({ studentId: id, status: "published" }, ctx.dataScope)` + `getUserNamesByIds(childrenIds)`
|
||||||
|
- `requirePermission(Permissions.DIAGNOSTIC_READ)`
|
||||||
|
- 组件:`ParentChildrenDataPage` / `ParentNoChildrenPage` / `StudentDiagnosticView` / `Card` / `CardContent`
|
||||||
|
- 类型:`ChildDiagnosticSuccessItem` / `ChildDiagnosticErrorItem`(联合类型 `ChildDiagnosticItem`)
|
||||||
|
- icons:`Stethoscope` / `AlertCircle`
|
||||||
|
- **ARCHITECTURE.md 契约**:✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.12 learning-path 模块(1 页规划,0 已完成,1 缺失)⚠️ CICD 无源
|
||||||
|
|
||||||
|
#### 2.12.1 ❌ `/shell/parent/learning-path`(子女学习路径,缺失)⚠️ CICD 无源
|
||||||
|
|
||||||
|
- **CICD 参考实现**:❌ CICD 无对应源页面(学生域有 `learning/study-path`,但家长域无对应页)
|
||||||
|
- **功能描述**:家长视角查看子女的 AI 学习路径,应展示:
|
||||||
|
- 多子女学习路径汇总(每个子女一块)
|
||||||
|
- AI 生成的个性化学习路径(参考学生域 `AiStudyPath` 组件)
|
||||||
|
- 路径进度、节点完成情况
|
||||||
|
- **技术栈建议**:参考 CICD 学生域 `learning/study-path/page.tsx` 的 `AiStudyPath` + `AiClientProvider` 模式 + `requirePermission(Permissions.AI_CHAT)`;家长域需扩展为多子女布局
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.13 practice 模块(1 页规划,0 已完成,1 缺失)
|
||||||
|
|
||||||
|
#### 2.13.1 ❌ `/shell/parent/practice`(子女练习查看,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\practice\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- **单/多子女分支渲染**(家长域特色):
|
||||||
|
- **单子女**:直接展示 4 列 `StatsGrid`(总练习次数 / 已完成 / 总答题数 / 正确率)+ `PracticeHistory` 历史列表
|
||||||
|
- **多子女**:每个子女一块 `WidgetBoundary`,含姓名标题 + 4 列统计 + 历史列表
|
||||||
|
- **家长无会话详情页**,仅展示只读卡片(注释明确:`家长无会话详情页,仅展示只读卡片`)
|
||||||
|
- `PracticeServiceProvider` 包裹(注入练习服务)
|
||||||
|
- `Promise.allSettled` 容错 + 循环 + 类型守卫(非 `as` 断言)
|
||||||
|
- 姓名映射并行启动(`nameMapPromise` 与 `childResults` 并行,但类型分离以利类型收窄)
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getPracticeStats(id)` + `getPracticeSessions(id, { pageSize: 20 })` + `getUserNamesByIds(childrenIds)`
|
||||||
|
- `requirePermission(Permissions.ADAPTIVE_PRACTICE_READ)`
|
||||||
|
- 组件:`PracticeServiceProvider` / `StatsGrid` / `PracticeHistory` / `WidgetBoundary` / `EmptyState`
|
||||||
|
- 类型:`PracticeSessionSummary` / `PracticeStats` / `ChildPracticeItem`
|
||||||
|
- icons:`Target` / `Users` / `CheckCircle2` / `TrendingUp` / `Award`
|
||||||
|
- `valueClassName: "tabular-nums"` 数字对齐
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.14 elective 模块(1 页规划,0 已完成,1 缺失)
|
||||||
|
|
||||||
|
#### 2.14.1 ❌ `/shell/parent/elective`(子女选课查看,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\elective\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- **多子女选课记录查看**(只读,家长不能选课)
|
||||||
|
- 每个子女一块:`ParentSelectionView`(子女姓名 + 选课记录列表)
|
||||||
|
- **双重校验**:`getChildrenAction()` 获取关系 + `getChildBasicInfoAction(r.studentId, r.relation)` 二次校验(防跨家庭信息泄露,G4-002 审计修复)
|
||||||
|
- `Promise.allSettled` 容错
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getStudentSelections(r.studentId)` + `getChildrenAction()` + `getChildBasicInfoAction(r.studentId, r.relation)`
|
||||||
|
- `requirePermission(Permissions.ELECTIVE_READ)`
|
||||||
|
- 组件:`ParentChildrenDataPage` / `ParentNoChildrenPage` / `ParentSelectionView`
|
||||||
|
- 类型:`ChildSelectionData`
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.15 leave 模块(1 页规划,0 已完成,1 缺失)
|
||||||
|
|
||||||
|
#### 2.15.1 ❌ `/shell/parent/leave`(家长在线请假,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\parent\leave\page.tsx`
|
||||||
|
- **功能描述**:
|
||||||
|
- **家长域少有的"写"操作页面**(其他多为只读)
|
||||||
|
- 顶部:**在线请假表单**(`LeaveRequestForm`,下拉选择子女,自动写入 classId)
|
||||||
|
- 底部:该家长**所有子女**的请假申请列表(`LeaveRequestList`,按 dataScope=children 过滤)
|
||||||
|
- 子女选项构造:遍历关系列表,查询每个子女的 `basicInfo`,仅包含有活跃班级的子女(`basic.classId && basic.className`)
|
||||||
|
- 返回仪表盘的面包屑(`ArrowLeft` + Link to `/parent/dashboard`)
|
||||||
|
- 无关联子女时显示提示卡片
|
||||||
|
- **技术栈**:
|
||||||
|
- Server Component + `force-dynamic`
|
||||||
|
- Server Actions:`getChildrenAction()` + `getLeaveRequests({ scope, currentUserId, page: 1, pageSize: 50 })` + `getChildBasicInfoAction(r.studentId, r.relation)`
|
||||||
|
- `getAuthContext()`(注意:未用 `requirePermission`,直接取 ctx)
|
||||||
|
- 组件:`LeaveRequestForm` / `LeaveRequestList` / `Card` / `Button`
|
||||||
|
- 类型:`ChildOption`(含 `id` / `name` / `classId` / `className`)
|
||||||
|
- icons:`ArrowLeft` / `CalendarDays`
|
||||||
|
- scope 处理:家长 scope 为 `children`,`getLeaveRequests` 按 scope 过滤所有子女记录
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.16 notifications + settings + preferences 模块(共享 2 页 + 家长专属 1 页,0 已完成,1 缺失 + 2 共享)
|
||||||
|
|
||||||
|
#### 2.16.1 ❌ `/shell/parent/preferences`(家长偏好设置,缺失)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:❌ CICD 无对应源页面
|
||||||
|
- **功能描述**:家长专属偏好设置,可能包括:
|
||||||
|
- 通知偏好(哪些子女事件需推送:成绩、考勤、作业、预警)
|
||||||
|
- AI 摘要偏好(摘要频率、详细程度)
|
||||||
|
- 隐私偏好(是否共享数据给 AI)
|
||||||
|
- **技术栈建议**:参考共享 settings 页面模式 + 家长域专属字段
|
||||||
|
- **ARCHITECTURE.md 契约**:❌
|
||||||
|
- **批次**:B4
|
||||||
|
|
||||||
|
#### 2.16.2 ❌ `/shell/notifications`(通知中心,缺失,共享)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:❌ CICD `parent/` 下无对应源页面
|
||||||
|
- **功能描述**:通知中心(家长可见范围)
|
||||||
|
- **ARCHITECTURE.md 契约**:✅(共享,已就绪)
|
||||||
|
- **批次**:B1/B3,共享路由
|
||||||
|
|
||||||
|
#### 2.16.3 ❌ `/shell/settings`(系统设置,缺失,共享)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:❌ CICD `parent/` 下无对应源页面
|
||||||
|
- **功能描述**:系统设置(通用)
|
||||||
|
- **ARCHITECTURE.md 契约**:✅(共享,已就绪)
|
||||||
|
- **批次**:B1/B3,共享路由
|
||||||
|
|
||||||
|
> **注**:portal-shell 当前 `/shell/notifications` / `/shell/settings` 路由是否存在需在共享层确认。家长域仅需补充 `/shell/parent/preferences`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、按模块汇总
|
||||||
|
|
||||||
|
| 模块 | 规划页数 | 已完成 | 缺失 | 备注 |
|
||||||
|
| --------------------- | -------- | ------ | ------ | --------------------------------------- |
|
||||||
|
| dashboard | 3 | 1 | 2 | 仪表盘基础页有,trend/weakness 详情页缺 |
|
||||||
|
| children | 1 | 0 | 1 | ⚠️ 家长域核心页,需 `myChildren` 契约 |
|
||||||
|
| grades | 2 | 0 | 2 | 多子女对比 + 报告卡 |
|
||||||
|
| exams | 2 | 0 | 2 | ⚠️ CICD 无源页面,全新设计 |
|
||||||
|
| homework | 1 | 0 | 1 | ⚠️ CICD 无源页面,全新设计 |
|
||||||
|
| attendance | 1 | 0 | 1 | 多子女考勤对比 |
|
||||||
|
| classes | 1 | 0 | 1 | ⚠️ CICD 无源页面,全新设计 |
|
||||||
|
| course-plans | 2 | 0 | 2 | 列表 + 详情,按子女班级过滤 |
|
||||||
|
| lesson-plans | 2 | 0 | 2 | 列表 + 只读详情,scope 校验 |
|
||||||
|
| error-book | 1 | 0 | 1 | 单/多子女分支,契约 ✅ |
|
||||||
|
| diagnostic | 1 | 0 | 1 | 多子女诊断,契约 ✅ |
|
||||||
|
| learning-path | 1 | 0 | 1 | ⚠️ CICD 无源页面,全新设计 |
|
||||||
|
| practice | 1 | 0 | 1 | 单/多子女分支,只读 |
|
||||||
|
| elective | 1 | 0 | 1 | 多子女选课查看 |
|
||||||
|
| leave | 1 | 0 | 1 | 家长域少有的"写"操作 |
|
||||||
|
| preferences | 1 | 0 | 1 | 家长专属偏好 |
|
||||||
|
| notifications(共享) | 1 | 0 | 1 | 共享路由,契约 ✅ |
|
||||||
|
| settings(共享) | 1 | 0 | 1 | 共享路由,契约 ✅ |
|
||||||
|
| **合计** | **23** | **1** | **22** | 不含 dashboard 的 2 个详情页已计入缺失 |
|
||||||
|
|
||||||
|
> ARCHITECTURE.md §9.3 规划 24 页(含 notifications/settings 共享路由 2 页)。portal-shell 当前 1 页已完成,22 页缺失,1 页为 dashboard 详情页(部分完成)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、关键技术差异 & 迁移注意事项
|
||||||
|
|
||||||
|
### 4.1 架构模式差异(最重要)
|
||||||
|
|
||||||
|
| 维度 | CICD(参考) | portal-shell(目标) |
|
||||||
|
| ------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||||
|
| 渲染模式 | Server Component 为主 | 当前仪表盘是 Client Component,需确定后续页面模式 |
|
||||||
|
| 数据获取 | Server Action + Drizzle 直查 DB | **必须改为** BFF / 微服务 API(`teacher-bff` / `data-ana` 等) |
|
||||||
|
| 权限校验 | `requirePermission` + `getAuthContext` + dataScope | 需通过 Gateway + BFF 鉴权,前端用 `usePermission().hasPermission()` |
|
||||||
|
| 国际化 | `next-intl` `getTranslations` | portal-shell 当前仪表盘硬编码中文,需统一策略 |
|
||||||
|
| 共享 UI | `@/shared/components/ui/*` | `@/shared/components/ui/*`(已迁移部分) |
|
||||||
|
| 模块组件 | `@/modules/<domain>/components/*` | 需在 portal-shell 重建或下沉到 BFF |
|
||||||
|
| 多子女布局 | `ParentChildrenDataPage` + `ParentNoChildrenPage` 通用容器 | 需在 portal-shell 重建(家长域核心模式) |
|
||||||
|
| 单/多子女分支 | error-book / practice 中 `isSingleChild` 分支 | 需保留此模式(家长域特色) |
|
||||||
|
|
||||||
|
### 4.2 路由路径对齐
|
||||||
|
|
||||||
|
| CICD 路径 | ARCHITECTURE.md §9.3 目标路径 | 备注 |
|
||||||
|
| ----------------------------------- | ------------------------------------------ | ----------------------------------------------- |
|
||||||
|
| `parent/dashboard` | `/shell/parent`(家长仪表盘) | 提升到 `/shell/parent/page.tsx` |
|
||||||
|
| `parent/dashboard/trend` | `/shell/parent/trend` | CICD 无独立页,需新建 |
|
||||||
|
| `parent/dashboard/weakness` | `/shell/parent/weakness` | CICD 无独立页,需新建 |
|
||||||
|
| `parent/children/[studentId]` | `/shell/parent/children/[studentId]` | 路径一致 |
|
||||||
|
| `parent/grades` | `/shell/parent/grades` | 路径一致 |
|
||||||
|
| `parent/grades/report-card` | `/shell/parent/grades/report-card` | 路径一致 |
|
||||||
|
| `(无)` | `/shell/parent/exams` | 全新设计 |
|
||||||
|
| `(无)` | `/shell/parent/exams/[id]/result` | 全新设计 |
|
||||||
|
| `(无)` | `/shell/parent/homework` | 全新设计 |
|
||||||
|
| `parent/attendance` | `/shell/parent/attendance` | 路径一致 |
|
||||||
|
| `(无)` | `/shell/parent/classes` | 全新设计 |
|
||||||
|
| `parent/course-plans` | `/shell/parent/course-plans` | 路径一致 |
|
||||||
|
| `parent/course-plans/[id]` | `/shell/parent/course-plans/[id]` | 路径一致 |
|
||||||
|
| `parent/lesson-plans` | `/shell/parent/lesson-plans` | 路径一致 |
|
||||||
|
| `parent/lesson-plans/[planId]/view` | `/shell/parent/lesson-plans/[planId]/view` | 路径一致 |
|
||||||
|
| `parent/error-book` | `/shell/parent/error-book` | 路径一致 |
|
||||||
|
| `parent/diagnostic` | `/shell/parent/diagnostic` | 路径一致 |
|
||||||
|
| `(无)` | `/shell/parent/learning-path` | 全新设计(学生域有 `learning/study-path` 参考) |
|
||||||
|
| `parent/practice` | `/shell/parent/practice` | 路径一致 |
|
||||||
|
| `parent/elective` | `/shell/parent/elective` | 路径一致 |
|
||||||
|
| `parent/leave` | `/shell/parent/leave` | 路径一致 |
|
||||||
|
| `(无)` | `/shell/parent/preferences` | 全新设计 |
|
||||||
|
| 共享 | `/shell/notifications` | 共享路由 |
|
||||||
|
| 共享 | `/shell/settings` | 共享路由 |
|
||||||
|
|
||||||
|
> **注**:家长域路由迁移**压力较小**,14 个 CICD 页面中有 13 个路径完全一致(仅 dashboard 从子路径提升到 `/shell/parent/page.tsx`)。需新建的 6 个页面(exams×2、homework、classes、learning-path、preferences)在 CICD 中无源。
|
||||||
|
|
||||||
|
### 4.3 权限点映射
|
||||||
|
|
||||||
|
CICD 中家长页面用到的权限点(迁移时需在 portal-shell `Permissions` 常量中对应):
|
||||||
|
|
||||||
|
- `GRADE_RECORD_READ`(grades / report-card)
|
||||||
|
- `ATTENDANCE_READ`(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 中无源,需从零设计。**
|
||||||
374
apps/portal-shell/docs/needtodo/shared-NeedTodo.md
Normal file
374
apps/portal-shell/docs/needtodo/shared-NeedTodo.md
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
# 共享路由(Shared)待完成功能分析
|
||||||
|
|
||||||
|
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\`
|
||||||
|
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\`
|
||||||
|
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.1 / §9.2 / §9.3 / §9.5(共享 ~7 页)
|
||||||
|
> 分析时间:2026-07-24
|
||||||
|
> 分析范围:仅分析,不写代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、页面完成度总览
|
||||||
|
|
||||||
|
| 状态 | 路径 | 说明 |
|
||||||
|
| ---- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| ✅ | `/login` | 登录页 |
|
||||||
|
| ✅ | `/shell` | 仪表盘根(按角色分发到 `/shell/{teacher\|student\|parent\|admin}`)。**CICD 参考**:无单一分发器——CICD 根 `src/app/page.tsx` 直接 `redirect("/dashboard")`(教师仪表盘);各角色仪表盘分散于 `(dashboard)/dashboard/`、`(dashboard)/student/dashboard/`、`(dashboard)/parent/dashboard/`、`(dashboard)/admin/dashboard/`。portal-shell 实现于 `src/app/shell/[[...route]]/page.tsx`(catch-all,空路由 → `redirect("/shell/{role}")`)+ `src/app/shell/layout.tsx`(AppFrame 框架,fail-closed 读 `x-user-role` 头) |
|
||||||
|
| ✅ | `/shell/forbidden` | 403 页(`PUBLIC_ROUTES` 白名单) |
|
||||||
|
| ❌ | `/shell/notifications` | 通知中心(缺失) |
|
||||||
|
| ❌ | `/shell/settings` | 设置(缺失) |
|
||||||
|
| ❌ | `/shell/settings/security` | 安全中心 / 改密码(缺失) |
|
||||||
|
| ❌ | `/shell/announcements` | 公告列表(缺失) |
|
||||||
|
| ❌ | `/shell/announcements/[id]` | 公告详情(缺失) |
|
||||||
|
| ❌ | `/shell/messages` | 消息(缺失) |
|
||||||
|
| ❌ | `/shell/messages/compose` | 撰写消息(缺失) |
|
||||||
|
| ❌ | `/shell/messages/[id]` | 消息详情(缺失) |
|
||||||
|
| ❌ | `/shell/profile` | 个人资料(缺失) |
|
||||||
|
|
||||||
|
> 备注 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 行,渲染 `<RegisterForm registerAction={registerAction} />`);布局 `e:\desktop\CICD\src\app\(auth)\layout.tsx` 注入品牌配置(`getBrandConfig()`,失败回退默认)
|
||||||
|
- **功能描述**:账号注册页。表单字段:姓名 / 邮箱 / 密码 / 出生年月日 / 邀请码(可选,提供时覆盖默认 student 角色)。未成年人保护:根据 `birthDate` 计算 `age`,`< 18` 触发监护人信息区(监护人姓名 / 电话 / 关系下拉)+ 监护人同意复选框。必勾"同意《隐私政策》与《用户协议》"复选框(链接到 `/privacy`、`/terms`,`target="_blank"` 新窗打开)。注册成功 → `router.push("/login")` 跳登录页
|
||||||
|
- **技术栈**:RSC + `RegisterForm` 客户端组件(`modules/auth/components/register-form.tsx`)+ `registerAction` Server Action(`modules/auth/actions`)+ `next-intl`(`useTranslations("auth.register")`)+ shadcn 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/*`),无需补齐。
|
||||||
795
apps/portal-shell/docs/needtodo/student-NeedTodo.md
Normal file
795
apps/portal-shell/docs/needtodo/student-NeedTodo.md
Normal file
@@ -0,0 +1,795 @@
|
|||||||
|
# 学生域(Student)待完成功能分析
|
||||||
|
|
||||||
|
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\student\`
|
||||||
|
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\student\`
|
||||||
|
> 规划依据:`apps/portal-shell/ARCHITECTURE.md` §9.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/<domain>/components/*` | 需在 portal-shell 重建或下沉到 BFF |
|
||||||
|
|
||||||
|
### 4.2 路由路径对齐
|
||||||
|
|
||||||
|
| CICD 路径 | ARCHITECTURE.md §9.2 目标路径 | 备注 |
|
||||||
|
| -------------------------------------------- | ----------------------------- | --------------------------- |
|
||||||
|
| `learning/assignments` | `homework` | 提升到顶层 |
|
||||||
|
| `learning/assignments/[assignmentId]` | `homework/[id]/submit` | 路径参数改名 |
|
||||||
|
| `learning/assignments/[assignmentId]/result` | `homework/[id]/analysis` | 子路径改名 |
|
||||||
|
| `learning/courses` | `courses` | 提升到顶层 |
|
||||||
|
| `learning/courses/[classId]` | `courses/[id]` | 参数名 `classId` → `id` |
|
||||||
|
| `learning/textbooks` | `textbooks` | 提升到顶层 |
|
||||||
|
| `learning/textbooks/[id]` | `textbooks/[id]/chapters` | 增加子路径 `chapters` |
|
||||||
|
| `learning/study-path` | `learning-path` | 提升到顶层 |
|
||||||
|
| `(无)` | `exams/*` | 全新设计 |
|
||||||
|
| `(无)` | `ai-tutor` | 全新设计 |
|
||||||
|
| `(无)` | `classes` | 全新设计(与 courses 区分) |
|
||||||
|
|
||||||
|
### 4.3 权限点映射
|
||||||
|
|
||||||
|
CICD 中学生页面用到的权限点(迁移时需在 portal-shell `Permissions` 常量中对应):
|
||||||
|
|
||||||
|
- `GRADE_RECORD_READ`(grades / report-card)
|
||||||
|
- `CLASS_READ`(schedule / courses)
|
||||||
|
- `ATTENDANCE_READ`(attendance)
|
||||||
|
- `COURSE_PLAN_READ`(course-plans)
|
||||||
|
- `LESSON_PLAN_READ`(lesson-plans)
|
||||||
|
- `TEXTBOOK_READ`(textbooks)
|
||||||
|
- `ERROR_BOOK_READ`(error-book)
|
||||||
|
- `AI_CHAT`(study-path / ai-tutor)
|
||||||
|
- `ADAPTIVE_PRACTICE_READ`(practice)
|
||||||
|
- `ELECTIVE_READ`(elective)
|
||||||
|
- `DIAGNOSTIC_READ`(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/<domain>/components/*` 的视图组件需评估是下沉到 BFF 返回结构化数据,还是在 portal-shell 重建
|
||||||
|
5. **CICD diagnostic 决策**:产品需确认是否保留学生端诊断报告功能,若保留需补充 ARCHITECTURE.md §9.2
|
||||||
|
6. **分批推进**:按本报告 §4.5 的 P0→P5 优先级分批实施,每批完成后跑 `pnpm run arch:scan` 同步 arch.db
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**分析完成。共发现 35 个待完成页面(含 CICD 额外的 diagnostic 1 页),其中 33 个完全缺失、2 个为 dashboard 详情页缺失。最关键的 exams/[id]/take 作答工作台在 CICD 中无源页面,需重点设计。**
|
||||||
579
apps/portal-shell/docs/needtodo/teacher-NeedTodo.md
Normal file
579
apps/portal-shell/docs/needtodo/teacher-NeedTodo.md
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
# 教师域(Teacher)待完成功能分析
|
||||||
|
|
||||||
|
> 参考项目:`e:\desktop\CICD\src\app\(dashboard)\teacher\`(53 个 page.tsx)
|
||||||
|
> 当前项目:`e:\Desktop\Edu\apps\portal-shell\src\app\shell\teacher\`(53 个 page.tsx)
|
||||||
|
> 规划依据:`apps\portal-shell\ARCHITECTURE.md` §9.1(教师域 56 页,B1+B2)
|
||||||
|
> 分析日期:2026-07-24
|
||||||
|
> 分析方式:逐页面对比 CICD 实现 vs portal-shell 实现,逐个读取 page.tsx + features/*-client.tsx
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 〇、方法论与对比基线
|
||||||
|
|
||||||
|
1. CICD 单体(Next.js App Router + Server Actions + Drizzle)作为**功能基线**,反映"老版单体已实现"的教师功能完整态。
|
||||||
|
2. portal-shell 作为**目标态**,遵循 ARCH §9.1 的契约与批次规划(B1+B2+B2末)。
|
||||||
|
3. portal-shell 普遍采用「Server Component page.tsx 仅做 `<Suspense>` 边界包裹 + Client Component `*-client.tsx` 承载业务」的统一架构;CICD 多为「Server Component 直接拉数据 + Rich Client Component 渲染」。
|
||||||
|
4. ARCH §9.1 规划 56 页,portal-shell 已实现 53 页,**3 页缺口** = `exams/[id]/proctoring`(B2末)+ `notifications`(B1 共享)+ `settings`(B1 共享)。
|
||||||
|
5. CICD 中 `exams/page.tsx`、`classes/page.tsx`、`exams/grading/*`、`exams/create` 等为**重定向/等价路径**,不算独立功能缺口。
|
||||||
|
6. CICD 中 `diagnostic/student/[studentId]` 在 ARCH §9.1 中**未规划**(ARCH 仅规划 `diagnostic` + `diagnostic/class/[classId]` 共 2 页),属 CICD 历史功能,portal-shell 缺失但**不违反 ARCH**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、页面完成度总览
|
||||||
|
|
||||||
|
| 状态 | 数量 | 说明 |
|
||||||
|
| --------------------- | ---- | ---------------------------------------------------------------------------------------------------- |
|
||||||
|
| ✅ 已完成且功能完整 | 33 | 功能与 CICD 持平或为 ARCH 新建且实现完整(含 MSW 兜底契约页) |
|
||||||
|
| 🟡 已完成但功能不完整 | 14 | 页面存在但子功能/技术栈与 CICD 有差距 |
|
||||||
|
| ❌ 完全缺失 | 4 | `exams/[id]/proctoring`、`diagnostic/student/[studentId]`、`/shell/notifications`、`/shell/settings` |
|
||||||
|
|
||||||
|
### ✅ 已完成且功能完整(33 页)
|
||||||
|
|
||||||
|
| 模块 | 页面 | 备注 |
|
||||||
|
| ---------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
|
| dashboard | `/shell/teacher`(teacher/page.tsx) | 仪表盘,4 StatCard + 班级概况 + 预警(注:标题用硬编码中文,违反 i18n,但功能完整) |
|
||||||
|
| exams | `/shell/teacher/exams/[id]/analytics` | 汇总卡片 + 分数段分布图 + 每题正确率 + 学生排名,混合契约 ✅ `assignmentAnalysis` |
|
||||||
|
| homework | `/shell/teacher/homework/submissions/[submissionId]` | 单份提交批改,含题目分数/教师评语/AI 建议应用/上下导航/总反馈 |
|
||||||
|
| homework | `/shell/teacher/homework/submissions/[submissionId]/scan-grading` | 扫描批改 |
|
||||||
|
| homework | `/shell/teacher/homework/submissions` | 提交列表 |
|
||||||
|
| homework | `/shell/teacher/homework/assignments/[id]/submissions` | 作业子提交列表 |
|
||||||
|
| homework | `/shell/teacher/homework` | 作业列表 |
|
||||||
|
| grades | `/shell/teacher/grades` | 成绩列表 |
|
||||||
|
| grades | `/shell/teacher/grades/analytics` | 成绩分析,汇总 + 分布 + 排名,混合契约 |
|
||||||
|
| grades | `/shell/teacher/grades/stats` | 成绩统计 |
|
||||||
|
| grades | `/shell/teacher/grades/report-card` | 报告卡 |
|
||||||
|
| attendance | `/shell/teacher/attendance` | 考勤列表 |
|
||||||
|
| attendance | `/shell/teacher/attendance/report` | 考勤报告 |
|
||||||
|
| attendance | `/shell/teacher/attendance/stats` | 考勤统计 |
|
||||||
|
| classes | `/shell/teacher/classes` | 班级列表(含 gradeId/subjectId/q 筛选) |
|
||||||
|
| classes | `/shell/teacher/classes/schedule` | 班级课表 |
|
||||||
|
| students | (见 🟡 学生列表,因缺科目成绩) | — |
|
||||||
|
| course-plans | `/shell/teacher/course-plans`、`/[id]` | 2 页 |
|
||||||
|
| elective | `/shell/teacher/elective`、`/create`、`/[id]/edit` | 3 页 |
|
||||||
|
| error-book | `/shell/teacher/error-book` | ✅ `errorBookItems/Stats` 真实契约 |
|
||||||
|
| diagnostic | `/shell/teacher/diagnostic` | 诊断列表 |
|
||||||
|
| analytics | `/shell/teacher/analytics`、`/[studentId]` | ARCH 新建 2 页,MSW 兜底 |
|
||||||
|
| ai | `/shell/teacher/ai/ai-assist`、`/ai-lesson-plan`、`/ai-report` | ARCH 新建 B2末 3 页,全 MSW 兜底,工作台布局完整 |
|
||||||
|
| knowledge-graph | `/shell/teacher/knowledge-graph` | ARCH 新建 B2末,三栏工作台,`knowledgePoint(id)` ✅ 真实 + 列表 MSW |
|
||||||
|
| questions | `/shell/teacher/questions` | 题库列表,🟡 `question(id)` |
|
||||||
|
| textbooks | `/shell/teacher/textbooks`、`/[id]` | 2 页,🟡 `textbook(id)` |
|
||||||
|
| lesson-plans | `/shell/teacher/lesson-plans`、`/new`、`/calendar`、`/heatmap`、`/library` | 5 页(edit 见 🟡) |
|
||||||
|
| practice | `/shell/teacher/practice` | 练习 |
|
||||||
|
| leave | `/shell/teacher/leave` | 请假 |
|
||||||
|
| schedule-changes | `/shell/teacher/schedule-changes` | 调课 |
|
||||||
|
|
||||||
|
### 🟡 已完成但功能不完整(14 页)
|
||||||
|
|
||||||
|
详见 §三。
|
||||||
|
|
||||||
|
### ❌ 完全缺失(4 页)
|
||||||
|
|
||||||
|
详见 §二。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、缺失页面清单
|
||||||
|
|
||||||
|
### 2.1 `/shell/teacher/exams/[id]/proctoring`(监考工作台)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\teacher\exams\[id]\proctoring\page.tsx`
|
||||||
|
- **功能描述**:实时监考面板,包含 `ProctoringDashboard` 组件,并行拉取:
|
||||||
|
- `getExamForProctoring(id)` — 考试基础信息
|
||||||
|
- `getExamProctoringSummary(id)` — 监考汇总(应到/实到/异常数)
|
||||||
|
- `getStudentProctoringStatuses(id)` — 学生实时状态列表
|
||||||
|
- `getRecentProctoringEvents(id, 20)` — 最近 20 条监考事件
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.EXAM_PROCTOR)` + `ProctoringDashboard` 客户端组件(实时 WS 推送)
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(schema 无,需 WS 契约工单 + MSW 先行)
|
||||||
|
- **批次**:B2 末(二期,WS)
|
||||||
|
- **建议**:✅ 必须补做。需先定 WS 协议契约(push-gateway),再写 MSW mock,最后实现 `ProctoringDashboard` 客户端组件。这是 B2末 明确规划页。
|
||||||
|
|
||||||
|
### 2.2 `/shell/teacher/diagnostic/student/[studentId]`(学生诊断详情)
|
||||||
|
|
||||||
|
- **CICD 参考实现**:`e:\desktop\CICD\src\app\(dashboard)\teacher\diagnostic\student\[studentId]\page.tsx`
|
||||||
|
- **功能描述**:单个学生的学情诊断页,包含:
|
||||||
|
- `StudentDiagnosticView` 组件,渲染 `MasteryRadarPoint` 雷达图(学生 vs 班级平均对比)
|
||||||
|
- `getStudentMasterySummary(studentId)` — 学生掌握度汇总
|
||||||
|
- `getDiagnosticReports({ studentId }, ctx.dataScope)` — 诊断报告列表(教师可看草稿)
|
||||||
|
- `getKnowledgePointStats(studentClassId)` — 班级平均掌握度(雷达图对比)
|
||||||
|
- DataScope 二次校验:`class_taught` scope 验师生关系,`class_members`/`children` scope 限制访问
|
||||||
|
- **技术栈**:Server Component + `requirePermission(Permissions.DIAGNOSTIC_READ)` + `WidgetBoundary` + `StudentDiagnosticView` 客户端组件
|
||||||
|
- **ARCHITECTURE.md 契约**:🟡(`diagnosticReports` ✅,但单生维度查询未明确)— **注意:ARCH §9.1 仅规划 2 页(`/diagnostic` + `/diagnostic/class/[classId]`),未含学生维度**
|
||||||
|
- **批次**: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 是手写 `<table>`
|
||||||
|
- ❌ **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 无路由跳转,**按钮无实际行为**)
|
||||||
|
- **缺失功能**:
|
||||||
|
- ⚠️ **编辑按钮未接路由** — `<Button variant="outline">{t("detail.edit")}</Button>` 无 `asChild` / `href` / `onClick`,点击无反应,应跳 `/shell/teacher/exams/[id]/edit`
|
||||||
|
- 💡 **无提交列表预览** — CICD 在 homework 详情有提交列表,exam 详情可考虑加 submissions 摘要(CICD 无此页,属增量)
|
||||||
|
- 💡 **无分析入口卡片** — 可加"查看分析"快捷入口跳 `/analytics`
|
||||||
|
- **技术差距**:
|
||||||
|
- 编辑按钮需补 `asChild` + `<Link href=".../edit">`
|
||||||
|
- ARCH §9.1 规划此页为 M 详情页,portal-shell 已满足基本契约,但交互不完整
|
||||||
|
|
||||||
|
### 3.3 exams - 新建考试表单 `/shell/teacher/exams/new`
|
||||||
|
|
||||||
|
- **路径**:`/shell/teacher/exams/new`
|
||||||
|
- **portal-shell 实现**:`exams/new/page.tsx` + `features/teacher/exams/new-exam-client.tsx`
|
||||||
|
- **CICD 实现**:`exams/new/page.tsx`(ExamRichForm 富文本)+ `exams/create/page.tsx`(ExamForm 简单)
|
||||||
|
- **已有功能**:
|
||||||
|
- 简单表单:classId/subjectId/title/description/examDate/duration/totalScore
|
||||||
|
- 内联校验(classId/title/examDate 必填)
|
||||||
|
- ✅ mutation `useCreateExam` MSW 兜底
|
||||||
|
- 成功后 `router.push` 回列表
|
||||||
|
- **缺失功能**:
|
||||||
|
- ❌ **富文本编辑器** — CICD `/exams/new` 用 `ExamRichForm`(含 Tiptap 富文本 + 题目结构),portal-shell 是纯字段表单
|
||||||
|
- ❌ **班级/科目下拉选择器** — portal-shell 用 `<input type="text">`,CICD 应为 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` 组件(含 `<select>` 班级列表),portal-shell 用 `<input type="text">` 让用户手输 classId
|
||||||
|
- ❌ **默认班级选择逻辑** — CICD 默认选中教师第一个班级(`defaultClassId = classes[0].id`),portal-shell 无默认
|
||||||
|
- ❌ **学生科目成绩展示** — CICD 调 `getStudentsSubjectScores(studentIds)` 拉取每个学生科目成绩,`StudentsTable` 有科目成绩列,portal-shell 无
|
||||||
|
- ❌ **服务端学生列表预加载** — CICD Server Component 调 `getClassStudents` + `getStudentsSubjectScores`,portal-shell 纯客户端 MSW
|
||||||
|
- ❌ **状态筛选** — CICD 有 status 维度筛选(active/inactive 等),portal-shell 无
|
||||||
|
- **技术差距**:
|
||||||
|
- 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 用 `<Suspense fallback={<AnalyticsSkeleton />}>` 包裹 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 用 `<input type="text">` 手输 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 包 `<AiClientProvider service={createCoreAiClientService()}>`,支持 AI 辅助生成/润色,portal-shell 无
|
||||||
|
- ❌ **教材/章节上下文** — CICD Server Component 调 `getTextbookById` + `getChaptersByTextbookId` + `findChapterById`,把 textbookTitle/chapterTitle 传给编辑器工具栏显示,portal-shell 无
|
||||||
|
- ❌ **LessonPlanProviderSetup** — CICD 有专门的 Provider 设置组件(注入编辑器所需上下文),portal-shell 无
|
||||||
|
- ❌ **服务端初始数据预加载** — CICD Server Component 预加载 plan + teacherClasses + textbook + 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 用 `<input type="text">` 手输 examId
|
||||||
|
- ❌ **班级下拉选择器** — CICD 调 `getTeacherClasses()` + scope 过滤 + `getClassGradeIdsByClassIds` 构建 classGradeMap(用于按试卷年级过滤班级),portal-shell 手输 classId
|
||||||
|
- ❌ **学生列表预加载** — CICD 调 `getClassStudentsForEntry(classId, ctx.dataScope)` 拉取班级学生,portal-shell 手输 studentId
|
||||||
|
- ❌ **考试题目列表** — CICD 调 `getExamForGradeEntry(examId, ctx.dataScope)` 返回试卷详情含题目,支持按题录入
|
||||||
|
- ❌ **DataScope 过滤** — CICD 用 `ctx.dataScope.type === "class_taught"` 过滤班级,portal-shell 无权限过滤
|
||||||
|
- ❌ **EmptyState when no exams** — CICD 在 `exams.length === 0` 时显示 EmptyState
|
||||||
|
- ❌ **按试卷年级过滤班级** — CICD 用 classGradeMap 客户端过滤
|
||||||
|
- **技术差距**:
|
||||||
|
- 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 用 `<input type="text">` 手输 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 + `<TeacherDashboardView dataPromise={...}>`,视图内 `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 服务,`<AiClientProvider>` 注入 AI 上下文
|
||||||
|
- 渲染 `<ExamAssembly>` 富组件(位于 `modules/exams/components/exam-assembly`),透传 examId/title/subject/grade/difficulty/totalScore/durationMin + initialSelected + initialStructure + questionOptions
|
||||||
|
- 配套 `error.tsx`(`useErrorReport` 错误上报 + `EmptyState` + retry 按钮)+ `loading.tsx`(3 栏 Skeleton,`lg:grid-cols-3` 2+1 布局)
|
||||||
|
- **技术栈**:Next.js App Router Server Component(`force-dynamic`)+ next-intl `getTranslations("examHomework.exam.build")` + `requirePermission` + `Permissions.EXAM_READ` + `ctx.dataScope` 数据范围过滤 + Drizzle data-access(`getExamById` / `getQuestions`)+ `normalizeStructure` 转换层 + `@paralleldrive/cuid2` + AI 集成(`AiClientProvider` + `createCoreAiClientService`)+ `ExamAssembly` 客户端组件 + 专用 error/loading 边界
|
||||||
|
- **ARCHITECTURE.md 契约**:❌(B2)— portal-shell `page.tsx` 注释明确 `@contract-pending`:`examBuild(examId)` / `questionsLibrary(filter)` / `saveExamBuild(input)` 三个根字段均不存在,全 MSW 兜底
|
||||||
|
- **portal-shell 现状**:
|
||||||
|
- `page.tsx` 仅 `<Suspense fallback={<WorkbenchPageSkeleton />}>` 包裹 `ExamBuildClient`,**无服务端预加载、无权限守卫、无 notFound**
|
||||||
|
- `ExamBuildClient` 纯客户端,使用 `useExamBuild` / `useQuestionsLibrary` / `useSaveExamBuild` 三个 Apollo hook(全 MSW 兜底)
|
||||||
|
- `WorkbenchPageShell` 三栏布局:左 `QuestionsLibraryPanel` / 中 `SelectedQuestionsPanel` / 右 `PreviewPanel`
|
||||||
|
- 左栏:q 关键词 + type 下拉 + difficulty 下拉筛选 + 题目列表 + 添加按钮(`Plus` 图标)+ 总数显示
|
||||||
|
- 中栏:已选题目列表,支持上移/下移(`↑`/`↓`)/编辑分值(`Input type="number"`)/移除(`Trash2`)
|
||||||
|
- 右栏:总分(含 baseline 差值 ± 提示,amber/destructive 配色)+ 及格分 + 题型分布(`countByType`)
|
||||||
|
- 本地 `useState<ExamBuildNode[] | null>` 管理 selected,`useEffect` 在 data 首次就绪时初始化(`sortBySortOrder`)
|
||||||
|
- 保存 mutation `useSaveExamBuild` + `notify.success/error` 反馈
|
||||||
|
- 三态合并到 `errorNode`(error + empty 共用降级节点)
|
||||||
|
- **无专用 error.tsx / loading.tsx 文件**
|
||||||
|
- **缺失功能**:
|
||||||
|
- ❌ **Server Component 数据预加载** — CICD 在服务端并行 `Promise.all` 拉取 exam + bank questions + selected questions,portal-shell 完全依赖客户端 MSW
|
||||||
|
- ❌ **DataScope 权限过滤** — CICD 用 `ctx.dataScope` 过滤考试可见性,portal-shell 无
|
||||||
|
- ❌ **`requirePermission(Permissions.EXAM_READ)` 权限守卫** — portal-shell 无
|
||||||
|
- ❌ **`notFound()` 404 处理** — CICD 考试不存在时调 `notFound()`,portal-shell 显示空态节点
|
||||||
|
- ❌ **AI 集成** — CICD 包 `<AiClientProvider service={createCoreAiClientService()}>` 支持 AI 辅助组卷,portal-shell 无 AI 接入(与 §3.9 教案编辑同类型缺失,见 §六 共性技术债 6)
|
||||||
|
- ❌ **`normalizeStructure` 转换层** — CICD 把 `exam.structure` 规范化为 `ExamNode[]` 树(支持嵌套分组节点),portal-shell 用扁平 `ExamBuildNode[]`,无结构化分组能力
|
||||||
|
- ❌ **structure 空时回退逻辑** — CICD 在 structure 空但 `initialSelected` 非空时回退为扁平节点列表(cuid2 生成 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` 等多处用 `<input type="text">` 替代 `<select>`,需补 data-access 查询 + select 组件。
|
||||||
|
4. **DataScope 校验缺失**:portal-shell 普遍无 `ctx.dataScope` 权限过滤(CICD 在 Server Component 内做 scope 过滤),需在后端契约就绪后补。
|
||||||
|
5. **Server Component 预加载缺失**:CICD 多个详情页在 Server Component 内并行预加载多个数据源,portal-shell 纯客户端 MSW,等契约就绪后需改 Server Component 预加载 + Suspense 流式。
|
||||||
|
6. **AI 集成缺失**:`exams/[id]/build`、`lesson-plans/[planId]/edit` 在 CICD 有 `AiClientProvider` + `createCoreAiClientService`,portal-shell 无 AI 接入。
|
||||||
|
7. **i18n 硬编码**:`dashboard` 用硬编码中文,应全面 `useTranslations`。
|
||||||
|
8. **SectionErrorBoundary / WidgetBoundary 缺失**:CICD 多处用 `SectionErrorBoundary`/`WidgetBoundary` 做局部降级,portal-shell 用 errorNode 整页降级,粒度较粗。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、参考文件索引
|
||||||
|
|
||||||
|
### CICD 参考实现
|
||||||
|
|
||||||
|
- `e:\desktop\CICD\src\app\(dashboard)\teacher\` — 教师域 53 页源
|
||||||
|
- `e:\desktop\CICD\src\modules\exams\components\exam-rich-form.tsx` — 富文本试卷表单
|
||||||
|
- `e:\desktop\CICD\src\modules\exams\components\exam-assembly.tsx` — 组卷工作台
|
||||||
|
- `e:\desktop\CICD\src\modules\classes\components\class-detail\` — 班级详情 5 个 widget
|
||||||
|
- `e:\desktop\CICD\src\modules\classes\components\my-classes-grid.tsx` — 班级网格
|
||||||
|
- `e:\desktop\CICD\src\modules\classes\components\students-table.tsx` — 学生表
|
||||||
|
- `e:\desktop\CICD\src\modules\homework\components\homework-assignment-question-error-overview-card.tsx` — 作业错误概览
|
||||||
|
- `e:\desktop\CICD\src\modules\homework\components\homework-assignment-exam-content-card.tsx` — 作业内容卡片
|
||||||
|
- `e:\desktop\CICD\src\modules\grades\components\batch-grade-entry.tsx` — 批量录入
|
||||||
|
- `e:\desktop\CICD\src\modules\lesson-preparation\components\lesson-plan-editor.tsx` — 教案编辑器
|
||||||
|
- `e:\desktop\CICD\src\modules\diagnostic\components\class-diagnostic-view.tsx` — 班级诊断视图
|
||||||
|
- `e:\desktop\CICD\src\modules\diagnostic\components\student-diagnostic-view.tsx` — 学生诊断视图
|
||||||
|
- `e:\desktop\CICD\src\modules\proctoring\components\proctoring-dashboard.tsx` — 监考面板
|
||||||
|
- `e:\desktop\CICD\src\modules\dashboard\components\teacher-dashboard\teacher-dashboard-view.tsx` — 仪表盘视图
|
||||||
|
|
||||||
|
### portal-shell 当前实现
|
||||||
|
|
||||||
|
- `e:\Desktop\Edu\apps\portal-shell\src\app\shell\teacher\` — 教师域 53 页
|
||||||
|
- `e:\Desktop\Edu\apps\portal-shell\src\features\teacher\` — 51 个 `*-client.tsx` + 辅助组件
|
||||||
|
- `e:\Desktop\Edu\apps\portal-shell\src\lib\api\` — Apollo hooks(含 MSW 兜底)
|
||||||
|
- `e:\Desktop\Edu\apps\portal-shell\src\shared\components\page-templates\` — ListPageShell/FormPageShell/DetailPageShell/WorkbenchPageShell
|
||||||
|
- `e:\Desktop\Edu\apps\portal-shell\ARCHITECTURE.md` §9.1 — 教师域 56 页规划
|
||||||
|
|
||||||
|
### 契约工单
|
||||||
|
|
||||||
|
- `docs/architecture/issues/contracts/core-edu_contract.md` — exams/homework/grades/questions mutation 工单
|
||||||
|
- `docs/architecture/issues/contracts/classes_contract.md` — classes/students/attendance 工单
|
||||||
|
- `docs/architecture/issues/contracts/data-ana_contract.md` — diagnostic/analytics 工单
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**分析完成。下一步建议按 §五 优先级逐项补做,每补一页同步更新 ARCH §9.1 状态 + 004 + arch.db。**
|
||||||
2
apps/portal-shell/next-env.d.ts
vendored
2
apps/portal-shell/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ interface Baseline {
|
|||||||
categories: Record<string, { pattern: string; min: number; label: string }>;
|
categories: Record<string, { pattern: string; min: number; label: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Baseline as of P2 B3 (2026-07-22, error-book + diagnostic + analytics modules added).
|
// Baseline as of B2 末 教师域 (2026-07-24, proctoring + student-diagnostic
|
||||||
|
// added: 2 new pages).
|
||||||
// Update when adding pages.
|
// Update when adding pages.
|
||||||
const BASELINE: Baseline = {
|
const BASELINE: Baseline = {
|
||||||
total: 58,
|
total: 69,
|
||||||
categories: {
|
categories: {
|
||||||
dashboards: {
|
dashboards: {
|
||||||
pattern: "shell/{admin,teacher,student,parent}/page.tsx",
|
pattern: "shell/{admin,teacher,student,parent}/page.tsx",
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ describe("E2E: 三层安全边界", () => {
|
|||||||
|
|
||||||
it("teacher 有 QUESTION_READ → 访问题库放行(OR 语义)", () => {
|
it("teacher 有 QUESTION_READ → 访问题库放行(OR 语义)", () => {
|
||||||
const result = checkRoutePermission(
|
const result = checkRoutePermission(
|
||||||
"/shell/teacher/question-bank",
|
"/shell/teacher/questions",
|
||||||
TEACHER_USER.bitmap,
|
TEACHER_USER.bitmap,
|
||||||
TEACHER_USER.role,
|
TEACHER_USER.role,
|
||||||
);
|
);
|
||||||
@@ -250,7 +250,7 @@ describe("E2E: 三层安全边界", () => {
|
|||||||
|
|
||||||
it("student 有 ELECTIVE_SELECT → 访问选修课选择放行(OR 语义)", () => {
|
it("student 有 ELECTIVE_SELECT → 访问选修课选择放行(OR 语义)", () => {
|
||||||
const result = checkRoutePermission(
|
const result = checkRoutePermission(
|
||||||
"/shell/student/electives",
|
"/shell/student/elective",
|
||||||
STUDENT_USER.bitmap,
|
STUDENT_USER.bitmap,
|
||||||
STUDENT_USER.role,
|
STUDENT_USER.role,
|
||||||
);
|
);
|
||||||
@@ -263,7 +263,7 @@ describe("E2E: 三层安全边界", () => {
|
|||||||
"DASHBOARD_READ",
|
"DASHBOARD_READ",
|
||||||
]);
|
]);
|
||||||
const result = checkRoutePermission(
|
const result = checkRoutePermission(
|
||||||
"/shell/student/electives",
|
"/shell/student/elective",
|
||||||
noElective,
|
noElective,
|
||||||
"student",
|
"student",
|
||||||
);
|
);
|
||||||
@@ -313,7 +313,7 @@ describe("E2E: 三层安全边界", () => {
|
|||||||
const teacherRoutes = [
|
const teacherRoutes = [
|
||||||
"/shell/teacher",
|
"/shell/teacher",
|
||||||
"/shell/teacher/lesson-plans",
|
"/shell/teacher/lesson-plans",
|
||||||
"/shell/teacher/question-bank",
|
"/shell/teacher/questions",
|
||||||
];
|
];
|
||||||
const results = batchCheckRoutePermission(
|
const results = batchCheckRoutePermission(
|
||||||
teacherRoutes,
|
teacherRoutes,
|
||||||
|
|||||||
38
apps/portal-shell/src/app/shell/admin/ai-settings/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/ai-settings/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 配置路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AiSettingsError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.aiSettings");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] ai-settings route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 配置路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AiSettingsLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
24
apps/portal-shell/src/app/shell/admin/ai-settings/page.tsx
Normal file
24
apps/portal-shell/src/app/shell/admin/ai-settings/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AiSettingsClient } from "@/features/admin/ai-settings/ai-settings-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Provider 配置与用量仪表盘(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 AiSettingsClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:
|
||||||
|
* - aiProviders(scope) ❌ schema 未就绪 → MSW 兜底(@contract-pending)
|
||||||
|
* - aiUsageDashboard(range) ❌ schema 未就绪 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AiSettingsPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<AiSettingsClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告编辑路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AnnouncementEditError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.announcements");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] announcement edit route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告编辑路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AnnouncementEditLoading(): React.ReactElement {
|
||||||
|
return <FormPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AnnouncementEditClient } from "@/features/admin/announcements/announcement-edit-client";
|
||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告编辑表单页(ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useParams 要求)。
|
||||||
|
* 业务逻辑在 AnnouncementEditClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminAnnouncement(id) + updateAnnouncement(id, input) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AnnouncementEditPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<FormPageSkeleton />}>
|
||||||
|
<AnnouncementEditClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告详情路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AnnouncementDetailError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.announcements");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] announcement detail route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告详情路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AnnouncementDetailLoading(): React.ReactElement {
|
||||||
|
return <DetailPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AnnouncementDetailClient } from "@/features/admin/announcements/announcement-detail-client";
|
||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告详情页(ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
* 业务逻辑在 AnnouncementDetailClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminAnnouncement(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
|
||||||
|
*/
|
||||||
|
export default function AnnouncementDetailPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<DetailPageSkeleton />}>
|
||||||
|
<AnnouncementDetailClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AnnouncementsError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.announcements");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] announcements route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AnnouncementsLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/announcements/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/announcements/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AnnouncementsListClient } from "@/features/admin/announcements/announcements-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告管理列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 AnnouncementsListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminAnnouncements(status) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AnnouncementsListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<AnnouncementsListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
apps/portal-shell/src/app/shell/admin/attendance/error.tsx
Normal file
36
apps/portal-shell/src/app/shell/admin/attendance/error.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考勤管理路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AdminAttendanceError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.attendance.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin attendance route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考勤管理路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AdminAttendanceLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
23
apps/portal-shell/src/app/shell/admin/attendance/page.tsx
Normal file
23
apps/portal-shell/src/app/shell/admin/attendance/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AdminAttendanceClient } from "@/features/admin/attendance/admin-attendance-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 考勤管理页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 AdminAttendanceClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminAttendanceStats / attendanceGradeCorrelation / adminClasses
|
||||||
|
* ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AdminAttendancePage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<AdminAttendanceClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据变更日志路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function DataChangesError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.auditLogs.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(
|
||||||
|
"[portal-shell] admin audit-logs/data-changes route error:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据变更日志路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function DataChangesLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { DataChangesClient } from "@/features/admin/audit-logs/data-changes-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据变更日志页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 DataChangesClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:dataChangeLogs(filter, pagination) / dataChangeTableOptions / dataChangeStats ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function DataChangesPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<DataChangesClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
apps/portal-shell/src/app/shell/admin/audit-logs/error.tsx
Normal file
36
apps/portal-shell/src/app/shell/admin/audit-logs/error.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计日志路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AuditLogsError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.auditLogs.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin audit-logs route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计日志路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AuditLogsLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录日志路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function LoginLogsError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.auditLogs.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(
|
||||||
|
"[portal-shell] admin audit-logs/login-logs route error:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录日志路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function LoginLogsLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { LoginLogsClient } from "@/features/admin/audit-logs/login-logs-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录日志页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 LoginLogsClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:loginLogs(filter, pagination) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function LoginLogsPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<LoginLogsClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计概览路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AuditOverviewError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.auditLogs.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(
|
||||||
|
"[portal-shell] admin audit-logs/overview route error:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计概览路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AuditOverviewLoading(): React.ReactElement {
|
||||||
|
return <DetailPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AuditOverviewClient } from "@/features/admin/audit-logs/audit-overview-client";
|
||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计概览页(ARCHITECTURE.md §7.3 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
* 业务逻辑在 AuditOverviewClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:auditOverviewStats / auditTrend / dataChangeActionStats ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AuditOverviewPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<DetailPageSkeleton />}>
|
||||||
|
<AuditOverviewClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/audit-logs/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/audit-logs/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AuditLogsListClient } from "@/features/admin/audit-logs/audit-logs-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审计日志列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 AuditLogsListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:auditLogs(filter, pagination) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AuditLogsListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<AuditLogsListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划编辑路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function CoursePlanEditError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.coursePlans");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin course-plan edit route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划编辑路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function CoursePlanEditLoading(): React.ReactElement {
|
||||||
|
return <FormPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { CoursePlanEditClient } from "@/features/admin/course-plans/course-plan-edit-client";
|
||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划编辑表单页(ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useParams 要求)。
|
||||||
|
*
|
||||||
|
* 数据契约:adminCoursePlan(id) + updateCoursePlan(input) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function CoursePlanEditPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<FormPageSkeleton />}>
|
||||||
|
<CoursePlanEditClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划详情路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function CoursePlanDetailError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.coursePlans");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(
|
||||||
|
"[portal-shell] admin course-plan detail route error:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划详情路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*
|
||||||
|
* 子页面(编辑)的 Skeleton 由 server page 的 <Suspense> 兜底,
|
||||||
|
* 本文件仅在 /shell/admin/course-plans/[id] 期间显示。
|
||||||
|
*/
|
||||||
|
export default function CoursePlanDetailLoading(): React.ReactElement {
|
||||||
|
return <DetailPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { CoursePlanDetailClient } from "@/features/admin/course-plans/course-plan-detail-client";
|
||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划详情页(ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
*
|
||||||
|
* 数据契约:adminCoursePlan(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function CoursePlanDetailPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<DetailPageSkeleton />}>
|
||||||
|
<CoursePlanDetailClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划新建路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function CoursePlanCreateError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.coursePlans");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(
|
||||||
|
"[portal-shell] admin course-plan create route error:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划新建路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function CoursePlanCreateLoading(): React.ReactElement {
|
||||||
|
return <FormPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { CoursePlanCreateClient } from "@/features/admin/course-plans/course-plan-create-client";
|
||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划新建表单页(ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
*
|
||||||
|
* 数据契约:mutation createCoursePlan(input) ❌ schema 无 Mutation 类型 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function CoursePlanCreatePage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<FormPageSkeleton />}>
|
||||||
|
<CoursePlanCreateClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/course-plans/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/course-plans/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function CoursePlansError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.coursePlans");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin course-plans route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*
|
||||||
|
* 子页面(详情/编辑/新建)的 Skeleton 由各自 server page 的 <Suspense> 兜底,
|
||||||
|
* 本文件仅在 /shell/admin/course-plans 列表/重定向期间显示。
|
||||||
|
*/
|
||||||
|
export default function CoursePlansLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/course-plans/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/course-plans/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { CoursePlansListClient } from "@/features/admin/course-plans/course-plans-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端课程计划列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 CoursePlansListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminCoursePlans(status) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function CoursePlansListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<CoursePlansListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程地图路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function CurriculumMapError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.curriculumMap");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin curriculum-map route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程地图路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function CurriculumMapLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { CurriculumMapClient } from "@/features/admin/curriculum-map/curriculum-map-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课程地图页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 CurriculumMapClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:standardsCoverageHeatmap / globalLessonPlanStats ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function CurriculumMapPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<CurriculumMapClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课编辑路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function ElectiveEditError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.elective");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin elective edit route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课编辑路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function ElectiveEditLoading(): React.ReactElement {
|
||||||
|
return <FormPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { ElectiveEditClient } from "@/features/admin/elective/elective-edit-client";
|
||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课编辑表单页(ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5 / admin-NeedTodo §四)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useParams 要求)。
|
||||||
|
* 业务逻辑在 ElectiveEditClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminElective(id) + updateElective(id, input) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function ElectiveEditPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<FormPageSkeleton />}>
|
||||||
|
<ElectiveEditClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课详情路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function ElectiveDetailError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.elective");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin elective detail route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课详情路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function ElectiveDetailLoading(): React.ReactElement {
|
||||||
|
return <DetailPageSkeleton />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/elective/[id]/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/elective/[id]/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { ElectiveDetailClient } from "@/features/admin/elective/elective-detail-client";
|
||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课详情页(ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5 / admin-NeedTodo §四)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
* 业务逻辑在 ElectiveDetailClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminElective(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
|
||||||
|
*/
|
||||||
|
export default function ElectiveDetailPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<DetailPageSkeleton />}>
|
||||||
|
<ElectiveDetailClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课新建路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function ElectiveCreateError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.elective");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin elective create route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课新建路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function ElectiveCreateLoading(): React.ReactElement {
|
||||||
|
return <FormPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { ElectiveCreateClient } from "@/features/admin/elective/elective-create-client";
|
||||||
|
import { FormPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课新建表单页(ARCHITECTURE.md §7.3 表单页 / §9.4 / §10 P5 / admin-NeedTodo §四)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
* 业务逻辑在 ElectiveCreateClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:createElective(input) ❌ schema 无 Mutation 类型 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function ElectiveCreatePage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<FormPageSkeleton />}>
|
||||||
|
<ElectiveCreateClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/elective/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/elective/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课管理路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function ElectiveListError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.elective");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin elective list route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课管理路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function ElectiveListLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/elective/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/elective/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { ElectiveListClient } from "@/features/admin/elective/elective-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选修课管理列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 ElectiveListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminElectives ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function ElectiveListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<ElectiveListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
apps/portal-shell/src/app/shell/admin/error-book/error.tsx
Normal file
36
apps/portal-shell/src/app/shell/admin/error-book/error.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错题本分析路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AdminErrorBookError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.errorBook.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin error-book route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错题本分析路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AdminErrorBookLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/error-book/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/error-book/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { ErrorBookClient } from "@/features/admin/error-book/error-book-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错题本分析页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 ErrorBookClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminErrorBookStats ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AdminErrorBookPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<ErrorBookClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/files/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/files/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function FilesError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.files");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] files route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
apps/portal-shell/src/app/shell/admin/files/loading.tsx
Normal file
9
apps/portal-shell/src/app/shell/admin/files/loading.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function FilesLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/files/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/files/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { FilesListClient } from "@/features/admin/files/files-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件管理列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
* 业务逻辑在 FilesListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:fileAttachments(filter) + fileStats ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function FilesListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<FilesListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邀请码管理路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function InvitationCodesError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.invitationCodes.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin invitation-codes route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邀请码管理路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function InvitationCodesLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { InvitationCodesListClient } from "@/features/admin/invitation-codes/invitation-codes-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邀请码管理列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 InvitationCodesListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:invitationCodes(status) / createInvitationCode / revokeInvitationCode ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function InvitationCodesListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<InvitationCodesListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教案详情只读路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
|
||||||
|
export default function AdminLessonPlanViewError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.lessonPlans");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin lesson plan view route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教案详情只读路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AdminLessonPlanViewLoading(): React.ReactElement {
|
||||||
|
return <DetailPageSkeleton />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AdminLessonPlanViewClient } from "@/features/admin/lesson-plans/lesson-plan-view-client";
|
||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教案详情只读页(ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
* 业务逻辑在 AdminLessonPlanViewClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminLessonPlan(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
|
||||||
|
*/
|
||||||
|
export default function AdminLessonPlanViewPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<DetailPageSkeleton />}>
|
||||||
|
<AdminLessonPlanViewClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/lesson-plans/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/lesson-plans/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教案管理路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
|
||||||
|
export default function AdminLessonPlansError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.lessonPlans");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin lesson plans route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教案管理路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AdminLessonPlansLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/lesson-plans/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/lesson-plans/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AdminLessonPlansListClient } from "@/features/admin/lesson-plans/lesson-plans-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 教案管理列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 AdminLessonPlansListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminLessonPlans ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AdminLessonPlansListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<AdminLessonPlansListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/organization/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/organization/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组织管理路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function OrganizationError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.organization");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin organization route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组织管理路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function OrganizationLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/organization/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/organization/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { OrganizationTreeClient } from "@/features/admin/organization/organization-tree-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组织管理树视图页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 OrganizationTreeClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:organizationTree() ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function OrganizationTreePage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<OrganizationTreeClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/permissions/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/permissions/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限目录路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function PermissionsError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.permissions");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin permissions route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限目录路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function PermissionsLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
23
apps/portal-shell/src/app/shell/admin/permissions/page.tsx
Normal file
23
apps/portal-shell/src/app/shell/admin/permissions/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { PermissionsListClient } from "@/features/admin/permissions/permissions-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限目录列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 PermissionsListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:permissions / permissionRoleCounts ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
* 契约工单:docs/architecture/issues/contracts/iam_contract.md#permissions
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function PermissionsListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<PermissionsListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/plugins/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/plugins/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件管理路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function PluginsError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.plugins");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] plugins route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件管理路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function PluginsLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
24
apps/portal-shell/src/app/shell/admin/plugins/page.tsx
Normal file
24
apps/portal-shell/src/app/shell/admin/plugins/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { PluginsClient } from "@/features/admin/plugins/plugins-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件管理(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / 004 §5.4)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 PluginsClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:
|
||||||
|
* - pluginRegistry ✅ schema 已就绪(config-service)
|
||||||
|
* - updatePluginRegistry(pluginId, input) ✅ schema 已就绪
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function PluginsPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<PluginsClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/questions/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/questions/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题库管理路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
|
||||||
|
export default function AdminQuestionsError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.questions");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin questions route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题库管理路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AdminQuestionsLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/questions/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/questions/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AdminQuestionsListClient } from "@/features/admin/questions/questions-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 题库管理列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 AdminQuestionsListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminQuestions(filter) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AdminQuestionsListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<AdminQuestionsListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/roles/[id]/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/roles/[id]/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色详情路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function RoleDetailError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.roles");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin role detail route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色详情路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function RoleDetailLoading(): React.ReactElement {
|
||||||
|
return <DetailPageSkeleton />;
|
||||||
|
}
|
||||||
22
apps/portal-shell/src/app/shell/admin/roles/[id]/page.tsx
Normal file
22
apps/portal-shell/src/app/shell/admin/roles/[id]/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { RoleDetailClient } from "@/features/admin/roles/role-detail-client";
|
||||||
|
import { DetailPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色详情页(ARCHITECTURE.md §7.3 详情页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
* 业务逻辑在 RoleDetailClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:role(id: ID!) ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.5 / §7.3 / §9.4 / §10 P5 / §11.3
|
||||||
|
*/
|
||||||
|
export default function RoleDetailPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<DetailPageSkeleton />}>
|
||||||
|
<RoleDetailClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/portal-shell/src/app/shell/admin/roles/error.tsx
Normal file
38
apps/portal-shell/src/app/shell/admin/roles/error.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色管理路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function RolesError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.roles");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin roles route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">
|
||||||
|
{t("error.title")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("error.unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("error.retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
apps/portal-shell/src/app/shell/admin/roles/loading.tsx
Normal file
9
apps/portal-shell/src/app/shell/admin/roles/loading.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色管理路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function RolesLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
23
apps/portal-shell/src/app/shell/admin/roles/page.tsx
Normal file
23
apps/portal-shell/src/app/shell/admin/roles/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { RolesListClient } from "@/features/admin/roles/roles-list-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角色管理列表页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹(useSearchParams 要求)。
|
||||||
|
* 业务逻辑在 RolesListClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:roles ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
* 契约工单:docs/architecture/issues/contracts/iam_contract.md#roles
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function RolesListPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<RolesListClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动排课路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AdminSchedulingAutoError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.scheduling.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error("[portal-shell] admin scheduling/auto route error:", error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动排课路由段加载骨架(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment loading.tsx,自动包裹页面渲染期间。
|
||||||
|
*/
|
||||||
|
export default function AdminSchedulingAutoLoading(): React.ReactElement {
|
||||||
|
return <ListPageSkeleton rows={5} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
|
import { AutoScheduleClient } from "@/features/admin/scheduling/auto-schedule-client";
|
||||||
|
import { ListPageSkeleton } from "@/shared/components/page-templates";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动排课页(ARCHITECTURE.md §7.3 列表页 / §9.4 / §10 P5 / admin-NeedTodo §四)
|
||||||
|
*
|
||||||
|
* Server Component 入口:仅负责 Suspense 边界包裹。
|
||||||
|
* 业务逻辑在 AutoScheduleClient(client component)中。
|
||||||
|
*
|
||||||
|
* 数据契约:adminClasses / autoSchedule ❌ schema 无 → MSW 兜底(@contract-pending)
|
||||||
|
*
|
||||||
|
* 关联:ARCHITECTURE.md §5.3 / §5.4 / §7.3 / §9.4 / §10 P5 / §11.3 / §11.4
|
||||||
|
*/
|
||||||
|
export default function AdminSchedulingAutoPage(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<ListPageSkeleton rows={5} />}>
|
||||||
|
<AutoScheduleClient />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排课变更审批路由错误边界(ARCHITECTURE.md §7.4 三态规范 / §11.3 DoD)。
|
||||||
|
* Next.js Route Segment error.tsx,捕获子树未处理异常。
|
||||||
|
*/
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/shared/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function AdminSchedulingChangesError({
|
||||||
|
error,
|
||||||
|
reset,
|
||||||
|
}: {
|
||||||
|
error: Error & { digest?: string };
|
||||||
|
reset: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const t = useTranslations("admin.scheduling.error");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.error(
|
||||||
|
"[portal-shell] admin scheduling/changes route error:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-destructive/30 bg-destructive/5 p-10">
|
||||||
|
<h2 className="text-lg font-semibold text-destructive">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{error.message || t("unknown")}
|
||||||
|
</p>
|
||||||
|
<Button onClick={reset} variant="outline">
|
||||||
|
{t("retry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user