Files
Edu/apps/portal-shell/ARCHITECTURE.md
SpecialX 0beeff6329 feat(portal-shell): restore codegen typescript-operations for data-ana domain (P1-7)
ARCHITECTURE.md §10 P1-7: dashboard domain's 6 operations strictly
match the schema, so disable skipDocumentsValidation for that output
and restore per-operation type generation.

Changes:
- codegen.yml: add dashboard-types.ts output (typescript +
  typescript-operations plugins, skipDocumentsValidation: false);
  move documents config into each generates entry
- dashboard.ts: remove 14 handwritten interfaces and 6 internal query
  type aliases; derive types via NonNullable<GetXxxQuery['xxx']> so
  the public hook API shape stays unchanged
- admin/student/teacher page.tsx: add ?? "--" / ?? 0 null guards on
  StatCard value props to match schema nullable semantics (parent page
  already uses toFixed chain, no change needed)

Acceptance (ARCHITECTURE.md §10 P1-7):
- codegen 3 outputs all SUCCESS
- tsc 0 errors / eslint 0 errors / vitest 231 passed / next build ok
- 6 operations strictly match schema with 0 errors

Refs: ARCHITECTURE.md §5.3 data layer / §10 P1-7
2026-07-22 15:37:06 +08:00

1102 lines
125 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# portal-shell 前端架构总纲v3.0
> 版本3.0
> 日期2026-07-20
> 状态:**P0 已完成 + P1-1/P1-2/P1-3/P1-4 已完成2026-07-22 验收)+ P1 进行中;架构审计完成 + 重设计方案定稿**
> 本文档地位:**portal-shell 前端工作的唯一权威指导文档**。所有后续 AI/人工在此模块的工作必须先读本文件,以其为准。
>
> 关联文档(按效力排序):
>
> 1. **本文件**v3.0 总纲:审计结论 + 目标架构 + 路线图 + 工作规范)
> 2. [README.md](./README.md)v2.0 模块文档:微内核仪表盘子系统的详细设计,仅其"插件仪表盘"部分继续有效)
> 3. [004 架构影响地图](../../docs/architecture/004_architecture_impact_map.md)后端架构唯一源§5.4 视口四层模型、§11.7 前端数据层)
> 4. [项目规则](../../.trae/rules/project_rules.md)强制约束§3.10 设计令牌、§4 安全、§14 多 AI 协作)
> 5. [MIGRATION_GUIDE.md](../../MIGRATION_GUIDE.md)CICD → Edu 迁移背景)
>
> **效力声明**README v2.0 中与本文件冲突的表述(完成度声明、设计方向、"旧 portal 已下线"等)以本文件为准;`docs/standards/ui-design-system.md`v1.0,面向已废弃的 4 微前端方案)自本文件发布之日起**废止**,其有效内容已并入本文件 §8`docs/architecture/0020_portal_shell_architecture.md`v1.0)为历史评审稿,仅作背景参考。
---
## 目录
1. [现状审计2026-07-20 快照)](#1-现状审计2026-07-20-快照)
2. [问题根因分析](#2-问题根因分析)
3. [目标架构 v3.0](#3-目标架构-v30)
4. [认证与身份链](#4-认证与身份链)
5. [数据层架构](#5-数据层架构)
6. [安全架构(安全边际)](#6-安全架构安全边际)
7. [信息架构与页面体系](#7-信息架构与页面体系)
8. [前端设计规范(强制)](#8-前端设计规范强制)
9. [页面迁移总表(~140 页)](#9-页面迁移总表140-页)
10. [实施路线图P0P6](#10-实施路线图p0p6)
11. [后续 AI 工作规范(强制)](#11-后续-ai-工作规范强制)
12. [风险登记册](#12-风险登记册)
13. [附录](#13-附录)
---
## 1. 现状审计2026-07-20 快照)
> 审计方法:全部结论均可复现。每项发现标注证据(文件路径 + 行号 / 可执行命令。审计环境Windows 11 + Node 24分支 `feat/architecture-v2.1`,工作区干净。
### 1.1 验收声明 vs 实测结果
README v2.0 声称"P0P4 全部验证通过、功能验收 23 项全部 [x]"。实测:**构建级声明属实,功能级声明不成立**——现有门禁只验证"骨架不塌",从不验证"功能可用"。
| README 声明 | 实测结果 | 结论 |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `typecheck` 0 错误 | `node node_modules/typescript/bin/tsc --noEmit` 无输出退出 | ✅ 属实 |
| `vitest` 206/206 通过 | 实测 19 文件 206 用例全部通过8.12s | ✅ 属实 |
| `build` 6 路由生成成功 | 实测成功:`/``/_not-found``/api/health``/api/log``/api/ready``/shell/[[...route]]` | ✅ 属实——但**全应用只有 1 条业务路由** |
| "31 个内置插件全部加载正常" | 31 插件全部注册并能渲染30 REAL + 1 PARTIAL无 STUB/MOCK但其 GraphQL 查询**严格匹配 0/50**(见 §1.3-F2运行时全部返回错误/空数据 | ⚠️ 代码真实,数据全断 |
| "31 个 widget 旧纸感令牌全部迁移1104 次替换,零违规)" | `src/widgets/` 仍残留 **271 处**旧令牌类(`text-heading-*`/`mt-sm`/`py-xs`/`p-md` 等),这些类在 Tailwind v4 主题中**不存在**,渲染时无样式 | ❌ **不属实** |
| "M10 旧 portal 下线完成" | 4 个旧 portal 完整保留在 `apps/`teacher 143 文件 / student 118 / parent 190 / admin 84含全部页面源码 | ❌ **不属实**(未删 ≠ 已下线;但也未迁移) |
| "三层安全边界 L1/L2/L3 已落地" | `checkRoutePermission` 仅被**测试文件**引用;无 `middleware.ts``shell/[[...route]]/page.tsx` 不调用任何权限检查。**L1/L2 在生产请求路径上是死代码** | ❌ 代码存在但从未接线 |
| "admin 改配置 → 用户刷新生效" | 无后端时 `fetchPluginConfig` 返回**空插件集**,仪表盘渲染"暂无可见插件";本地无种子数据/无 mock**开发态开箱即为空壳** | ❌ 链路存在,起点即断 |
| ESLint 强制插件隔离与 notify 封装(`no-restricted-imports` | `eslint.config.js` 仅有 hex 颜色 + 字体名两条规则;**无任何 import 限制规则** | ❌ 不属实 |
### 1.2 一句话诊断
**portal-shell 当前是一个"质量门禁全绿、但没有任何可用页面"的空壳**微内核仪表盘骨架Shell/Registry/SlotRenderer/PluginBoundary/数据层封装)工程质量合格,但它之上**既没有登录认证链,也没有一个真实业务页面**31 个卡片插件查询的是后端并不存在的 GraphQL 字段;距离 CICD 原版35 模块、4 端完整页面体系)的功能差距约为 **140 个页面**
### 1.3 十大断裂点(按严重度排序,证据化)
#### F1.【致命】没有任何真实业务页面 —— "前端不可用"的第一根因
- 全应用仅 1 条业务路由:`src/app/shell/[[...route]]/page.tsx`catch-all`ShellPage` **不读取 route 参数**`src/app/shell/[[...route]]/page.tsx:26-45``/shell/teacher/grades``/shell/admin/users` 渲染完全相同的内容;业务区分被压缩到 query params`?classId`/`?termId` 等)。
- `route-permissions.ts` 声明的 24 条路由(如 `/shell/admin/users``/shell/teacher/lesson-plans`)在文件系统中**一条都不存在**——权限表保护的是幽灵路由(该表从 CICD 项目平移,本意给网关 middleware 用Next.js 侧从未接线)。
- 对照资产CICD 原版 **155 个页面**auth 4 + onboarding 1 + dashboard 公共 8 + admin 41 + teacher 54 + student 25 + parent 14 + management/grade 5含 register/privacy/termsEdu 旧 4 portal 共 **140 个 page.tsx**teacher 56 / student 36 / parent 24 / admin 24已在仓库内但从未接入 portal-shell旧 portal 本身缺 management/grade 与 register/onboarding 体系,属缺口待补)。
#### F2.【致命】GraphQL 契约前后端全面脱节 —— 50 个操作严格匹配 0 个
- 前端 `operations/*.graphql.ts` 定义 **50 个操作**32 query + 18 mutationREADME 写 51其中 GetMyChildrenOverview 改名去重后为 50
- 合并 schema`src/lib/api/__generated__/combined-schema.graphql`700 行)仅有 **38 个 Query 字段,且完全没有 Mutation 类型**
- **严格匹配数0/50**。唯一 root 字段名命中的是 `GetLayoutTemplates``layoutTemplates`,但其选择的 `availableSlots` 字段在 `LayoutTemplateGql` 上不存在,仍然校验失败。
- 按域不匹配统计:**admin 19/19、teacher 6/6、student 8/8、parent 4/4、sidebar 3/3、topbar 3/3、universal 7/7全部不匹配**。典型样例:
- 列表查询不存在:`grades(classId)`/`homeworks(classId)`/`exams(classId)` → 后端仅有 `grade(id)`/`homework(id)`/`exam(id)` 单查;`schedule`/`attendance`/`announcements`/`myClasses`/`terms`/`myChildren`/`users`/`roles`/`permissions`/`auditLogs`/`invitationCodes`/`school`/`lessonPlans`/`schedulingRules`/`myLearningPath`/`electiveCourses`/`aiTutorSessions`/`leaveRequests`/`search` 在 schema 中**全部不存在**。
- 形状不符:`me{id,name,email,role}` → iam `User` 实为 `userId/email/name/status/dataScope``exams``name/subject/maxScore``Exam` 实为 `title/totalScore``notifications(limit,offset)` 期望 `{items,total}` 包装 → msg 实为 `notifications(userId: ID!)` 平铺数组;`errorBookItems` 底层为 snake_case`question_id` 等)。
- **18 个 mutation 全部无契约**schema 无 Mutation 类型SaveLessonPlan/ApproveLeave/RejectLeave/UpdateUserStatus/UpdateUserRole/UpdateRolePermissions/CreateInvitationCode/RevokeInvitationCode/UpdateSchool/SaveSchedulingRule/EnrollCourse/DropCourse/MarkErrorMastered/SendAiTutorMessage 及 plugin-manager 的 4 个配置 mutation。
- **config-service resolver 实测**(唯一声称支撑全架构配置的后端):仅实现 `plugin/plugins/layoutTemplates/userLayoutOverride/pluginConfig` 5 个 Query**plugin-manager 面板依赖的 `rolePluginMapping`/`roleLayoutDefault` 及全部 update\*/reset mutation 在 schema 与 resolver 双层均未实现**——也就是说,即使后端全栈启动,"admin 改配置"这个插件架构的旗舰功能也**无法保存**。
- 唯一验证通过的查询:仪表盘启动用的 `pluginConfig`(不经 operations/,内联于 `config-fetcher.ts:24-58``PluginConfigLayoutGql``availableSlots`/`layoutSchemaJson`,形状吻合)——这就是"后端就绪时仪表盘能出框架、但所有卡片空转"的原因。
- 编译期防线自毁:`codegen.yml:49-56` 注释自述"forward-looking spec fields … not yet present in services subgraph SDL … 44 个 'Cannot query field X' 错误",被迫 `skipDocumentsValidation: true`;同时 codegen 未配置 `typescript-operations`**没有生成任何 per-operation 类型**lib/api 的返回类型全部手写——编译期 0 报错,运行期全报错。
- 后端真正可用但**没有任何 widget 使用**的资产data-ana 的 4 个仪表盘聚合查询(`teacherDashboard`/`studentDashboard`/`parentDashboard`/`adminDashboard`+ `warnings`/`mastery*`/`diagnosticReports`/`errorBook*` + config-service 5 个配置查询 + iam `user/role/dataScope` + core-edu/content 按 id 单查。
- 无 GraphQL schema 的服务:`classes``student-bff``teacher-bff``parent-bff``push-gateway``api-gateway` 为 router 除外)——`classes` 域查询当前**无处落地**。
#### F3.【致命】认证与身份链断裂 —— 没有登录页,角色写死 teacher
- 无登录/注册页面;`src/app/page.tsx` 直接 `redirect("/shell")`
- `ShellPage``x-user-id`/`x-user-role` 请求头取身份,**缺失时默认 `dev-user` / `teacher`**`page.tsx:28-31`)——任何人直达 portal-shell 都是"教师"。
- Apollo Client 期望从 `localStorage["edu_token"]` 读 JWT`ApolloProvider.tsx:18-27`),但**全应用没有任何代码写入这个 token**(没有登录流程)→ 所有 GraphQL 请求永远匿名。
- 浏览器直连 apollo-router :3000**绕过 api-gateway**JWT 校验在 gateway而 gateway 不在浏览器→router 路径上)。
#### F4.【严重】安全边界是"纸面合规" —— L1/L2 从未接入请求路径
-`middleware.ts``checkRoutePermission`/`batchCheckRoutePermission` 仅被 `*.test.ts` 引用grep 实测 60+ 处引用全部在 `__tests__`)。
- 后果:`/shell/admin/users` 对 teacher/student/parent 角色**直接放行**;唯一的真实过滤发生在 config-serviceL3而它过滤的只是"仪表盘上显示哪些卡片"。
- 权限位图 `PERMISSION_BITMAP_ORDER`**`GRADE_READ` 重复定义两次**`permission-bitmap.ts:62``:79`)——按"顺序不可变"铁律属数据缺陷,趁未签发真实 JWT 前必须去重。
- `infra/docker-compose.yml` 的 portal-shell 服务(含生产 profile环境变量写入 **`NEXT_PUBLIC_DEV_MODE: "true"`**——生产部署也以 dev 模式运行,等于永久绕过认证。
- `infra/apollo-router/router.yaml` 硬编码 `router-authorization: dev-router-secret`(明文入库)。
#### F5.【严重】设计系统三方冲突 + 令牌断裂
- **三个互相矛盾的"权威"**`docs/standards/ui-design-system.md`(纸感米白 + Fraunces/Inter/JetBrains Mono 三字体 + 学术深蓝,面向已废弃的 4 微前端方案README v2.0shadcn 标准 + Inter 单字体);实际实现(`packages/ui-tokens` = shadcn zinc 默认色板)。
- 31 个 widget 残留 **271 处**旧令牌类grep`text-heading-3`/`mt-sm`/`py-xs`/`p-md`/`space-y-xs` 等),这些类在 `@theme` 中**没有定义** → 相关间距/字号渲染为零值widget 视觉上是坏的。
- 另有批量出现的 `border border` 重复类(如 `grades-widget/index.tsx:32,42`)。
- `src/styles/tokens.css`README 附录自称"旧,待 P1 移除")仍在源码树中。
#### F6.【严重】31 个插件 ≈ 31 张信息卡片,不是功能
- 全量审计结论31/31**30 REAL + 1 PARTIAL0 STUB / 0 MOCK**——插件代码是"真"的,问题是**薄**且数据断供。平均 139 行/个,形态为"一张卡片 + 列表/表单局部"。
- 典型缺陷(审计实测):
- `question-bank`(唯一 PARTIAL新建题目**不接 mutation**,仅 push 进本地 state`local-${Date.now()}`),刷新即失。
- `lesson-plan-editor`:仅 标题/目标/内容/资源 四字段表单216 行);对照 CICD 同功能lesson-preparation 是全仓最大模块113 文件:结构树 + 纸面编辑器 + AI 建议 + 版本/审核/发布 + xyflow 画布);错误处理 `catch { /* toast */ }` 空吞(违反 notify 强制)。
- `quick-actions`:导航目标 `/homework/new``/schedule``/grades` 等**全部不存在**(缺 `/shell` 前缀且无对应路由),点击即掉进 catch-all 仪表盘。
- `global-search`:跳转目标 `/students/:id` 等不存在;`user-menu` 无登出项;`locale-switcher` 只切本地 state 不加载任何 i18n`ai-tutor` 切会话不加载历史;`notifications-widget` 分页 offset 固定 0。
- 对比 CICD 同功能页面量级:考试模块 11 页all/create/new/[id]/build/edit-rich/analytics/proctoring/grading、作业模块含扫描阅卷、成绩模块 5 页、教案模块 7 页。
- 插件间无页面级导航目标(没有可跳转的详情页),"点击卡片进详情"无从谈起。
#### F7.【严重】开发体验不可用 —— 无后端 = 空壳
- `fetchPluginConfig` 三级降级的终点是 `getDefaultConfig()`**`plugins: []`**`config-fetcher.ts:193-212`)→ 无后端/无种子数据时,仪表盘渲染空壳"暂无可见插件"。
- 仓库内**没有**本地 mock/seed/MSW 任何一条让前端独立可运行的路径;旧 portal 里的 MSW handlers`teacher-portal/src/mocks/handlers-p*.ts`,覆盖 P4/P5/P7 场景)未被复用。
#### F8.【中】路由权限命名双轨制混乱
- README 示例用点号权限(`grade.read`),实际 `PERMISSION_BITMAP_ORDER``<RESOURCE>_<ACTION>` 大写下划线(`GRADE_READ`widget manifest 的 `requiredPermissions` 字段 31 个插件**无一使用**L2 插件级门禁名存实亡)。
#### F9.【中】基础设施与文档脱节
- README §6.1 说 7 domain API 文件 "Widget 数" 与 §13.2 表格不一致topbar 4 插件 vs 2 API 的统计口径混乱,实际 admin.ts 有 20 个 hook§11.3 测试矩阵与附录 B "95 用例" 自相矛盾(实际 206
- `src/shell/Registry.tsx` 头注释写"内置插件共 28 个…admin3",实际注册 31 个admin 6——代码对、注释陈旧。
- `docs/standards/ui-design-system.md` 全文面向"4 个微前端 + Module Federation"——该架构已被 ADR-033 废弃。
- `MIGRATION_GUIDE.md`/根 README 仍写"前端Next.js + Module Federation4 微前端)"。
#### F10.【低】工程细节
- `next.config.js` 同时保留 Turbopack 与 webpack 双份配置(注释已说明,可接受)。
- `.env.local` 入库(含本地配置,虽无密钥,但 `.env.example` 已存在时应 gitignore
- `tsconfig.tsbuildinfo`286KB 构建缓存)入库。
### 1.4 可复用资产盘点(不要把婴儿和洗澡水一起倒掉)
| 资产 | 位置 | 状态 | 处置 |
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------- |
| 微内核仪表盘骨架Shell/LayoutManager/SlotRenderer/Registry/PluginBoundary/PluginLifecycle/PropsMerger | `src/shell/` + `src/shared/components/plugin-boundary.tsx` | ✅ 工程质量合格,测试覆盖 | **保留**,收缩为"角色首页仪表盘"专用 |
| 数据层封装apollo-client/useWidgetQuery/useWidgetMutation/ApiError | `src/lib/` | ✅ 合格 | **保留**改走同域代理§5.2 |
| lib/api 7 domain + 50 operations | `src/lib/api/` | ⚠️ 结构合格;**50 操作严格匹配 0 个**,类型全部手写 | **保留结构**,按真实 schema 逐域重写操作并恢复 codegen 全量类型§5.3 |
| 三级错误边界 + useErrorReport + notify | `src/shared/` + `packages/hooks/` | ✅ 合格 | **保留**,扩展到页面级 |
| 权限位图 + route-permissions 表 | `packages/shared-ts/` + `src/shared/lib/` | ⚠️ 未接线 + GRADE_READ 重复 | **接线到 middleware**,位图去重 |
| shadcn 令牌三层 + ui-components 19 件 | `packages/ui-tokens/` + `packages/ui-components/` | ✅ 合格 | **保留为唯一设计系统** |
| **4 个旧 portal 的 140 个页面实现** | `apps/{teacher,student,parent,admin}-portal/` | ⚠️ 完整但栈不同urql + next-intl + MSW + 纸感令牌) | **迁移源**:逐页移植 + 换数据层 + 换令牌 |
| 旧 portal 的 MSW handlersP4/P5/P7 全覆盖) | `apps/teacher-portal/src/mocks/` 等 | ✅ 可用 | **迁移为 portal-shell 开发兜底层** |
| next-intl messageszh-CN/en | 旧 portal `src/messages/` | ✅ 可用 | **随页面迁移** |
| PQ Manifest + APQ + Router limits 安全栈 | `public/pq-manifest.json` + `infra/apollo-router/router.yaml` | ✅ 链路完整 | **保留**manifest 随操作清单更新重新生成 |
| 测试基座vitest + 19 文件 206 用例) | `src/**/__tests__/` | ✅ 合格 | **保留**,新增页面级测试 |
---
## 2. 问题根因分析
为什么"质量门禁全绿"的系统会完全不可用?后续工作必须避免重蹈覆辙,四条根因:
1. **验收指标错位**:门禁只覆盖 typecheck/lint/单测/build 这些"骨架指标",从未把"页面数、契约匹配率、关键用户流 E2E"纳入验收。于是"206 测试全过"与"没有一个可用页面"同时成立。**对策**§10 的每个阶段退出标准必须包含功能指标 + 可复现证据。
2. **契约逆向编写**:前端 operations 按 spec 文档"超前"编写50 个操作按设计意图而非后端实现,**0 个通过严格校验**),后端子图只落地了 38 个只读查询、0 个 mutation。`skipDocumentsValidation: true` + 不生成 per-operation 类型,让这种脱节编译期不可见。**对策**§5.3 契约纪律——operations 只允许引用真实 schema 字段mock 数据走 MSW 而不是"假契约"。
3. **范围误判**v2.1 把"统一前端"收缩成"统一仪表盘",默认了"页面以后再说"但没有文档记录这个范围缺口README 反而把仪表盘骨架的完成写成了整个前端的完成。**对策**:本文件 §7/§9 把页面体系定义为 portal-shell 的一等公民职责。
4. **文档激励扭曲**:多处"已完成"声明与实测不符(令牌迁移、旧 portal 下线、安全边界),说明验收只看了 PR 描述没看运行态。**对策**§11.6 文档同步纪律——验收声明必须附可复现命令输出Reviewer 运行命令复核。
---
## 3. 目标架构 v3.0
### 3.1 设计原则
| # | 原则 | 含义 |
| --- | ------------------ | -------------------------------------------------------------------------------------------------------------- |
| P1 | **页面一等公民** | 业务功能落在真实 Next.js 路由页面上(可寻址、可分享、可深链);插件仪表盘只承担"角色首页聚合" |
| P2 | **契约真实** | 前端 GraphQL 操作只允许引用后端真实 schema后端未就绪的功能用 MSW mock 数据,禁止"假契约" |
| P3 | **fail-closed** | 无身份 → 登录页;无权限 → 403 页;配置缺失 → 内置默认(而非空壳);错误 → 显式兜底(而非静默) |
| P4 | **单一设计系统** | shadcn 标准令牌(@edu/ui-tokens为唯一视觉权威任何页面/插件不引入第二套令牌 |
| P5 | **微内核收敛** | Shell 插件系统收缩为仪表盘专用机制,不再承载"整个应用"的野心;新增功能默认建页面而非建插件 |
| P6 | **迁移优先于重写** | 140 个旧页面是迁移源而非重写对象移植时换数据层urql→Apollo hooks、换令牌纸感→shadcn、保留业务交互逻辑 |
| P7 | **证据化验收** | 每个里程碑以可复现命令 + 运行态截图/录屏验收,禁止"声明即完成" |
### 3.2 总体结构(混合路由模型)
```
┌────────────────────────────────────────────────────────────────┐
│ 浏览器 │
└──────────────┬─────────────────────────────────────────────────┘
┌──────────────▼─────────────────────────────────────────────────┐
│ portal-shell :4010Next.js 16 App Router 单容器) │
│ │
│ middleware.ts
│ ├─ 公共路径放行(/login、/api/health、静态资源
│ ├─ JWT cookie 校验 → 注入 x-user-id/x-user-role/x-perms │
│ └─ checkRoutePermissionL1 角色 + L2 权限点fail-closed
│ │
│ /login 登录页(新) │
│ /shell/forbidden 403 页(新) │
│ /shell ───────────────────── ┐ │
│ layout.tsx新框架 │ AppFrameTopBar + Sidebar │
│ ├─ page.tsxcatch-all │ + 导航菜单(按角色/权限过滤) │
│ │ = 角色仪表盘 │ │
│ │ (微内核插件区,保留) │ │
│ ├─ teacher/exams/page.tsx │ 真实业务页面(迁移) │
│ ├─ teacher/exams/[id]/… │ │
│ ├─ student/my-grades/… ┘ │
│ └─ … ~140 页面 │
│ │
│ /api/graphql GraphQL 同域代理 │
│ └─ 从 httpOnly cookie 取 JWT → Authorization → apollo-router │
│ /api/auth/login 登录代理:调 iam → Set-Cookie │
│ /api/health /api/ready /api/log 保留 │
└──────────────┬─────────────────────────────────────────────────┘
│ 仅两条出站:
├─ /api/graphql → apollo-router :3000业务查询APQ
└─ /api/v1/* → api-gateway :8080REST认证/上传/SSE 等)
```
**关键决策v3.0 ADR 摘要,详见 §3.4**
- **V3-A1 混合路由模型**:真实页面 + 保留仪表盘微内核。废除"一切皆卡片、单 catch-all 承载全站"的极端catch-all 仅保留给仪表盘(可选路由参数如 `?layout=`),新页面一律走显式路由。
- **V3-A2 认证链闭环**:登录页 + `/api/auth/login` 代理 + httpOnly cookie + middleware 校验 + `/api/graphql` 代理。**删除 localStorage JWT 方案**。
- **V3-A3 门禁接线**`checkRoutePermission` 移入 `middleware.ts` 真实执行;新增路由必须在 `route-permissions.ts` 登记CI 校验路由表与文件系统一致性)。
- **V3-A4 契约纪律**operations 与真实 schema 强绑定codegen 恢复 `typescript-operations` 全量类型;`skipDocumentsValidation` 随后端补齐分域关闭§5.3)。
- **V3-A5 设计系统单源**shadcn 标准zinc为唯一方向废止纸感全局方案备课编辑器二期可用模块命名空间`--lp-*`)局部纸感化。
- **V3-A6 接入 next-intl**:复用旧 portal 的 messages页面迁移不丢失已翻译文案。
- **V3-A7 MSW 开发兜底**`NEXT_PUBLIC_MSW=1` 时启用,前端无后端可开发/演示/跑 E2E生产构建永不包含。
- **V3-A8 验收门禁改革**CI 增加契约匹配率检查、路由表一致性检查、页面计数、Playwright E2EP6
### 3.3 与现有架构的关系(保留什么、改变什么)
| 现有v2.0 | v3.0 处置 | 理由 |
| --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| 单 catch-all `/shell/[[...route]]` 承载一切 | **收缩**:仅渲染角色仪表盘;新增显式路由优先于 catch-allNext.js 路由规则天然支持) | 页面可寻址、RSC 预取、loading/error 按路由生效 |
| LayoutManager 5 模板classic/focus/split/triple/canvas | **保留**为仪表盘区布局;页面区用固定 AppFrameTopBar+Sidebar+Main | 模板化价值在仪表盘;页面需要稳定框架 |
| 31 个 widget 插件 | **保留骨架**,内容逐步替换/下线:与页面功能重复的卡片改为"页面入口卡 + 关键摘要",数据接通真实 schema | 卡片应该是页面的摘要与入口,不是功能的全部 |
| config-service 三层插件配置 | **保留**,职责收缩为"仪表盘 + 导航可见性"配置 | 页面不再依赖插件配置,配置挂了不影响页面 |
| SWR 配置静默刷新 | **保留**于仪表盘 | 低频配置变更无需推送 |
| URL Search Params 跨插件共享 | **保留**并扩展为页面间状态契约classId/childId/termId | 已验证有效 |
| RSC 流式渲染use() + Suspense | **保留**并推广到页面级(页面 RSC 预取 + 流式注入) | 收益真实 |
| 三级错误边界 | **保留**扩展Route页面 error.tsx→ Section页面区块→ Widget卡片 | 与页面体系对齐 |
### 3.4 v3.0 ADR 全文
#### V3-A1混合路由模型真实页面 + 仪表盘微内核)
- **背景**v2.0 用单 catch-all + 插件配置承载全站导致页面不可寻址F1、权限表空转F4、功能止步于卡片F6
- **决策**:业务功能落在显式 Next.js 路由页面(`/shell/<role>/<module>/...`),共享 `AppFrame` 布局;插件系统收缩为 `/shell` 角色首页的聚合仪表盘。
- **取舍**:放弃"admin 可配置一切页面"的幻想——页面是代码、受版本控制、可 code reviewadmin 可配置的收缩为"导航可见性 + 仪表盘卡片 + 卡片 props"。这换来:真实 URL、浏览器前进后退、RSC 流式预取、路由级 loading/error、中间件门禁——全部是 Next.js 原生能力,不再自造。
- **插件与页面的关系**:卡片 = 页面的摘要 + 入口。点击卡片 → `router.push` 到对应页面。universal 插件按 role 渲染不同摘要视图的机制保留。
#### V3-A2认证链闭环 + 删除 localStorage JWT
- **背景**F3无登录页、localStorage token 无人写入、默认角色 teacher
- **决策**
1. `/login` 页面(迁移自旧 portal login换 shadcn 令牌)。
2. `/api/auth/login` Route Handler转发凭证到 iam经 api-gateway成功后把 JWT 写入 **httpOnly + Secure + SameSite=Strict cookie**project_rules §4JS 永远接触不到 token。
3. `middleware.ts`:读取 cookie → 校验(开发态 decode 验签跳过,生产用 iam JWKS RS256 验签)→ 注入 `x-user-id`/`x-user-role`/`x-user-permissions` 请求头供 RSC 读取;无 token → redirect `/login?next=<pathname>`
4. `/api/graphql` Route HandlerApollo Client 的 HttpLink 指向同域 `/api/graphql`Handler 从 cookie 取 token 加 `Authorization: Bearer` 转发 apollo-router响应原样回传含 APQ hash 请求)。
- **取舍**:多一跳同域代理(<5ms 本地开销),换来 token 全程不出 httpOnly cookie——消除 XSS 窃取凭证面;同时修复"浏览器绕过 api-gateway"问题(代理在服务端调 router与 gateway 同信任域)。
- **DEV_MODE**:仅本地开发;提供 `dev-token` 合成身份middleware 生成 x-user-* 头)。生产环境变量缺失/为 true 时启动直接拒绝(`instrumentation.ts` 启动校验)。
#### V3-A3路由门禁 middleware 接线 + 权限模型修正
- **背景**F4checkRoutePermission 死代码、F8命名双轨
- **决策**
1. `middleware.ts` 对每个 `/shell/**` 请求执行 `checkRoutePermission(pathname, bitmap, role)`;拒绝 → `/shell/forbidden`403 页,新)。
2. `route-permissions.ts` 四表保留,**新增/移动路由必须同步登记**CI 运行一致性脚本:扫描 `src/app/**/page.tsx` 路由 ⇄ 权限表双向核对,未登记即失败。
3. 权限点唯一来源 `PERMISSION_BITMAP_ORDER`README 的点号示例作废31 个 manifest 补登 `requiredPermissions`L2 插件级config-service 过滤用)。
4. `GRADE_READ` 重复项去重:保留首次出现位(索引 26删除第二次索引 31 处),该位标记 `_RESERVED_31` 占位永不复用;趁系统未签发真实 JWT现在就改P0
- **边界说明**middleware 是 L1/L2 的**第一道**防线(性能/体验不是唯一防线——resolver 级 `@RequirePermission`(后端)仍是权威。前端门禁防"走错门",后端门禁防"恶意请求"。
#### V3-A4GraphQL 契约纪律
- **背景**F250 操作 vs 后端 38 查询/0 mutation严格匹配 0/50
- **决策**
1. **操作清单真实化**`operations/*.graphql.ts` 只允许引用 `combined-schema.graphql` 真实存在的字段。每新增一个页面,先确认 schema 有所需字段;没有 → 走"契约工单"§11.4)推动后端补齐,**同时**用 MSW mock 让页面先行。
2. **codegen 恢复强校验**:分域关闭 `skipDocumentsValidation`(哪个域后端补齐了就关哪个域),恢复 `typescript-operations` 生成操作级类型,**删除 lib/api 手写 inline 类型**(漂移源)。
3. **schema 同步自动化**`scripts/normalize-schema.ts` 保留;新增 CI 步骤:子图 SDL 变更 → codegen diff 检查operations 引用不存在字段即失败。
4. **PQ Manifest 随构建更新**`prebuild` 已串联,保留。
- **后端已就绪可立即使用的资产**config-service 全量配置查询data-ana 的 4 个 `*Dashboard` 聚合查询 + `warnings`/`mastery*`/`diagnosticReports`/`errorBook*`iam 的 `user/role/dataScope`core-edu/content 的按 id 单查。**仪表盘首页应优先改用这些真实查询**P1 任务)。
#### V3-A5设计系统单源决议
- **背景**F5三方冲突 + 271 处断裂类)。
- **决策**
1. **shadcn 标准令牌(@edu/ui-tokenszinc 色板)为唯一设计系统**全局适用dashboard + 全部页面 + 登录页)。
2. `docs/standards/ui-design-system.md` 废止;其仍有价值的内容并入本文件 §8布局/组件/动效/a11y 规范,按 shadcn 令牌重写)。
3. 纸感编辑器方向Fraunces 主文 + 米白纸面)**降级为二期备课编辑器模块命名空间**`--lp-*`),仅 lesson-plan 工作台内部使用;现在不做。
4. 31 widget 令牌清债271 处)列入 P1机械替换`text-heading-3``text-lg font-semibold``mt-sm``mt-2``py-xs``py-1``px-sm``px-2``space-y-xs``space-y-1``p-md``p-4``gap-xs``gap-1``border border``border`arch:scan 增加"未知 Tailwind 类"检测规则。
- **理由**CICD 是已验证的 UX 基线shadcn 生态组件、文档、AI 协作默契度)最成熟;纸感全局化在旧 portal 实践中与 shadcn 组件冲突成本高,收敛为局部模块更现实。
#### V3-A6next-intl 正式接入
- **背景**:旧页面用 `useTranslations`messages 资产完整portal-shell 现有自造 `useT()` 只有 3 条文案。
- **决策**:接入 next-intlApp Router 模式),`messages/zh-CN.json` + `messages/en.json` 从旧 portal 合并迁移;`ThemeI18nProvider` 删除自造 i18n保留主题切换新代码文案一律走 `useTranslations`禁止硬编码中文字符串ESLint 规则 P1 后启用,迁移期 warn
- **取舍**:增加一个依赖与少量配置,换取不重写全部文案 + 与 CICD/旧 portal 一致的 i18n 习惯。
#### V3-A7MSW 开发兜底层
- **背景**F7无后端 = 空壳)。
- **决策**:迁移旧 portal 的 MSW handlers 为 portal-shell 的 `src/mocks/`(按 domain 分文件);`NEXT_PUBLIC_MSW=1` 时 browser worker 拦截 `/api/graphql``/api/v1/*``src/instrumentation.ts`(或 middleware 旁路)在 dev + MSW 开启时给 RSC 侧也注入 mock fetcher。**生产构建通过 env 条件 import 保证 mocks 零字节进 bundle**。
- **双重收益**前端不依赖后端就绪即可开发页面Playwright E2E 直接复用 MSW 场景数据,测试稳定。
#### V3-A8验收门禁改革
- **背景**:根因 1验收指标错位
- **决策**CI 质量门禁从 4 项扩为 8 项:`lint` + `typecheck` + `vitest` + `build` + **契约校验**codegen diff+ **路由表一致性** + **页面计数回归**pages 数不得下降)+ **E2E 冒烟**P6 起:登录 → 仪表盘 → 每角色 1 条核心流)。每个阶段的 README/本文件验收勾选必须附**命令输出或截图链接**。
---
## 4. 认证与身份链
### 4.1 目标流程
```mermaid
sequenceDiagram
participant U as 浏览器
participant PS as portal-shell
participant GW as api-gateway :8080
participant IAM as iam
participant RT as apollo-router :3000
U->>PS: POST /api/auth/login {email, password}
PS->>GW: POST /api/v1/iam/auth/login
GW->>IAM: 验证凭证
IAM-->>GW: JWT (RS256) + user + permissions bitmap
GW-->>PS: 200 {token, user}
PS-->>U: Set-Cookie: edu_session=<JWT>; httpOnly; Secure; SameSite=Strict
Note over U: JS 永远拿不到 token
U->>PS: GET /shell/teacher/exams
PS->>PS: middleware: 校验 cookie → 注入 x-user-* 头 → checkRoutePermission
alt 无/坏 token
PS-->>U: 302 /login?next=/shell/teacher/exams
else 无权限
PS-->>U: 302 /shell/forbidden
else 放行
PS->>PS: RSC 读 x-user-* 头渲染页面
end
U->>PS: POST /api/graphql (Apollo, APQ hash)
PS->>PS: 从 cookie 取 token
PS->>RT: POST /graphql + Authorization: Bearer <JWT>
RT-->>PS: data
PS-->>U: data
```
### 4.2 组件清单
| 组件 | 文件 | 职责 |
| ------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| 登录页 | `src/app/login/page.tsx` | 表单 → `/api/auth/login`;已登录跳转 next迁移自旧 portal换 shadcn 令牌 |
| 登录代理 | `src/app/api/auth/login/route.ts` | 调 gateway iam 登录 → Set-Cookie失败归一化错误限流透传 |
| 登出 | `src/app/api/auth/logout/route.ts` | 清 cookie + 调 iam 注销 |
| 中间件 | `src/middleware.ts` | 公共路径白名单 → cookie 校验 → 注入身份头 → `checkRoutePermission` → 重定向 |
| GraphQL 代理 | `src/app/api/graphql/route.ts` | cookie→Bearer 转发APQ 透传;错误归一化;`cache: "no-store"` |
| RSC 身份工具 | `src/lib/auth/server.ts` | `getServerIdentity(): Promise<{userId, role, permissions}>`(读 middleware 注入的头,缺失即抛 → error boundary 跳登录) |
| DEV_MODE 守卫 | `src/instrumentation.ts` | `NODE_ENV=production && NEXT_PUBLIC_DEV_MODE=true` → 启动即抛错 |
| 403 页 | `src/app/shell/forbidden/page.tsx` | 说明 + 返回仪表盘链接 |
### 4.3 会话细节决策
- **cookie 名**`edu_session``Path=/``Max-Age` 与 JWT exp 对齐;`Secure` 仅生产(本地 http 开发豁免,用 env 控制)。
- **验签**middleware 用 `jose``createRemoteJWKSet(iam JWKS endpoint)` 验 RS256JWKS 端点不可达时 **fail-closed**跳登录dev 模式降级为仅 decode日志警告
- **权限位图来源**:登录响应中 iam 返回 permissions → login route 计算 base36 位图 → 写入第二个非 httpOnly cookie `edu_perms`JS 可读,用于客户端按钮级显隐;**仅为 UX不作为安全依据**——安全判断一律走后端 resolver + middleware 的 httpOnly 通道。middleware 同样把位图注入 `x-user-permissions` 头。
- **角色切换/多角色**iam 返回单主角色(与现 `x-user-role` 对齐);多角色支持属后端二期,前端不预留。
- **旧 portal 的 `lib/auth.ts`localStorage 方案)**:迁移时**删除**,不按原样搬运。
---
## 5. 数据层架构
### 5.1 分层(保留 v2.0 四层,修正两端)
```
页面/插件UI
→ lib/api/<domain>.ts语义化 hooksuseExams(classId)
→ lib/api/operations/*.graphql.tsgql 文档,真实 schema 子集)
→ lib/useWidgetQuery / useWidgetMutationApollo 封装 + ApiError
→ Apollo ClientAPQ link→ /api/graphql同域代理
→ apollo-router :3000 → 子图
```
- **RSC 服务端**:页面 RSC 用 `createApolloClient()`(已有)经内网直连 router服务端不经代理直接带 middleware 解析出的身份头);客户端组件经 `/api/graphql` 代理。
- **SWR/轮询**:仪表盘配置保留 SWR业务数据默认 Apollo `fetchPolicy: "cache-first"`,需要实时性的(通知铃铛)用 `pollInterval`SSE 通知二期经 realtime-gateway。
### 5.2 GraphQL 同域代理(/api/graphql
- 单文件 Route Handler`POST` only透传 bodyAPQ hash 或 query注入 `Authorization`;响应状态/JSON 原样返回。
- 不缓存(`export const dynamic = "force-dynamic"`)。
- 错误归一化:网络错误 → `{ errors: [{ message: "UPSTREAM_UNAVAILABLE" }] }`,客户端 `ApiError` 统一。
- 保留直连开关:`APOLLO_ROUTER_URL`(服务端 RSC 用),客户端永远只用 `/api/graphql`
### 5.3 契约纪律frontend ⇄ backend
1. **schema 唯一源**`src/lib/api/__generated__/combined-schema.graphql``scripts/normalize-schema.ts``services/*/src/graphql/generated/schema.graphql` 生成;**后端 SDL 变更后必须重跑 codegen**CI 做 diff 检查。
2. **operations 真实性**:每个 operation 引用字段必须存在于 combined schema恢复 `typescript-operations` 生成类型lib/api 的手写 interface 逐步删除P1 起按域推进config → data-ana → core-edu → content → msg → iam
3. **后端未就绪的功能**:页面允许先上,数据走 MSW`src/mocks/`),并在页面头部注释 `@contract-pending: <工单号>`契约工单§11.4)登记后端待补字段,**每周同步一次状态**。
4. **禁止事项**:禁止在 widget/页面内联 gql禁止手写与 schema 冲突的类型;禁止为迁就前端瞎改 normalize 脚本(当前对 ai 子图的 String 改写保留,单独登记 TD
### 5.4 MSW 兜底层
- 位置:`src/mocks/``browser.ts`/`server.ts`/`handlers/<domain>.ts`),自旧 portal `src/mocks/handlers-p*.ts` 迁移并按 50+ 操作重组。
- 启用:`NEXT_PUBLIC_MSW=1`(仅 dev/test`src/app/layout.tsx` 条件 `import("@/mocks/browser")`(动态 importproduction env 下该 import 永不执行 → tree-shaken
- 数据原则使用与种子用户一致的语义数据teacher2@edu.test 的班级/学生/成绩),数据量足够展示分页/空态/超长文本三种边界。
- E2E 复用Playwright webServer 以 `NEXT_PUBLIC_MSW=1` 启动,用例不依赖真实后端。
### 5.5 后端已就绪查询的立即利用P1 必做)
| 真实查询schema 存在且 resolver 已实现) | 用于 | 注意 |
| ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| `pluginConfig(userId, role)` / `plugins` / `plugin(pluginId)` / `userLayoutOverride(userId)` | 仪表盘配置(已接,保留) | ✅ 全字段吻合 |
| `layoutTemplates` | admin 模板列表 | ⚠️ 仅 `layoutId/displayName/description/isActive`**无 `availableSlots`**——前端选择集必须裁剪 |
| `teacherDashboard` / `studentDashboard` / `parentDashboard` / `adminDashboard` | **角色首页仪表盘主数据源**(替换现有卡片的假契约查询) | ✅,底层字段 snake_case 需在 lib/api 层映射 |
| `warnings` / `masterySummary` / `studentMastery` / `masteryDistribution` | 学情预警卡片 | ✅,同上 |
| `diagnosticReports` / `errorBookItems` / `errorBookStats` | 诊断/错题卡片与页面 | ✅,字段形状与现 widget 期望不同,需重写 hook |
| `user(userId)` / `role(roleId)` / `dataScope(userId)` | 用户菜单、权限显隐 | ⚠️ `User` 字段为 `userId/email/name/status/dataScope`(无 `id`/`role` |
| `exam(id)` / `homework(id)` / `grade(id)` / `classInfo(id)` / `textbook(id)` / `question(id)` / `chapter(id)` / `knowledgePoint(id)` | 详情页首版(列表点击带 id 进入时可真实查询) | ⚠️ 字段名需按真实类型重写(如 `Exam.title/totalScore` 而非 `name/maxScore` |
| `notifications(userId)` | 通知中心首版 | ⚠️ 平铺数组,无分页包装 |
| `lessonPlanStatus` | ai 子图唯一查询 | 仅状态轮询可用 |
---
## 6. 安全架构(安全边际)
### 6.1 威胁模型与防线矩阵
| 威胁 | 防线 | 位置 | 现状 | v3.0 处置 |
| ----------------------------------- | ----------------------------------------------------- | --------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------- |
| 未认证访问业务页 | middleware cookie 校验 | `src/middleware.ts` | ❌ 不存在 | P0 实现fail-closed |
| 越权访问路由teacher 进 admin 页) | L1 角色 + L2 权限点 | middleware + route-permissions 表 | ❌ 死代码 | P0 接线 + CI 一致性 |
| 越权访问数据 | resolver `@RequirePermission` + DataScope | 后端子图 | ✅(审计过 50 resolver | 保持;前端不兜底也不替代 |
| 凭证被 XSS 窃取 | httpOnly cookieJS 零接触 | `/api/auth/login` | ❌ localStorage 方案 | P0 替换 |
| GraphQL 任意查询探测 | APQ + PQ Manifest 白名单 | Apollo link + router | ✅ 链路完整(`require_manifest` 生产需开) | 保持;部署 env 列入 P6 检查单 |
| 深度/成本/批量 DoS | router limits | `router.yaml` | ✅ 已配 | 保持 |
| 绕过 router 直调子图 | RouterAuthGuard + `router-authorization` | 子图 | ⚠️ 密钥明文 `dev-router-secret` 入库 | P6 换 env 注入;生产 secret 轮换 |
| CSRF | SameSite=Strict cookie + 自定义头要求 | cookie + 代理 | ⚠️ 依赖 cookie 属性 | `/api/graphql` 要求 `content-type: application/json`(预检),登录接口 SameSite=Strict |
| 生产 DEV_MODE 误开 | 启动自检 | `instrumentation.ts` | ❌ compose 生产 profile 写 true | P0compose 改 false + 启动校验抛错 |
| 错误上报刷爆 | sessionStorage digest 节流 | `useErrorReport` | ✅ 已实现 | 保持 |
| XSS 富文本 | 禁 `dangerouslySetInnerHTML`grep 实测 src 内 0 处) | ESLint + 规范 | ✅ | 保持规则;富文本编辑器(二期)必须 DOMPurify |
| 敏感信息入仓 | `.env.local` 已入库(无密钥) | 仓库 | ⚠️ | P0移出 + gitignore 补规则 |
### 6.2 三层安全边界(接线后真实形态)
| 层 | 机制 | 执行点 | 失败行为 |
| ----------- | ------------------------------------------------------------ | -------------------------------------------------- | ------------------------- |
| L1 角色门禁 | `requiredRoles` | **middleware**(每个 /shell/** 请求) | 302 → `/shell/forbidden` |
| L2 权限点 | `requiredPermissions`/`anyOfPermissions`AND/OR位图校验 | **middleware**(路由级)+ config-service卡片级 | 302 → 403 页 / 卡片不渲染 |
| L3 数据范围 | DataScope 6 级 | 后端 resolver权威前端仅 UX 显隐 | 后端拒绝 / 空态 |
**边界纪律(防"安全边际"幻觉)**
1. 前端门禁是**体验层**,后端 resolver 是**权威层**;任何"前端已挡"不得成为后端不加 `@RequirePermission` 的理由。
2. `edu_perms` 可读 cookie 只用于按钮显隐;**禁止**用它做任何数据请求的放行判断。
3. middleware 校验失败一律 fail-closed`checkRoutePermission` 未匹配的路由**默认拒绝**当前实现默认放行P0 改为:/shell/** 下未登记路由默认拒绝,公共路径显式白名单)。
4. JWT 过期统一处理:`/api/graphql` 收到 401 → 清 cookie → 返回 `{ errors: [{extensions:{code:"UNAUTHENTICATED"}}] }`;客户端 Apollo errorLink 拦截 → `location.href = "/login"`
---
## 7. 信息架构与页面体系
### 7.1 路由树(目标态)
```
/login 登录
/shell/forbidden 403
/shell 角色仪表盘(按 x-user-role 分发渲染对应仪表盘)
/shell/teacher/… 教师功能区(~50 页,见 §9.1
/shell/student/… 学生功能区(~34 页,见 §9.2
/shell/parent/… 家长功能区(~21 页,见 §9.3
/shell/admin/… 管理功能区(~22 页,见 §9.4
/shell/settings 通用设置(全角色)
/shell/notifications 通知中心(全角色)
```
- URL 前缀 `/shell/<role>/` 与 route-permissions 表语义对齐PREFIX 表按前缀批量保护)。
- 仪表盘 `/shell` 按角色渲染不同插件集(现有机制),同时是各功能卡的入口枢纽。
- 跨角色同构页面(如 notifications/settings放共享路径权限表按角色开放。
### 7.2 AppFrame页面框架
`src/app/shell/layout.tsx`(新):
```
┌──────────────────────────────────────────────┐
│ TopBarglobal-search · notification-bell │
│ locale-switcher · user-menu │
├──────────┬───────────────────────────────────┤
│ Sidebar │ Main{children}
│ 导航菜单 │ 页面区(显式路由页面) │
│ (按角色/ │ 或仪表盘(/shell
│ 权限过滤)│ │
│ + 上下文 │ │
│ 选择器 │ │
└──────────┴───────────────────────────────────┘
```
- **TopBar**:复用现有 4 个 topbar 插件(它们本就是全站级组件),不再走插件配置(改为静态挂载 + 权限显隐),仪表盘配置不再能"关掉导航"导致页面失联。
- **Sidebar 导航菜单(新组件)**:导航项 = 静态注册表 `src/shared/lib/navigation.ts`label/i18n key/icon/href/requiredRoles/requiredPermissions渲染前用 `batchCheckRoutePermission` 过滤(对应 004 §5.4 视口 L1当前路由高亮收纳现有 4 个 sidebar 插件class-selector/child-selector/term-switcher/quick-actions于菜单下方"上下文区"。
- **Main**:仪表盘路由渲染微内核仪表盘;业务路由渲染页面。页面内可用 `DashboardSection`/`SectionErrorBoundary` 组织区块。
- **RSC 流式**layout 保持轻量(导航静态),页面各自 RSC 预取 + Suspense现有 ClientShell 流式机制迁移为页面级工具(`src/lib/streaming.tsx` 导出 `StreamedPage` 封装,避免每页重写 Provider/Suspense 样板)。
### 7.3 页面四种类型与模板(新页面必须套模板)
| 类型 | 适用 | 结构骨架 | 示例 |
| ------------ | ---------------------- | ------------------------------------------------------------------------- | ---------------------------------------- |
| **列表页** | 集合浏览 + 筛选 + 分页 | PageHeader + FilterBar + DataTable/列表 + Pagination + 空态/骨架/错误三态 | exams、homework、users |
| **详情页** | 单实体查看 + 关联操作 | PageHeader标题+操作) + 信息区 + Tabs/分区 + 关联列表 | exams/[id]、textbooks/[id] |
| **表单页** | 创建/编辑 | PageHeader + 表单react-hook-form + zod+ 提交/取消 + 错误摘要 | exams/new、lesson-plans/new |
| **工作台页** | 多栏协同复杂任务 | 三栏(树/画布/属性复合组件Workbench 模式) | lesson-plans/[id]/edit、exams/[id]/build |
模板代码位置:`src/shared/components/page-templates/{list-page,detail-page,form-page,workbench-page}.tsx`P1 建)。**新页面一律 `import` 模板而非手排布局**——保证 140 页视觉/交互一致,也是 AI 批量迁移时的统一落点。
### 7.4 页面级数据获取模式(统一)
```tsx
// src/app/shell/teacher/exams/page.tsxRSC 示例骨架)
export default async function ExamsPage(): Promise<React.ReactElement> {
const { userId, role } = await getServerIdentity();
const client = createApolloClient();
const dataPromise = client.query({
query: GET_EXAMS_DOC,
variables: {/* … */},
});
return (
<ListPageShell titleKey="exams.title">
{/* Promise 直传客户端组件use() 消费(沿用现有流式模式) */}
<ExamsList dataPromise={dataPromise} />
</ListPageShell>
);
}
```
- 列表/详情页RSC 预取首屏 → `use()` 流式注入 → 客户端交互(筛选/翻页)走 Apollo hooks。
- 表单页:客户端组件 + `useWidgetMutation`;成功后 `router.refresh()`
- 每页必须实现三态:`loading.tsx`(骨架,复用 PluginSkeleton 变体)/ `error.tsx`Route 级边界,已有模式)/ 空态(`empty-state.tsx`)。
---
## 8. 前端设计规范(强制)
> 本章是 portal-shell 唯一的视觉/交互权威,废止 `docs/standards/ui-design-system.md`。所有新页面/插件/组件必须遵守;迁移页面时按本章改造。违反本节任意"铁律"的 PR 不得合入。
### 8.1 设计令牌(三层,唯一来源)
| 层 | 位置 | 业务可用性 |
| ------------ | -------------------------------------------------------------- | ---------------------------------------------------------------- |
| L1 Primitive | `packages/ui-tokens/src/primitive.css` | ❌ 禁止直接引用 |
| L2 Semantic | `packages/ui-tokens/src/semantic-{light,dark}.css` | ✅ 经 `hsl(var(--*))` |
| L3 Tailwind | `packages/ui-tokens/src/tailwind-theme.css``@theme inline` | ✅ 首选:`bg-background`/`text-foreground`/`bg-card`/`border` 等 |
**铁律ESLint + code review 双重执行)**
1.`#hex` 字面量(现有规则保留)。
2. 禁字体名字面量(现有规则保留)。
3.`font-size: Npx`;字号用 Tailwind 阶梯(`text-xs/sm/base/lg/xl/2xl`)或 `text-size-*`
4. 禁 Tailwind 任意值(`w-[137px]`);间距用默认阶梯(`p-2/p-4/p-6/gap-2/gap-4`)或 `--space-*`
5. **禁引入第二套令牌/第二套 CSS 变量体系**(纸感 `--paper-*`/`--ink-*` 一律不得回潮;迁移旧页面时必须清除)。
6. 暗色主题:仅经 `.dark` class + 语义令牌响应;禁组件内写死亮色值。
7. 圆角:`rounded-xl`(卡)/`rounded-md`(控件)/`rounded-full`(头像徽章);阴影克制:浮层 `shadow-sm`,卡片默认无阴影。
### 8.2 字体与排版
- **单一字体族 Inter**`next/font` 已挂载 `--font-inter``font-sans`);数字密集表格列用 `font-mono`Tailwind 默认等宽)右对齐。
- 标题层级:页面标题 `text-2xl font-semibold`;区块标题 `text-lg font-semibold`;卡片标题 `text-base font-medium`;正文 `text-sm`;辅助 `text-xs text-muted-foreground`
- 行高:正文 `leading-relaxed`;标题 `leading-tight`
- 中文界面默认 `lang="zh-CN"`已配置i18n 文案经 next-intl。
### 8.3 组件使用
- **基础组件一律来自** `src/shared/components/ui/*`shadcn 标准件)与 `packages/ui-components/*`data-table/form/modal/chart/calendar 等 19 件);缺组件先扩库再使用,**禁止页面内手写第三套按钮/输入框**。
- 业务复合件:`page-templates` 四件套§7.3+ `page-header`/`stat-card`/`stats-grid`/`empty-state`/`filter-bar`(已有)。
- Toast 一律 `notify.*``src/shared/lib/notify.ts`**禁** `import { toast } from "sonner"`P0 补 ESLint `no-restricted-imports` 真正落地);**禁空 catch**(现有 `catch { /* toast */ }` 反例必须改)。
- 图标:`lucide-react` 单一来源;图标按钮必须 `aria-label`
- 表格:数据列数字 `font-mono` 右对齐;行分隔 `divide-y divide-border`;禁大面积色块。
### 8.4 布局与间距
- 页面容器:`px-6 py-6`(桌面),最大宽度不设限(工作台型产品),列表页可 `max-w-7xl`
- 区块间距 `space-y-6`;卡片内边距 `p-4``p-6`
- 三栏工作台仅在"编辑类"页面使用(备课/组卷),比例 260px / 1fr / 380pxWorkbench 模板内置)。
### 8.5 动效
- 交互反馈 ≤ 200ms页面过渡 ≤ 300ms遵守 `prefers-reduced-motion`globals.css 已有)。
- 加载一律骨架屏PluginSkeleton 五变体 / 页面 loading.tsx**禁居中 spinner 长转**。
- 禁装饰性持续动画hover 反馈用 `bg-muted`/`bg-accent`,禁 `scale-105`
### 8.6 可访问性WCAG AA
- 对比度:正文 ≥ 4.5:1zinc 令牌天然满足,禁自调浅色)。
- 所有交互元素可 Tab 聚焦;`focus-visible:ring-2 focus-visible:ring-ring`
- 语义化标签(`nav/main/aside/table`);表单控件必须 `<label>``aria-label`;动态更新区 `aria-live="polite"`(骨架组件已带,页面复用即可)。
- 右键/下拉菜单支持键盘Radix 组件默认满足,勿破坏)。
### 8.7 文案与 i18n
- 一律 `useTranslations`;命名空间按页面域(`exams.*`/`homework.*`/`common.*`)。
- 占位/空态文案必须给出"下一步行动"(如"暂无考试 → 新建考试"按钮),禁裸"暂无数据"。
---
## 9. 页面迁移总表(~140 页)
> 功能全景基准CICD 原版 **155 页**Next.js 16 + Server Actions + Drizzle无 GraphQLauth 4 + onboarding 1 + dashboard 公共 8 + admin 41 + teacher 54 + student 25 + parent 14 + management/grade 5
> 迁移源:仓库内 4 个旧 portal`apps/*-portal/`,共 140 个 page.tsx其页面实现完整但栈为 urql + next-intl + MSW + 纸感令牌。
> 缺口说明:旧 portal 未覆盖 CICD 的 management/grade 年级组维度5 页)与 register/privacy/terms/onboarding4+ 页)——这两块列为**新增N**,排在对应批次末尾。
> 迁移动作统一定义:**M = 迁移改造**(移植交互逻辑 + 换 Apollo 数据层 + 换 shadcn 令牌 + 套页面模板);**N = 新建**(旧版没有);**R = 替换**(现有 widget 升级为页面)。
> 批次即 §10 路线图的阶段B1=先行骨架P1、B2=教师P2、B3=学生P3、B4=家长P4、B5=管理P5
> 契约列:✅=schema 已有(注意字段形状差异,见 §5.5);🟡=部分(单查有/列表无);❌=schema 无,需契约工单 + MSW 先行。
### 9.1 教师域teacher-portal 56 页 → /shell/teacher/*B1+B2
| 源路由teacher-portal | 目标路由 | 动作 | 契约 | 批次 |
| ------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------- | ---------------------------------- | ----- |
| `login` | `/login` | M全站唯一登录页 | ✅ | B1 |
| `(app)/dashboard` | `/shell`(教师仪表盘) | R仪表盘改接 `teacherDashboard` 真实查询) | ✅ | B1 |
| `(app)/exams` | `/shell/teacher/exams` | M 列表页 | 🟡 | B2 |
| `(app)/exams/new` | `/shell/teacher/exams/new` | M 表单页 | ❌ mutation | B2 |
| `(app)/exams/[id]` | `/shell/teacher/exams/[id]` | M 详情页 | ✅ `exam(id)` | B2 |
| `(app)/exams/[id]/build` | `/shell/teacher/exams/[id]/build` | M 工作台页 | ❌ | B2 |
| `(app)/exams/[id]/edit-rich` | `/shell/teacher/exams/[id]/edit` | M 工作台页 | ❌ | B2 |
| `(app)/exams/[id]/analytics` | `/shell/teacher/exams/[id]/analytics` | M 详情页(图表) | 🟡 `assignmentAnalysis` | B2 |
| `(app)/exams/[id]/proctoring` | `/shell/teacher/exams/[id]/proctoring` | M二期WS | ❌ | B2 末 |
| `(app)/homework``/new``/[id]``/submissions`(共 7 页) | `/shell/teacher/homework/*`(同构 7 页) | M | 🟡/❌ | B2 |
| `(app)/grades``/entry``/analytics``/stats``/report-card`5 页) | `/shell/teacher/grades/*`5 页) | M | 🟡/❌ | B2 |
| `(app)/lesson-plans``/new``/library``/calendar``/heatmap``/[planId]/edit`6 页) | `/shell/teacher/lesson-plans/*`6 页) | Medit 为工作台页) | ❌ | B2 |
| `(app)/questions` | `/shell/teacher/questions` | M 列表页 | 🟡 `question(id)` | B2 |
| `(app)/textbooks``/[id]`2 页) | `/shell/teacher/textbooks/*`2 页) | M | 🟡 `textbook(id)` | B2 |
| `(app)/attendance``/sheet``/report``/stats`4 页) | `/shell/teacher/attendance/*`4 页) | M | ❌ | B2 |
| `(app)/classes``/[id]``/schedule`3 页) | `/shell/teacher/classes/*`3 页) | M | 🟡 `classInfo(id)` | B2 |
| `(app)/students` | `/shell/teacher/students` | M | ❌ | B2 |
| `(app)/course-plans``/[id]`2 页) | `/shell/teacher/course-plans/*`2 页) | M | ❌ | B2 |
| `(app)/elective``/create``/[id]/edit`3 页) | `/shell/teacher/elective/*`3 页) | M | ❌ | B2 |
| `(app)/error-book` | `/shell/teacher/error-book` | M | ✅ `errorBookItems/Stats` | B2 |
| `(app)/diagnostic``/class/[classId]`2 页) | `/shell/teacher/diagnostic/*`2 页) | M | ✅ `diagnosticReports` | B2 |
| `(app)/analytics``/[studentId]`2 页) | `/shell/teacher/analytics/*`2 页) | M | 🟡 data-ana 多查询 | B2 |
| `(app)/ai-assist``/ai-lesson-plan``/ai-report`3 页) | `/shell/teacher/ai/*`3 页) | M | ❌ai 子图仅 `lessonPlanStatus` | B2 末 |
| `(app)/knowledge-graph` | `/shell/teacher/knowledge-graph` | M | 🟡 `knowledgePoint(id)` | B2 末 |
| `(app)/practice` | `/shell/teacher/practice` | M | ❌ | B2 |
| `(app)/schedule-changes` | `/shell/teacher/schedule-changes` | M | ❌ | B2 |
| `(app)/leave` | `/shell/teacher/leave` | M | ❌ | B2 |
| `(app)/notifications` | `/shell/notifications`(共享) | M | ✅ `notifications(userId)` | B1 |
| `(app)/settings` | `/shell/settings`(共享) | M | ✅ | B1 |
### 9.2 学生域student-portal 36 页 → /shell/student/*B3
| 源路由 | 目标路由 | 动作 | 契约 | 批次 |
| ----------------------------------------------- | ------------------------------------------- | ---------------------- | ------------------------------------------------------- | ----- |
| `dashboard``/trend``/weakness` | `/shell`(学生仪表盘)+ 2 详情页 | R + M | ✅ `studentDashboard`/`learningTrend`/`studentWeakness` | B3 |
| `my-grades``/report-card` | `/shell/student/grades/*`2 页) | M | ❌(列表) | B3 |
| `my-exams``/[id]/result``/[id]/take` | `/shell/student/exams/*`3 页) | Mtake 为作答工作台) | 🟡/❌ | B3 |
| `my-homework``/[id]/submit``/[id]/analysis` | `/shell/student/homework/*`3 页) | M | 🟡/❌ | B3 |
| `schedule` | `/shell/student/schedule` | M | ❌ | B3 |
| `my-attendance` | `/shell/student/attendance` | M | ❌ | B3 |
| `my-classes` | `/shell/student/classes` | M | ❌ `myClasses` | B3 |
| `courses``/[id]` | `/shell/student/courses/*`2 页) | M | ❌ | B3 |
| `course-plans``/[id]` | `/shell/student/course-plans/*`2 页) | M | ❌ | B3 |
| `lesson-plans``/[id]/view` | `/shell/student/lesson-plans/*`2 页) | M | ❌ | B3 |
| `textbooks``/[id]/chapters` | `/shell/student/textbooks/*`2 页) | M | 🟡 | B3 |
| `error-book` | `/shell/student/error-book` | M | ✅ | B3 |
| `learning``learning-path` | `/shell/student/learning``/learning-path` | M | ❌ | B3 |
| `practice``/[sessionId]` | `/shell/student/practice/*`2 页) | M | ❌ | B3 |
| `elective``/[id]` | `/shell/student/elective/*`2 页) | M | ❌ | B3 |
| `ai-tutor` | `/shell/student/ai-tutor` | M | ❌ | B3 末 |
| `announcements``/[id]` | `/shell/announcements/*`(共享 2 页) | M | ❌(列表) | B3 |
| `messages` | `/shell/messages` | M | ❌ | B3 末 |
| `notifications``settings` | 共享路由 | M | ✅ | B1/B3 |
| `leave` | `/shell/student/leave` | M | ❌ | B3 |
### 9.3 家长域parent-portal 24 页 → /shell/parent/*B4
| 源路由 | 目标路由 | 动作 | 契约 | 批次 |
| --------------------------------------------------- | -------------------------------------- | ----- | -------------------- | ---- |
| `parent/dashboard``/trend``/weakness` | `/shell`(家长仪表盘)+ 2 页 | R + M | ✅ `parentDashboard` | B4 |
| `parent/children/[studentId]` | `/shell/parent/children/[studentId]` | M | ❌ `myChildren` | B4 |
| `parent/grades``/report-card` | `/shell/parent/grades/*`2 页) | M | ❌ | B4 |
| `parent/exams``/[id]/result` | `/shell/parent/exams/*`2 页) | M | 🟡/❌ | B4 |
| `parent/homework` | `/shell/parent/homework` | M | ❌ | B4 |
| `parent/attendance` | `/shell/parent/attendance` | M | ❌ | B4 |
| `parent/classes` | `/shell/parent/classes` | M | ❌ | B4 |
| `parent/course-plans``/[id]` | `/shell/parent/course-plans/*`2 页) | M | ❌ | B4 |
| `parent/lesson-plans``/[planId]/view` | `/shell/parent/lesson-plans/*`2 页) | M | ❌ | B4 |
| `parent/error-book` | `/shell/parent/error-book` | M | ✅ | B4 |
| `parent/diagnostic` | `/shell/parent/diagnostic` | M | ✅ | B4 |
| `parent/learning-path` | `/shell/parent/learning-path` | M | ❌ | B4 |
| `parent/practice` | `/shell/parent/practice` | M | ❌ | B4 |
| `parent/elective` | `/shell/parent/elective` | M | ❌ | B4 |
| `parent/leave` | `/shell/parent/leave` | M | ❌ | B4 |
| `parent/notifications``/settings``/preferences` | 共享 + `/shell/parent/preferences` | M | ✅/❌ | B4 |
### 9.4 管理域admin-portal 24 页 → /shell/admin/*B5
| 源路由 | 目标路由 | 动作 | 契约 | 批次 |
| ---------------------------------- | -------------------------------------------- | --------------------------------- | ------------------- | ---- |
| `admin/dashboard` | `/shell`(管理仪表盘) | R | ✅ `adminDashboard` | B5 |
| `admin/users` | `/shell/admin/users` | R现有 widget 升级整页) | ❌ `users` 列表 | B5 |
| `admin/roles``admin/permissions` | `/shell/admin/roles``/permissions`2 页) | R/M | ❌ | B5 |
| `admin/audit-logs` + 3 子页 | `/shell/admin/audit-logs/*`4 页) | M | ❌ | B5 |
| `admin/invitation-codes` | `/shell/admin/invitation-codes` | R | ❌ | B5 |
| `admin/school` + 5 子页 | `/shell/admin/school/*`6 页) | M | ❌ | B5 |
| `admin/classes` | `/shell/admin/classes` | M | ❌ | B5 |
| `admin/students``admin/teachers` | `/shell/admin/students``/teachers`2 页) | M | ❌ | B5 |
| `admin/organization` | `/shell/admin/organization` | M | ❌ | B5 |
| `admin/announcements` | `/shell/admin/announcements` | M | ❌ | B5 |
| `admin/files` | `/shell/admin/files` | M | ❌ | B5 |
| `admin/ai-settings` | `/shell/admin/ai-settings` | M | ❌ | B5 |
| `admin/system` | `/shell/admin/system` | M | ❌ | B5 |
| `admin/viewports` | `/shell/admin/viewports` | M对齐 004 §5.4 视口配置) | ❌ | B5 |
| —(新)插件管理 | `/shell/admin/plugins` | R现有 plugin-manager 升级整页) | ✅ config-service | B5 |
### 9.5 计数与节奏
| 域 | 页面数 | 契约已就绪(✅/🟡) | 批次 |
| -------------------------------------------------------------------------------------------------- | -------- | ------------------- | ------------- |
| 共享login/notifications/settings/announcements/messages/forbidden | ~7 | 4 | B1/B3 |
| 教师 | ~50 | ~14 | B1+B2 |
| 学生 | ~34 | ~8 | B3 |
| 家长 | ~21 | ~5 | B4 |
| 管理 | ~22 | ~3 | B5 |
| 缺口新增management/grade 5 页 + register/onboarding/privacy/terms ~5 页CICD 有而旧 portal 无) | ~10 | 0 | B2/B5 末N |
| **合计** | **~144** | **~34** | — |
**节奏原则**:契约就绪页先行;❌ 页用 MSW 先上 UI契约工单跟踪后端补齐后切换真实查询切换 = 改 hook 的 fetcher 指向,页面不动)。
---
## 10. 实施路线图P0P6
> 每阶段列出:目标 / 范围 / 验收标准(可复现命令 + 运行态证据)。阶段内任务按 §11 规范拆给多 AI 并行。**未完成上一阶段退出标准不得进入下一阶段。**
### P0 · 地基止血1 周)— ✅ 已完成2026-07-22 验收)
**目标**:消除"空壳即不可用 + 认证裸奔"两大致命伤。
| # | 任务 | 验收 | 状态 |
| ---- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| P0-1 | `/login` 页 + `/api/auth/login | logout`+`edu_session` httpOnly cookie | 本地起 iam/gateway错误密码报错、正确密码进 /shellcookie 在 DevTools Application 可见且 httpOnly | ✅ |
| P0-2 | `src/middleware.ts`cookie 校验 + 身份头注入 + `checkRoutePermission` 接线 + `/shell/forbidden` | 无 cookie 访问 /shell/** → 302 /loginstudent 访问 /shell/admin/users → 302 /shell/forbidden截图 | ✅ |
| P0-3 | `/api/graphql` 代理 + Apollo Client 指向同域 + 删除 localStorage token 方案 | DevTools Network 面板无 `localhost:3000` 直连;`grep -r "localStorage" src/providers` 无 token 读取 | ✅ |
| P0-4 | 配置兜底改进:`getDefaultConfig()` 返回**内置默认仪表盘插件集**按角色静态定义config-service 不可用时仪表盘仍有内容 | 停掉 config-service 与 router启动前端/shell 显示默认卡片而非"暂无可见插件"(截图) | ✅ |
| P0-5 | DEV_MODE 生产守卫:`instrumentation.ts` 启动校验 + `docker-compose.yml` 生产 profile 改 `NEXT_PUBLIC_DEV_MODE: "false"` | `NODE_ENV=production NEXT_PUBLIC_DEV_MODE=true pnpm start` 启动即报错退出 | ✅ |
| P0-6 | 权限位图修正:`GRADE_READ` 去重(保留索引位,二见位改 `_RESERVED_`+ 单测更新 | `vitest run permission-bitmap` 通过;`validateRoutePermissionConfigs()` 返回空 | ✅ |
| P0-7 | ESLint 补齐:`no-restricted-imports`(禁 widget 跨目录 import、禁直 import sonner、禁页面绕过 lib/api 直 import @apollo/client | `pnpm lint` 对预埋反例报错(附输出) | ✅ |
| P0-8 | 仓库卫生:`.env.local` 移出版本控制 + `.gitignore``tsconfig.tsbuildinfo` | `git status` 干净 | ✅ |
**P0 验收证据2026-07-22**
- **P0-1**`POST http://localhost:8080/api/v1/iam/login`(错误密码)→ 401`POST http://localhost:4010/api/auth/login`(错误密码)→ 401 `{"error":"INVALID_CREDENTIALS","message":"邮箱或密码错误"}`。实现:[src/app/api/auth/login/route.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/app/api/auth/login/route.ts) + [src/app/api/auth/logout/route.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/app/api/auth/logout/route.ts) + [src/app/login/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/login/page.tsx)
- **P0-2**[src/middleware.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/middleware.ts) 实现 cookie 校验 + 身份头注入 + `checkRoutePermission`[src/app/shell/forbidden/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/forbidden/page.tsx) 提供 403 页面
- **P0-3**`grep -r "localStorage" src/providers` 仅匹配 [ApolloProvider.tsx:12](file:///e:/Desktop/Edu/apps/portal-shell/src/providers/ApolloProvider.tsx#L12) 的删除说明注释,无 token 读取;[src/app/api/graphql/route.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/app/api/graphql/route.ts) 提供同域代理
- **P0-4**[src/lib/config-fetcher.ts:214](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/config-fetcher.ts#L214) `getDefaultConfig(role)` 返回内置默认仪表盘插件集classic 布局 + top/side/main slots
- **P0-5**[src/instrumentation.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/instrumentation.ts) `register()``NODE_ENV=production``NEXT_PUBLIC_DEV_MODE!=false``process.exit(1)`
- **P0-6**`vitest run permission-bitmap` → 27/27 通过;[packages/shared-ts/src/permission-bitmap.ts](file:///e:/Desktop/Edu/packages/shared-ts/src/permission-bitmap.ts) `GRADE_READ` 去重,`_RESERVED_31` 占位
- **P0-7**`eslint .` → 0 errors, 2 warnings均在 `__generated__/types.ts` 生成文件);[eslint.config.js](file:///e:/Desktop/Edu/apps/portal-shell/eslint.config.js) 含 `no-restricted-imports`
- **P0-8**`git status` 干净commit `cfb7b00``.env.local``tsconfig.tsbuildinfo` 已在 [.gitignore](file:///e:/Desktop/Edu/apps/portal-shell/.gitignore)
### P1 · 框架与数据源接通12 周)— 进行中P1-1/P1-2/P1-3/P1-4/P1-5/P1-6 ✅ 2026-07-22 验收)
**目标**AppFrame + 导航 + 真实数据仪表盘 + 页面模板 + MSW + i18n页面迁移的"流水线"建成。
| # | 任务 | 验收 | 状态 |
| ---- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| P1-1 | `src/app/shell/layout.tsx` AppFrameTopBar 静态挂载 + Sidebar 导航菜单 `navigation.ts` + 权限过滤) | 4 角色各见各菜单;导航项与 route-permissions 表一致(单测) | ✅ |
| P1-2 | 仪表盘改接真实查询:`teacherDashboard`/`studentDashboard`/`parentDashboard`/`adminDashboard` + `warnings`/`errorBookStats` | 起全栈4 角色仪表盘显示真实聚合数据截图widget 假契约查询grades/homeworks/schedule/attendance/exams/announcements全部下线或改造 | ✅ |
| P1-3 | 页面模板四件套list/detail/form/workbench+ 三态规范 | Storybook 或示例页 4 张(/shell/dev/templates/*,仅 dev 可见Next.js 私有文件夹 `_xxx` 不参与路由,故使用 `dev` 而非 `_dev` | ✅ |
| P1-4 | next-intl 接入 + messages 合并迁移 | 切换 locale 页面文案切换(截图);`useT` 自造函数删除 | ✅ |
| P1-5 | MSW 兜底层(迁移旧 handlers覆盖 dashboard/users/exams/grades 四域起步) | `NEXT_PUBLIC_MSW=1` 无后端启动,仪表盘 + users 页有数据(截图);生产构建 bundle 无 mocks | ✅ |
| P1-6 | 31 widget 令牌清债271 处机械替换)+ `border border` 去重 + 未知类检测进 arch:scan | `grep -c "text-heading-\|mt-sm\|py-xs\|p-md" src/widgets` = 0`pnpm lint:tokens` 通过 | ✅ |
| P1-7 | codegen 恢复 typescript-operationsconfig + data-ana 两域先行关闭 skipDocumentsValidation | 生成操作级类型lib/api 对应域删除手写 interfacetypecheck 通过 | ✅ |
| P1-8 | CI 增补:路由表一致性脚本 + 页面计数 + codegen diff 检查 | CI 对预埋违规报红(附 pipeline 链接) | ⏳ |
**P1-1 验收证据2026-07-22**
- **单测**`vitest run --reporter=verbose navigation` → 7/7 passed
- 每个导航项 href 在 EXACT/PREFIX/DASHBOARD 表登记
- teacher/student/parent/admin 各仅见本组导航项
- 4 角色导航项两两不交叉(角色隔离)
- admin 无权限位图时 `checkRoutePermission` 拒绝
- **实现文件**
- [src/shared/lib/navigation.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/lib/navigation.ts)27 项扁平数组,按 4 角色分区,`group` 字段用于角色过滤
- [src/app/shell/layout.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/layout.tsx)RSC AppFrame`headers()` 读取身份头,`batchCheckRoutePermission` 按位图二次过滤导航项
- [src/shared/components/layout/user-menu.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/components/layout/user-menu.tsx):顶部用户菜单,显示 userId + role登出 POST `/api/auth/logout`
- [src/shared/lib/route-permissions.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/lib/route-permissions.ts):补全 7 个列表页根路由的 EXACT 登记(`/shell/admin/announcements``/shell/admin/classes``/shell/teacher/exams` 等),与 PREFIX 表互补
- **质量校验**`tsc --noEmit` 通过;`eslint`5 文件)通过
**P1-2 验收证据2026-07-22**
- **假契约查询全部下线**`grep -rn "useGrades\|useHomework\|useSchedule\|useAttendance\|useExams\|useAnnouncements" src` 无业务调用6 个 widgetgrades/homework/schedule/attendance/exams/announcements改为"数据源已迁移"占位 Card
- **6 个真实聚合查询接入**
- [src/lib/api/operations/dashboard.graphql.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/api/operations/dashboard.graphql.ts)`GET_TEACHER_DASHBOARD_DOC` / `GET_STUDENT_DASHBOARD_DOC` / `GET_PARENT_DASHBOARD_DOC` / `GET_ADMIN_DASHBOARD_DOC` / `GET_WARNINGS_DOC` / `GET_ERROR_BOOK_STATS_DOC`,字段全部 snake_case 对齐 data-ana 子图
- [src/lib/api/dashboard.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/api/dashboard.ts)6 个 hooks`useTeacherDashboard` / `useStudentDashboard` / `useParentDashboard` / `useAdminDashboard` / `useWarnings` / `useErrorBookStats`+ 全部领域模型类型(`TeacherDashboard` / `StudentDashboard` / `ParentDashboard` / `AdminDashboard` / `WarningInfo` / `ErrorBookStats` 等)
- **4 角色仪表盘页面**(显式路由,先于 catch-all 匹配):
- [src/app/shell/teacher/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/teacher/page.tsx)StatCard×4班级总数/学生总数/班级平均分/待批作业)+ DashboardSection×2班级概况/近期预警)
- [src/app/shell/student/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/student/page.tsx)StatCard×3平均分/班级排名/待交作业)+ DashboardSection×2薄弱知识点/近期成绩趋势)
- [src/app/shell/parent/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/parent/page.tsx)StatCard×2孩子平均分/班级排名)+ DashboardSection×2薄弱知识点/预警通知)
- [src/app/shell/admin/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/admin/page.tsx)StatCard×4教师总数/学生总数/班级总数/全校平均分)+ DashboardSection×2近期预警/AI 用量)
- **混合路由模型V3-A1**[src/app/shell/\[\[...route\]\]/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/[[...route]]/page.tsx) 新增 `params` 异步读取 + `/shell` 空路由 `redirect(\`/shell/${role}\`)` 兜底
- **三态规范**4 个仪表盘页统一遵循 loadingStatCard isLoading 骨架)→ errorCard 错误提示)→ success真实数据三态
- **质量校验**`tsc --noEmit` 通过;`eslint`(新/改文件)通过;`vitest run` 全量 20 test files / 212 tests 全部通过
**P1-3 验收证据2026-07-22**
- **4 个页面模板组件**[src/shared/components/page-templates/](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/components/page-templates/)
- [list-page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/components/page-templates/list-page.tsx)`ListPageShell` + `ListPageSkeleton`,结构 = PageHeader + FilterBar + 主内容 + Pagination三态 = loading/empty/errorNode 优先级链
- [detail-page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/components/page-templates/detail-page.tsx)`DetailPageShell` + `DetailSection` + `DetailField` + `DetailPageSkeleton`,结构 = PageHeader含 backHref + 分区 + 字段表
- [form-page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/components/page-templates/form-page.tsx)`FormPageShell` + `FormPageSkeleton`,结构 = PageHeader + form + errorSummary + 提交/取消按钮
- [workbench-page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/components/page-templates/workbench-page.tsx)`WorkbenchPageShell` + `WorkbenchPanel` + `WorkbenchPageSkeleton`,结构 = PageHeader + 三栏(左树/中画布/右属性),左右栏宽度可配置
- [index.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/shared/components/page-templates/index.ts)barrel 导出
- **4 个 dev 示例页**(仅 dev 可见,生产环境 `notFound()` 兜底):
- [/shell/dev/templates](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/dev/templates/page.tsx):索引页,列出 4 张模板
- [/shell/dev/templates/list](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/dev/templates/list/page.tsx):列表页示例,支持 `?state=loading|empty|success` 切换三态
- [/shell/dev/templates/detail](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/dev/templates/detail/page.tsx):详情页示例
- [/shell/dev/templates/form](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/dev/templates/form/page.tsx):表单页示例
- [/shell/dev/templates/workbench](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/dev/templates/workbench/page.tsx):工作台页示例
- **路径命名修正**:原 ARCHITECTURE.md 写 `/shell/_dev/templates/*`,但 Next.js 将下划线开头的文件夹视为"私有文件夹"(不参与路由),实测 `/shell/_dev/templates``[[...route]]/page.tsx` catch-all 兜底接管ClientShell 微内核渲染)。改用 `dev` 命名后显式路由优先匹配catch-all 不再触发。route-permissions.ts 同步登记 `PREFIX /shell/dev/`(空 config = 仅校验登录身份)
- **三态规范验证**HTTP 200 + 内容断言):
- `GET /shell/dev/templates/list?state=loading` → 200HTML 含 `animate-pulse` 骨架,无表格数据
- `GET /shell/dev/templates/list?state=empty` → 200HTML 含"暂无数据"空态,无表格数据
- `GET /shell/dev/templates/list`(默认 success→ 200HTML 含表格行"考试 A"
- **单测**`vitest run --reporter=verbose page-templates` → 19/19 passed覆盖 4 个模板的三态、字段渲染、提交按钮禁用、errorSummary alert 等)
- **质量校验**`tsc --noEmit` 通过;`eslint src/shared/components/page-templates src/app/shell/dev src/shared/lib/route-permissions.ts` 通过;`vitest run` 全量 21 test files / 231 tests 全部通过212 原有 + 19 新增)
**P1-4 验收证据2026-07-22**
- **next-intl v4.13.2 接入(无 i18n 路由模式)**
- [next.config.js](file:///e:/Desktop/Edu/apps/portal-shell/next.config.js)`createNextIntlPlugin("./src/i18n/request.ts")` 包装 `nextConfig`Turbopack resolveAlias 注入 `next-intl/config`
- [src/i18n/request.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/i18n/request.ts)`getRequestConfig``NEXT_LOCALE` cookie 读取 locale缺省 `zh-CN`),动态 import 对应 messages JSON
- [src/app/layout.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/layout.tsx)`async RootLayout` + `getLocale()` / `getMessages()` + `<NextIntlClientProvider>` 注入全局 messages`<html lang={locale}>` 随 cookie 切换
- **messages 合并迁移**
- [src/messages/zh-CN.json](file:///e:/Desktop/Edu/apps/portal-shell/src/messages/zh-CN.json):从 teacher-portal 合并 + 新增 `shell.dev.templates` 命名空间title/description/list/detail/form/workbench 及其 listExample 等子键)
- [src/messages/en.json](file:///e:/Desktop/Edu/apps/portal-shell/src/messages/en.json):与 zh-CN.json 结构完全对称
- **自造 i18n 删除**
- [src/providers/ThemeI18nProvider.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/providers/ThemeI18nProvider.tsx) 已删除(含 `useT()` 函数 + `DEFAULT_MESSAGES` 常量)
- [src/providers/ThemeProvider.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/providers/ThemeProvider.tsx):新建,仅保留主题切换(`usePluginStore(s => s.theme)` + `document.documentElement.classList` 切换 `.dark`
- [src/shell/PluginStore.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/shell/PluginStore.ts):移除 `locale` / `setLocale` 字段
- `grep -rn "useT(\|ThemeI18nProvider\|DEFAULT_MESSAGES" src` → 无匹配(自造函数全删除)
- **locale-switcher 改造**[src/widgets/topbar/locale-switcher/index.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/widgets/topbar/locale-switcher/index.tsx) 使用 `useLocale()` + `useTranslations()` + cookie 写入 + `router.refresh()` 触发 RSC 重新渲染
- **dev/templates 页面演示**[src/app/shell/dev/templates/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/dev/templates/page.tsx) 使用 `getTranslations("shell.dev.templates")`Server Component
- **locale 切换 HTTP 验证**dev server `next dev --turbopack -p 4010`
- `GET /login`(无 cookie`<html lang="zh-CN">` + `NextIntlClientProvider locale="zh-CN"` + 中文文案("保存"/"取消"/"切换侧栏"
- `GET /login``Cookie: NEXT_LOCALE=en`)→ `<html lang="en">` + `NextIntlClientProvider locale="en"` + 英文文案("Save"/"Cancel"/"Toggle sidebar"
- **质量校验**`tsc --noEmit` 通过;`eslint src` → 0 errors, 2 warnings`__generated__/types.ts` 生成文件);`vitest run` 全量 21 test files / 231 tests 全部通过
**P1-5 验收证据2026-07-22**
- **MSW v2.7.0 兜底层(覆盖 dashboard/users/exams/grades 四域起步)**
- [src/mocks/index.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/index.ts)`initMocks()` 入口,`NEXT_PUBLIC_MSW=1` 时按环境动态启动 browser worker 或 node server
- [src/mocks/handlers.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/handlers.ts):拦截 `POST /api/graphql``POST {APOLLO_ROUTER_URL}`,按 `operationName` 路由
- [src/mocks/graphql-data.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/graphql-data.ts)mock 数据与 `graphqlResponse()` 函数,供 handlers.tsMSW browser和 route.tsSSR共用
- [src/mocks/browser.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/browser.ts) / [server.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/server.ts)`setupWorker` / `setupServer`
- [public/mockServiceWorker.js](file:///e:/Desktop/Edu/apps/portal-shell/public/mockServiceWorker.js)MSW Service Worker`pnpm exec msw init public/ --save`
- **SSR 端 mock 数据Apollo Client 走同域代理)**
- [src/lib/apollo-client.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/apollo-client.ts)`MSW_ENABLED` 时 SSR 端 HttpLink 也指向 `/api/graphql`(不走直连 router
- [src/app/api/graphql/route.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/app/api/graphql/route.ts)`MSW_ENABLED` 时直接返回 mock 数据,不连接后端
- **生产构建安全bundle 无 mocks**
- [src/mocks/empty.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/mocks/empty.ts):空 stub导出与 `index.ts`/`graphql-data.ts` 同签名的 no-op 函数
- [next.config.js](file:///e:/Desktop/Edu/apps/portal-shell/next.config.js)`NEXT_PUBLIC_MSW!=1` 时 Turbopack `resolveAlias` + webpack `resolve.alias``@/mocks``@/mocks/graphql-data` 重定向到 `@/mocks/empty`Turbopack 不支持 Windows 绝对路径,故使用 `@/` 说明符)
- [src/providers/MswProvider.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/providers/MswProvider.tsx):静态 `import { initMocks } from "@/mocks"`alias 生效后指向 empty.ts`MSW_ENABLED=false` 时 useEffect 分支被 dead-code 消除
- **构建验证**`NEXT_PUBLIC_MSW=0 next build` 成功20 路由生成);`Select-String -Path ".next/static/chunks/**/*.js",".next/server/**/*.js" -Pattern "张老师","stu-001","setupWorker","dev-teacher-001","二次函数"`**CLEAN客户端 + 服务端 bundle 均无 mock 字符串**
- **dev server 验证MSW=1 无后端)**`GET /api/graphql` 返回 `{msw:true}``POST /api/graphql` 返回 mock 数据teacherDashboard: total_classes=5/total_students=142users: 5 条记录questions: 3 条grades: 3 条)
- **质量校验**`tsc --noEmit` 通过;`eslint src` → 0 errors, 2 warnings`__generated__/types.ts``vitest run` 全量 21 test files / 231 tests 全部通过
**P1-6 验收证据2026-07-22**
- **31 widget 令牌清债516 处机械替换,覆盖 25 文件)**
- 间距工具类 `xs/sm/md/lg/xl` → 数字 `1/2/3/4/6``mt-sm``mt-2``px-sm``px-2``py-xs``py-1``p-md``p-3``gap-sm``gap-2``space-y-xs``space-y-1``h-xs``h-1` 等)
- `text-heading-3``text-lg font-semibold`25 处)
- `bg-danger``bg-destructive`6 处shadcn 标准)
- `border border``border`(去重,仅匹配真正的重复 border 类)
- **lint:tokens 修复**[.eslintrc.tokens.js](file:///e:/Desktop/Edu/apps/portal-shell/.eslintrc.tokens.js) 原导入 `@typescript-eslint/parser`(未安装)导致脚本不可用,改用 `typescript-eslint` 包的 `tseslint.parser`(与 eslint.config.js 一致)
- **验收命令**
- `grep -rn "text-heading-\|mt-sm\|py-xs\|p-md" src/widgets`**0 匹配**
- `eslint -c .eslintrc.tokens.js src`**0 errors**
- **质量校验**`tsc --noEmit` 通过;`eslint src` → 0 errors, 2 warnings`vitest run` 全量 21 test files / 231 tests 全部通过
**P1-7 验收证据2026-07-22**
- **codegen 配置([codegen.yml](file:///e:/Desktop/Edu/apps/portal-shell/codegen.yml)**
- 新增第 3 个输出 `src/lib/api/__generated__/dashboard-types.ts`,作用域 `documents: src/lib/api/operations/dashboard.graphql.ts`,插件 `typescript + typescript-operations``skipDocumentsValidation: false`(关闭校验)
- 全局 `skipDocumentsValidation: true` 保留(其余 7 域 operations 仍引用 schema 未实现字段44 个 "Cannot query field" 错误仍需 skip
- config 域关闭推迟admin.graphql.ts 中 `GET_LAYOUT_TEMPLATES_DOC` 查询 `availableSlots` 字段,但 schema `LayoutTemplateGql` 未定义该字段,需后端补齐后才能关闭
- **data-ana 域 6 个 operation 严格匹配 schema**`GetTeacherDashboard` / `GetStudentDashboard` / `GetParentDashboard` / `GetAdminDashboard` / `GetWarnings` / `GetErrorBookStats`,全部通过 codegen 严格校验,生成 per-operation Query/Variables 类型
- **lib/api/dashboard.ts 手写 interface 全部删除**
- 删除 14 个手写 interface`TeacherDashboard` / `StudentDashboard` / `ParentDashboard` / `AdminDashboard` / `WarningList` / `WarningInfo` / `ClassSummary` / `StudentSummary` / `WeakPoint` / `TrendPoint` / `AIUsageSummary` / `AIUsageByProvider` / `KnowledgePointErrorStats` / `ErrorBookStats`
- 删除 6 个内部 Query 类型别名:`TeacherDashboardQueryData` / `StudentDashboardQueryData` / `ParentDashboardQueryData` / `AdminDashboardQueryData` / `WarningsQueryData` / `ErrorBookStatsQueryData`
- 改用 `NonNullable<GetXxxQuery['xxx']>` 从生成类型派生 14 个 type alias保持对外 API 形状不变widgets 无需改动)
- 6 个 hook 改用生成的 `GetXxxQuery` 作为 `useWidgetQuery<TData, TVars>` 的泛型参数
- **page.tsx 空值守卫补全**4 个仪表盘页teacher/student/parent/admin的 StatCard `value` prop 改用 `?? "--"` / `?? 0` 兜底,对齐 schema 字段可空语义
- **验收命令**
- `tsx scripts/normalize-schema.ts``Combined schema written`
- `graphql-codegen --config codegen.yml` → 3 outputs 全部 SUCCESS ✅
- `tsc --noEmit` → 0 errors ✅
- `eslint src` → 0 errors, 4 warningspre-existing `any` 警告,非本次引入) ✅
- `vitest run` → 21 test files / 231 tests 全部 passed ✅
- `next build``✓ Compiled successfully in 4.9s`
- **实现文件**
- [codegen.yml](file:///e:/Desktop/Edu/apps/portal-shell/codegen.yml)3 个 generates 输出dashboard-types.ts 为新增
- [src/lib/api/**generated**/dashboard-types.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/api/__generated__/dashboard-types.ts)codegen 生成6 个 Query 类型 + 6 个 Variables 类型 + Exact 工具类型 + schema 类型子集)
- [src/lib/api/dashboard.ts](file:///e:/Desktop/Edu/apps/portal-shell/src/lib/api/dashboard.ts):手写 interface 全部删除,改用生成类型派生
- [src/app/shell/teacher/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/teacher/page.tsx) / [student/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/student/page.tsx) / [parent/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/parent/page.tsx) / [admin/page.tsx](file:///e:/Desktop/Edu/apps/portal-shell/src/app/shell/admin/page.tsx)StatCard value 空值守卫补全
### P2 · 教师域页面23 周,可与 P3 部分并行)
- 范围§9.1 全表(~50 页。顺序建议exams → homework → grades → lesson-plans → questions/textbooks → attendance/classes/students → diagnostic/error-book/analytics → elective/course-plans → ai-* → practice/schedule-changes/leave。
- 每模块退出:列表/详情/表单页齐 + 三态 + MSW 场景 + 单测(数据变换函数)+ 页面计数更新。
- 契约工单exams CRUD mutation、homework CRUD、grades 录入/列表、lessonPlans CRUD、attendance、classes 列表等,随模块开工即登记。
### P3 · 学生域页面1.52 周)
- 范围§9.2 全表(~34 页。先行dashboard 详情trend/weakness 已 ✅)→ error-book→ grades/exams/homework 只读 → exams/take 作答台 → practice → ai-tutor。
### P4 · 家长域页面11.5 周)
- 范围§9.3 全表(~21 页)。多为只读视图,复用学生域组件;重点:`myChildren` 契约 + child-overview 聚合。
### P5 · 管理域页面1.52 周)
- 范围§9.4 全表(~22 页。先行users❌ 列表契约优先推)→ roles/permissions → audit-logs → school 体系 → invitation-codes → plugins→ viewports对齐 004 §5.4)。
- 收尾:旧 4 portal 目录归档(`apps/_archive/`)或删除(决策点:待全量验收后执行,单独 PR
### P6 · 硬化与验收12 周)
- Playwright E2E登录 → 各角色核心流 1 条(教师建考试/学生交作业/家长看成绩/管理员建用户)+ 三级错误边界 + 门禁越权。
- 视觉回归5 布局 + 每域代表页截图基线。
- 性能:首屏 JS ≤ 300KB(gzip)、LCP < 2s本地 Docker 压测,附报告)。
- 生产检查单:`APOLLO_REQUIRE_PQ_MANIFEST=true``APOLLO_ROUTER_INTROSPECTION=false``NEXT_PUBLIC_DEV_MODE=false``ROUTER_AUTH_SECRET` env 化、CSP 头(`next.config.js headers()`)、`/api/log` 换生产端点。
- 文档README v3.0 重写(本文件内容沉淀回模块 README + 本文件标记"已并入 README")。
---
## 11. 后续 AI 工作规范(强制)
> 本章是给**所有后续在本模块工作的 AI/人**的操作规程。多 AI 协作同时遵守 project_rules §14模块单一负责制本模块只改 `apps/portal-shell/`,跨模块变更按 §14.4 顺序)。
### 11.1 开工前必读(按序)
1. 本文件v3.0 总纲)
2. `project_rules.md` §3.4/§3.10/§4/§14
3. 所接任务的页面源(旧 portal 对应 page.tsx与目标模板§7.3
### 11.2 目录与文件约定
```
src/
├─ app/
│ ├─ login/page.tsx # 登录
│ ├─ shell/
│ │ ├─ layout.tsx # AppFrame唯一框架
│ │ ├─ forbidden/page.tsx # 403
│ │ ├─ page.tsx # 角色仪表盘catch-all 保留于仪表盘内部可选)
│ │ ├─ <role>/<module>/page.tsx # 业务页面(显式路由)
│ │ └─ <role>/<module>/{loading,error}.tsx
│ └─ api/{graphql,auth/login,auth/logout,health,ready,log}/route.ts
├─ middleware.ts # 认证 + 门禁(唯一入口)
├─ features/<domain>/ # 页面私有实现(组件/hooks/类型),按域组织
│ └─ exams/{exams-list.tsx, exam-detail.tsx, use-exam-filters.ts}
├─ widgets/ # 仅仪表盘卡片(存量,冻结新增——新功能建页面不建插件)
├─ shell/ # 微内核(存量,冻结)
├─ lib/{api,apollo-client,auth} # 数据层/认证工具
├─ mocks/ # MSWdev/test only
├─ shared/{components,lib} # 共享 UI 与工具
└─ messages/{zh-CN,en}.json # i18n
```
- **页面文件瘦身**`page.tsx` 只做 RSC 预取 + 组装,实现代码放 `features/<domain>/`;单文件 ≤ 300 行(超出即拆分)。
- **features 域清单**exams / homework / grades / lesson-plans / questions / textbooks / attendance / classes / students / course-plans / elective / error-book / diagnostic / analytics / practice / schedule-changes / leave / ai / notifications / settings / users / rbac / audit / school / invitations / plugins / organization / files / system / viewports / children / messages / announcements。新增域先在本文件 §9 登记。
### 11.3 每页硬性清单DoD
- [ ] 显式路由 + `route-permissions.ts` 已登记CI 一致性通过)
- [ ] 套页面模板§7.3),未手排布局
- [ ] `loading.tsx` + `error.tsx` + 空态三态齐全
- [ ] 数据走 `lib/api` hooks无内联 gql无手写与 schema 冲突类型
- [ ] schema 未就绪的查询走 MSW + 文件头 `@contract-pending: <工单>` 注释 + §11.4 登记
- [ ] 文案走 `useTranslations`;无硬编码中文(迁移期 lint warnP2 末转 error
- [ ] 无禁用类/字面量§8.1 铁律);`pnpm lint` + `lint:tokens` 通过
- [ ] 交互错误有 `notify.error`;无空 catch
- [ ] 数据变换/权限判断等纯函数有 vitest 单测
- [ ] i18n 两份 messages 同步更新
- [ ] 提交信息:`feat(portal-shell): <module> <page> migration`Conventional Commits
### 11.4 契约工单流程(前端缺 schema 字段时)
1.`docs/architecture/issues/contracts/<service>_contract.md` 追加需求(字段名/类型/权限点/使用页面)。
2. 本文件 §9 对应行"契约"列标注工单号。
3. 页面用 MSW 先行;后端落地后:重跑 normalize + codegen → 该域关 `skipDocumentsValidation` → 切换 fetcher → 删 mock → 工单关闭。
4. **禁止**为绕过校验把查询写得与 schema 不符后开启 skip。
### 11.5 多 AI 并行分工建议
| AI 角色 | 负责 | 边界 |
| -------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------- |
| 框架 AI | P0/P1middleware/代理/AppFrame/模板/MSW/i18n | 独占 `src/{app/shell/layout.tsx,middleware.ts,shared/,lib/,mocks/}` |
| 教师域 AI | §9.1 | 独占 `src/app/shell/teacher/**` + `src/features/{exams,homework,grades,lesson-plans,...}` |
| 学生域 AI | §9.2 | 独占 `src/app/shell/student/**` + 对应 features |
| 家长域 AI | §9.3 | 独占 `src/app/shell/parent/**` + 对应 features |
| 管理域 AI | §9.4 | 独占 `src/app/shell/admin/**` + 对应 features |
| 公共区login/notifications/settings | 框架 AI | 防止撞车 |
- 跨域共享组件必须先提"共享申请"(改 `src/shared/``packages/ui-components/`),由框架 AI 评审合入;**禁止**各自复制粘贴。
- 分支:每域一个 feature 分支(`feat/portal-shell-<domain>`),按 project_rules §14.3。
### 11.6 文档与验收纪律
- 每个 PR 更新:本文件 §9 对应行状态 +如涉及README 对应章节。
- 验收勾选必须附**可复现命令输出**typecheck/lint/test/build/截图),禁止"已完成"裸声明——本文件 §1.1 的对照表即为反面教材。
- 每阶段结束运行 `pnpm run arch:scan` 更新 arch.db并同步 004 相关章节。
### 11.7 禁止事项(红线)
1. 禁止新增 widget 插件实现业务功能(冻结 `src/widgets/` 新增;新功能 = 新页面)。
2. 禁止恢复 localStorage 存 token禁止 JS 读取 `edu_session`
3. 禁止在页面/widget 内联 gql 或绕过 `lib/api` 直接 `useQuery`
4. 禁止引入第二套设计令牌 / 第二套组件库 / 第二套 toast。
5. 禁止"默认放行"式的权限代码(未匹配路由必须拒绝)。
6. 禁止跳过 `@contract-pending` 登记直接写假查询。
7. 禁止删除/弱化现有测试来让门禁变绿。
---
## 12. 风险登记册
| # | 风险 | 等级 | 缓解 |
| --- | ---------------------------------------------- | ---- | ----------------------------------------------------------------------------------- |
| R1 | 后端契约补齐速度成为页面切换瓶颈(~100 页 ❌) | 高 | MSW 先行解耦;契约工单周同步;优先推 mutations 框架(后端 0 mutation 是系统性缺口) |
| R2 | 140 页迁移量大AI 并行产出不一致 | 高 | 模板四件套强制 + 共享组件申请制 + 每模块 DoD 清单 + CI 一致性脚本 |
| R3 | 旧页面纸感令牌清理遗漏导致视觉混杂 | 中 | 迁移 DoD 含令牌检查arch:scan 未知类检测P6 视觉回归 |
| R4 | middleware 验签引入 JWKS 可用性依赖 | 中 | JWKS 缓存5min+ 故障 fail-closed + 启动自检 |
| R5 | next-intl 与 RSC 流式模式冲突 | 低 | P1 先做技术验证(一个页面跑通再推广) |
| R6 | MSW 数据与真实 schema 漂移 | 中 | mock 数据类型从 codegen types 导入;契约切换时删 mock |
| R7 | 旧 portal 删除时发现有未迁移边角功能 | 低 | P5 删除前做路由 diff 清单评审;先归档后删除 |
| R8 | config-service 单点故障影响仪表盘 | 低 | P0-4 内置默认配置已兜底;页面不依赖插件配置 |
---
## 13. 附录
### 13.1 命令手册
```bash
# 开发(无后端)
NEXT_PUBLIC_MSW=1 NEXT_PUBLIC_DEV_MODE=true node node_modules/next/dist/bin/next dev -p 4010
# 质量门禁CI 全量)
node node_modules/typescript/bin/tsc --noEmit # typecheck
node node_modules/.bin/eslint src # lint或 pnpm lint
node node_modules/vitest/vitest.mjs run # 单测
node node_modules/next/dist/bin/next build # 构建
node node_modules/.bin/eslint -c .eslintrc.tokens.js src # 令牌专项
# 契约
node_modules/.bin/tsx scripts/normalize-schema.ts && node_modules/.bin/graphql-codegen --config codegen.yml
node_modules/.bin/tsx scripts/generate-pq-manifest.ts
# 一致性校验P1 起)
node scripts/check-route-permissions.mjs # 路由表 ⇄ 文件系统
```
### 13.2 关键文件速查
| 主题 | 文件 |
| -------------- | ----------------------------------------------------------------------------------------------------------------- |
| 微内核仪表盘 | `src/shell/{Shell,ClientShell,LayoutManager,SlotRenderer,Registry,PropsMerger,PluginLifecycle,PluginStore}.tsx?` |
| 数据层 | `src/lib/{apollo-client,useWidgetQuery,useWidgetMutation,config-fetcher,usePluginConfig}.ts` + `src/lib/api/` |
| 安全 | `src/shared/lib/route-permissions.ts` + `packages/shared-ts/src/permission-bitmap.ts` +(新)`src/middleware.ts` |
| 错误处理 | `src/shared/components/{plugin,route,section}-boundary.tsx` + `packages/hooks/src/use-error-report.ts` |
| 设计令牌 | `packages/ui-tokens/src/{primitive,semantic-light,semantic-dark,tailwind-theme}.css` |
| 组件库 | `src/shared/components/ui/*` + `packages/ui-components/src/*` |
| 迁移源 | `apps/{teacher,student,parent,admin}-portal/src/app/**/page.tsx` + `apps/teacher-portal/src/mocks/handlers-p*.ts` |
| 后端 schema 源 | `services/*/src/graphql/generated/schema.graphql``src/lib/api/__generated__/combined-schema.graphql` |
### 13.3 参考文档
| 文档 | 用途 |
| ----------------------------------------------------------------------- | ------------------------------------------ |
| [README.md](./README.md) v2.0 | 微内核仪表盘子系统详细设计(保留有效部分) |
| [004 §5.4](../../docs/architecture/004_architecture_impact_map.md) | 视口四层模型(导航/路由/组件/数据) |
| [004 §11.7](../../docs/architecture/004_architecture_impact_map.md) | 前端数据层 + GraphQL 安全栈设计意图 |
| [GraphQL @auth 审计](../../docs/security/graphql-auth-audit-2026-07.md) | 后端 resolver 权限现状 |
| [MIGRATION_GUIDE.md](../../MIGRATION_GUIDE.md) | CICD → Edu 迁移背景与资产清单 |
| [多 AI 协作规范](../../docs/standards/multi-ai-collaboration.md) | 并行工作规则 |
### 13.4 术语表
| 术语 | 定义 |
| ------------------- | ------------------------------------------------------------------- |
| AppFrame | 全站页面框架TopBar + Sidebar + Main`src/app/shell/layout.tsx` |
| 微内核仪表盘 | `/shell` 角色首页的插件聚合区v2.0 遗产,保留收缩) |
| 页面模板四件套 | list / detail / form / workbench 四种页面骨架§7.3 |
| 契约工单 | 前端缺 schema 字段时的后端需求登记单§11.4 |
| MSW 兜底层 | `src/mocks/` 开发态模拟后端§5.4 |
| `@contract-pending` | 页面头注释,标记数据来自 MSW 待契约切换 |
| fail-closed | 无身份/无权限/服务不可用时一律拒绝并显式提示,禁止静默放行 |
| 视口四层 | 004 §5.4:导航 L1 / 路由 L2 / 组件 L3 / 数据 L4 |
---
> **本文件自发布之日起为 portal-shell 前端工作唯一权威。任何"已完成"声明必须能以 §1.1 同等严格度复现。下一阶段开工前,先完成 P0 并回填本文件 §9/§10 状态。**