# portal-shell 前端架构总纲(v3.0) > 版本:3.0 > 日期:2026-07-20 > 状态:**P0 已完成(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. [实施路线图(P0–P6)](#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 声称"P0–P4 全部验证通过、功能验收 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/terms);Edu 旧 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 mutation;README 写 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-service(L3),而它过滤的只是"仪表盘上显示哪些卡片"。 - 权限位图 `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.0(shadcn 标准 + 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 PARTIAL,0 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` 用 `_` 大写下划线(`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 个…admin(3)",实际注册 31 个(admin 6)——代码对、注释陈旧。 - `docs/standards/ui-design-system.md` 全文面向"4 个微前端 + Module Federation"——该架构已被 ADR-033 废弃。 - `MIGRATION_GUIDE.md`/根 README 仍写"前端:Next.js + Module Federation(4 微前端)"。 #### 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 handlers(P4/P5/P7 全覆盖) | `apps/teacher-portal/src/mocks/` 等 | ✅ 可用 | **迁移为 portal-shell 开发兜底层** | | next-intl messages(zh-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 :4010(Next.js 16 App Router 单容器) │ │ │ │ middleware.ts(新) │ │ ├─ 公共路径放行(/login、/api/health、静态资源) │ │ ├─ JWT cookie 校验 → 注入 x-user-id/x-user-role/x-perms │ │ └─ checkRoutePermission(L1 角色 + L2 权限点,fail-closed) │ │ │ │ /login 登录页(新) │ │ /shell/forbidden 403 页(新) │ │ /shell ───────────────────── ┐ │ │ layout.tsx(新框架) │ AppFrame:TopBar + Sidebar │ │ ├─ page.tsx(catch-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 :8080(REST:认证/上传/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 E2E(P6)。 ### 3.3 与现有架构的关系(保留什么、改变什么) | 现有(v2.0) | v3.0 处置 | 理由 | | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | 单 catch-all `/shell/[[...route]]` 承载一切 | **收缩**:仅渲染角色仪表盘;新增显式路由优先于 catch-all(Next.js 路由规则天然支持) | 页面可寻址、RSC 预取、loading/error 按路由生效 | | LayoutManager 5 模板(classic/focus/split/triple/canvas) | **保留**为仪表盘区布局;页面区用固定 AppFrame(TopBar+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///...`),共享 `AppFrame` 布局;插件系统收缩为 `/shell` 角色首页的聚合仪表盘。 - **取舍**:放弃"admin 可配置一切页面"的幻想——页面是代码、受版本控制、可 code review;admin 可配置的收缩为"导航可见性 + 仪表盘卡片 + 卡片 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 §4);JS 永远接触不到 token。 3. `middleware.ts`:读取 cookie → 校验(开发态 decode 验签跳过,生产用 iam JWKS RS256 验签)→ 注入 `x-user-id`/`x-user-role`/`x-user-permissions` 请求头供 RSC 读取;无 token → redirect `/login?next=`。 4. `/api/graphql` Route Handler:Apollo 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 接线 + 权限模型修正 - **背景**:F4(checkRoutePermission 死代码)、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-A4:GraphQL 契约纪律 - **背景**:F2(50 操作 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-tokens,zinc 色板)为唯一设计系统**,全局适用(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-A6:next-intl 正式接入 - **背景**:旧页面用 `useTranslations`,messages 资产完整;portal-shell 现有自造 `useT()` 只有 3 条文案。 - **决策**:接入 next-intl(App Router 模式),`messages/zh-CN.json` + `messages/en.json` 从旧 portal 合并迁移;`ThemeI18nProvider` 删除自造 i18n,保留主题切换;新代码文案一律走 `useTranslations`,禁止硬编码中文字符串(ESLint 规则 P1 后启用,迁移期 warn)。 - **取舍**:增加一个依赖与少量配置,换取不重写全部文案 + 与 CICD/旧 portal 一致的 i18n 习惯。 #### V3-A7:MSW 开发兜底层 - **背景**: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=; 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 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)` 验 RS256;JWKS 端点不可达时 **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/.ts(语义化 hooks:useExams(classId)) → lib/api/operations/*.graphql.ts(gql 文档,真实 schema 子集) → lib/useWidgetQuery / useWidgetMutation(Apollo 封装 + ApiError) → Apollo Client(APQ 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;透传 body(APQ 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/.ts`),自旧 portal `src/mocks/handlers-p*.ts` 迁移并按 50+ 操作重组。 - 启用:`NEXT_PUBLIC_MSW=1`(仅 dev/test);`src/app/layout.tsx` 条件 `import("@/mocks/browser")`(动态 import,production 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 cookie,JS 零接触 | `/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 | P0:compose 改 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//` 与 route-permissions 表语义对齐(PREFIX 表按前缀批量保护)。 - 仪表盘 `/shell` 按角色渲染不同插件集(现有机制),同时是各功能卡的入口枢纽。 - 跨角色同构页面(如 notifications/settings)放共享路径,权限表按角色开放。 ### 7.2 AppFrame(页面框架) `src/app/shell/layout.tsx`(新): ``` ┌──────────────────────────────────────────────┐ │ TopBar:global-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.tsx(RSC 示例骨架) export default async function ExamsPage(): Promise { const { userId, role } = await getServerIdentity(); const client = createApolloClient(); const dataPromise = client.query({ query: GET_EXAMS_DOC, variables: {/* … */}, }); return ( {/* Promise 直传客户端组件,use() 消费(沿用现有流式模式) */} ); } ``` - 列表/详情页: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 / 380px(Workbench 模板内置)。 ### 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:1(zinc 令牌天然满足,禁自调浅色)。 - 所有交互元素可 Tab 聚焦;`focus-visible:ring-2 focus-visible:ring-ring`。 - 语义化标签(`nav/main/aside/table`);表单控件必须 `