Files
gaokao-volunteer-system/FRONTEND_REFACTOR_PLAN_2026-07-02.md

906 lines
43 KiB
Markdown
Raw Normal View History

# 前端重构方案 · Frontend Refactoring Plan
> 评审人:**Frontend Developer (资深前端开发工程师)**
> 日期2026-07-02
> 输入约束:
> - **UI 与交互**:严格采用 `前端原型代码/` 中已落地的设计(不容许外观/行为回归)
> - **技术栈**:由前端专家决定(推荐方案附后)
> 上游依据:`REVIEW_REPORT_2026-07-02_SENIOR_DEVELOPER.md`V2 全面版)
> 上游审查4,114 行 TS/TSX、8 页 / 7 组件 / 8 hooks / 247 行 design-system.css 已扫读
---
## 0. TL;DR
**核心判断:原型代码的 UI 与交互已经过一轮有意识的设计打磨247 行 design-system、三态主题、消息路由模式、信息收集进度条、冲稳保三 Tab、风险徽章、紧凑移动端可以直接收编为 monorepo 中的 `apps/web` 子项目,并补齐三件缺失物:测试基础设施 / 真实后端对接 / 可维护的 UI 组件库。**
**技术栈决策(前端专家决定)**
| 层 | 选型 | 理由 |
|---|---|---|
| 框架 | **Next.js 16.xApp Router** | 沿用原型,避免重新评估 |
| UI 运行时 | **React 19.x** | 沿用原型,与 Next 16 协同验证 |
| 语言 | **TypeScript 5.xstrict** | 沿用原型 |
| 样式 | **Tailwind CSS 4 + design-system.css tokens** | 沿用原型 + CSS 变量映射 |
| 客户端状态 | **Zustand4.x** | hook 数量从 8 → 20+ 时避免 prop drilling |
| 服务端状态 | **TanStack Query 5.x** | 替换 localStorage 缓存 + 真实 API 同步 |
| 表单 | **React Hook Form + Zod** | FormCard 已 374 行,缺类型安全 |
| API 客户端 | **openapi-typescript-codegen** | 基于 `admin/app.py` OpenAPI 自动生成 |
| Markdown | **react-markdown 10 + rehype-sanitize 6 + remark-gfm 4** | 沿用XSS 安全) |
| 单元测试 | **Vitest 2.x + React Testing Library** | 与 Vite 生态契合 |
| E2E 测试 | **Playwright 1.5x** | 跨浏览器,移动/桌面双视图 |
| 视觉回归 | **Chromatic / Storybook Test** | design token 改动安全网 |
| 国际化 | **next-intl 3.x** | `lang="zh-CN"` 起步,预留 en-US |
| 监控 | **Sentry前端 + Vercel Analytics / Web Vitals** | Core Web Vitals 预算 |
| 构建 | **Turborepo + pnpm workspaces** | monorepo 编排 |
| 包管理 | **pnpm 9.x** | 节省磁盘,提升 monorepo 性能 |
**为什么不用 Vite + React Router 替代 Next.js** ① 沿用原型决策可避免一轮"无收益的技术换血";② Next 16 的 RSC + Server Actions 对未来**公开门户 SEO 优化**`/pricing` `/privacy`)有结构性优势;③ 部署可走 CloudStudio 预览或自托管,对小团队更轻。
**为什么加 Zustand** ① 当前 8 个 hook 已经够用,但 B 阶段接入真实后端后会膨胀;② TanStack Query 已覆盖 90% 服务端状态Zustand 只承担"跨页 UI 状态"(主题、侧边栏折叠、对比选择);③ 比 Redux Toolkit 少 70% 模板代码。
**为什么加 TanStack Query 而非 SWR** ① mutation / optimistic update / infinite query 能力更全;② DevTools 体验对调试 `useChat` 状态机更友好。
---
## 1. UI / 交互不变量(强约束)
> **本节是迁移的"宪法"——所有后续 PR 必须满足这些不变量,不允许任何"看起来更优"的越权修改。**
### 1.1 视觉设计系统(来自 `前端原型代码/src/styles/design-system.css`
#### 1.1.1 品牌色9 阶)
```
--brand-50 #eff6ff --brand-500 #3b82f6 --brand-900 #1e3a8a
--brand-gradient: linear-gradient(135deg, --brand-600 → #7c3aed)
```
**保留**:原型已有 `bg-gradient-to-br from-blue-600 to-purple-600`Logo / 头像)—— 不替换为单色。
#### 1.1.2 业务语义色
```
录取概率: rush #f97316 / stable #3b82f6 / safe #22c55e
风险等级: high #ef4444 / medium #eab308 / low #22c55e
风险背景: high-bg #fef2f2 / medium-bg #fefce8 / low-bg #f0fdf4
风险边框: high-border #fecaca / medium-border #fef08a / low-border #bbf7d0
```
**保留**:暗色主题下需调整 `--color-risk-*-bg``rgba(..., 0.15)` 透明度(原型已做,迁移时不动)。
#### 1.1.3 排印 / 间距 / 圆角 / 动效
```
font-sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei"…
text: 7 阶xs 0.75 / sm 0.875 / base / lg / xl / 2xl / 3xl
space: 11 阶0 ~ 164px 基准
radius: 6 阶sm 0.375 / md 0.5 / lg 0.75 / xl 1 / 2xl 1.25 / full 9999
transition: 4 阶fast 150ms / normal 250ms / slow 350ms / spring cubic-bezier(0.34, 1.56, 0.64, 1)
```
**保留**:原型已有"圆角 2xl = 卡片""full = 胶囊徽章"—— 不替换为 shadcn 的 `rounded-md` 默认值。
### 1.2 三态主题系统(来自 `lib/theme.ts` + `components/shared/ThemeToggle.tsx`
| 模式 | 行为 | 持久化 | 触发 |
|---|---|---|---|
| `light` | `data-theme="light"` | localStorage `theme=light` | 用户点击 ☀️ |
| `dark` | `data-theme="dark"` | localStorage `theme=dark` | 用户点击 🌙 |
| `system` | 移除 `data-theme` 属性,由 `@media (prefers-color-scheme: dark)` 接管 | localStorage 移除 key | 用户点击 💻 |
**关键不变量**
-**`initThemeScript()` 必须在 `<head>` 内联同步执行**(防 FOUC 闪白)—— `app/layout.tsx:20` 已挂载
-**ThemeToggle 是 `role="radiogroup"`,三个按钮 `role="radio"` `aria-checked`** —— 不能改成下拉框
-**不允许"跟随系统"自动覆盖 localStorage** —— 用户的显式选择必须保留
-**不允许去掉 `suppressHydrationWarning`** —— 这是闪白防护的必要条件
### 1.3 消息路由模式(来自 `components/ChatMessage.tsx`
```ts
type MessageType = 'text' | 'form_card' | 'plan_card' | 'career_card' | 'audit_report' | 'file_upload_prompt' | 'system';
```
**ChatMessage 不允许变成"自由渲染"** —— 必须按 `message.type` 路由到对应子组件PlanCard / CareerCard / AuditReportCard / FormCard / FileUploadPrompt / SafeMarkdown
**不变量**
- ✅ 用户消息:`flex justify-end` + `bg-blue-600 text-white` + 圆角 `rounded-2xl rounded-br-md`
- ✅ AI 消息:`flex items-start gap-3` + AI 头像(蓝→紫渐变) + 圆角 `rounded-2xl rounded-tl-md`
- ✅ 系统消息:居中灰底胶囊
-**不允许出现"消息卡片之间不留 16px 间距"** —— `mb-4 px-4`
### 1.4 模式指示器(来自 `components/navigation/ModeIndicator.tsx`
```ts
type ChatMode = 'explore' | 'generating' | 'auditing' | 'adjusting';
```
**`deriveMode(profile, currentPlan, isAuditActive)` 决策树**
```
isAuditActive → 'auditing' ✅
currentPlan → 'adjusting' 🔄
profile.province && profile.score → 'generating' 📊
其他 → 'explore' 🔍
```
**不变量**
- ✅ 4 个模式 4 套配色(蓝/紫/橙/绿)
- ✅ 必须放在聊天区 header 左侧(`Sidebar` 之后),移动端可见
-**不允许合并为单一 status bar** —— 4 个模式对应 4 类 AI 行为,用户需要可观察的提示
### 1.5 信息收集进度条(来自 `components/shared/ProgressSteps.tsx`
```ts
steps = [
{ key: 'province', label: '省份' },
{ key: 'score', label: '分数' },
{ key: 'subjects', label: '选科' },
];
```
**触发条件**`!hasCoreInfo && hasAnyInfo`(部分信息已收齐时显示,引导用户继续)。
**不变量**
- ✅ 3 个圆点 + 连接线(`bg-green-500` / `bg-brand-500 ring-2 ring-brand-200` / `bg-gray-200`
- ✅ 进度容器:`bg-white/90 border border-gray-100 rounded-full px-3 py-1.5 shadow-sm`
-**不允许换成 stepper 横向长条** —— 紧凑胶囊是关键
### 1.6 PlanCard 三 Tab来自 `components/PlanCard.tsx`
**结构****不可破坏**
1. Header标题 `📊 你的志愿方案` + 用户画像摘要(省份·选科·分数·位次)
2. **TABS冲刺/稳妥/保底**mobile 用单字 `冲/稳/保`desktop 用全名)
3. School List每行展开概率条 + 风险徽章 + 预估分数
4. Footer保存 / 导出 / 调整 三按钮
**不变量**
- ✅ Tabs 用 `role="tablist" aria-label="志愿方案分类"`
- ✅ 当前 Tab 文字色 = TABS 配置色(`text-orange-600` / `text-blue-600` / `text-green-600`),下方有 0.5 高亮线
- ✅ 概率条 `role="progressbar" aria-valuenow aria-valuemin aria-valuemax aria-label="录取概率 X%"`
- ✅ 颜色映射:`p >= 80` 绿、`50 ≤ p < 80` 蓝、`30 ≤ p < 50` 黄、`p < 30`
- ✅ 风险徽章三色:`低` 绿、`中` 黄、`高/较高`
-**不允许把"冲刺/稳妥/保底"合并为一个 list** —— 这是用户认知的核心分组
-**不允许去掉"调整"徽章动画**`animate-pulse` 表示"已更新"
### 1.7 移动端断点(来自 `app/page.tsx` + `Sidebar.tsx` + `MobileNav.tsx`
| 视口 | 行为 |
|---|---|
| `< 1024px` | 显示 `MobileNav`(底部 3 Tab对话/方案/记录),隐藏 `Sidebar`Logo 移动端可见 |
| `>= 1024px` | 显示 `Sidebar`280px 宽,左侧固定),隐藏 `MobileNav`,导航链接在 header |
| `>= 1024px` header | 右侧导航 `💬 咨询记录` `📋 我的方案` |
| `< 1024px` header | 右侧新建对话按钮 ✨ |
**不变量**
-`Sidebar` 仅在 `lg:flex` 时渲染(不是 `hidden`,避免 SSR 闪烁)
-`MobileNav` 底部 padding 使用 `env(safe-area-inset-bottom, 0px)`iPhone 安全区)
-`MobileNav` Tab 高度 ≥ 48pxmin-h-[48px])—— WCAG 2.5.5 触控目标
### 1.8 输入区交互(来自 `app/page.tsx:215-255`
**不变量**
- ✅ 文本域:自动撑高,最大 180px`scrollHeight` 监听)
- ✅ Enter 发送 / Shift+Enter 换行
- ✅ 发送按钮:空内容时灰底禁用,有内容时蓝底阴影
- ✅ 附件按钮:折叠时灰、展开时蓝底高亮
- ✅ 底部小字 "AI辅助决策请以官方信息为准" 仅桌面端可见(`hidden lg:block`
-**不允许换成"按下回车不换行直接发送"** 的非 textarea 实现
### 1.9 快速提示(来自 `app/page.tsx:60-67`
**三态动态推荐**
- `hasCoreInfo`(省份+分数+选科都有)→ `['生成志愿方案', '审核我的方案', '调整方案(只看珠三角)', '了解人工智能工程师']`
- `hasAnyInfo`(部分有)→ `['我是广东省的', '物理类考生', '了解一下平行志愿', '审核我的方案']`
- `!hasAnyInfo`(全无)→ `['了解人工智能工程师', '平行志愿怎么录取', '广东省物理类考生', '审核我的方案']`
**不变量**
- ✅ 水平滚动 `overflow-x-auto scrollbar-none`
- ✅ 胶囊样式 `bg-gray-50 border border-gray-200 rounded-full`
- ✅ 点击 → 写入 input 并 focus不直接发送
### 1.10 表单分步(来自 `components/FormCard.tsx`
**3 步**`basic``subjects``prefs`
**不变量**
- ✅ 步骤按钮:当前 active 蓝底,未完成灰色禁用,已完成绿底可回退
- ✅ 不允许跳过未完成步骤(`goToStep` 守卫)
- ✅ 字段触摸后才显示验证(`touched` 状态)
- ✅ 顶部"更喜欢直接聊?也可以在对话框里说…"提示条
- ✅ 省份下拉31 个省级行政单位(香港/澳门/台湾按国家级处理为缺省"广东"映射)
-**不允许改成单页长表单** —— 三步法是用户认知负荷的关键保护
### 1.11 上传条(来自 `components/UploadBar.tsx`
**4 种模式**
| 模式 | 接受类型 | 大小限制 | 颜色 |
|---|---|---|---|
| Excel | `.xlsx, .xls` | 5 MB | 绿 |
| 图片 | `image/*` | 10 MB | 紫 |
| PDF | `.pdf` | 10 MB | 橙 |
| 粘贴 | 无 | 无 | 蓝(切换为 textarea |
**不变量**
- ✅ 默认折叠,点击"展开 ▼"展开网格
- ✅ 粘贴模式textarea + 取消按钮 + 提交按钮
- ✅ 文件大小超限:`alert` 弹出错误(**保留**,与现有 7-7-7 规则一致——不要换 toast
-**不允许使用 modal**(用户已在聊天上下文,弹窗打断对话)
### 1.12 无障碍(必须保留)
| 项 | 实现 | 不可移除原因 |
|---|---|---|
| 全局焦点指示器 | `*:focus-visible` `outline: 2px solid var(--brand-500)` | 键盘导航唯一可见反馈 |
| 减少动画偏好 | `@media (prefers-reduced-motion: reduce)` 覆盖所有 `animation-duration` | 前庭功能障碍用户 |
| 移动端 Tab 高度 | `min-h-[48px]` | WCAG 2.5.5 触控目标 |
| 概率条 ARIA | `role="progressbar" aria-valuenow` | 屏幕阅读器可读 |
| 风险徽章 ARIA | `role="alert"` | 风险即时播报 |
| Tabs ARIA | `role="tablist" / "tab" aria-selected` | 屏幕阅读器路由 |
---
## 2. 现状盘点
### 2.1 文件清单(`前端原型代码/src/`
```
src/ 4114 行(已扫读)
├── app/ 8 页面
│ ├── layout.tsx 33 RootLayout + theme init
│ ├── page.tsx 270 聊天对话
│ ├── globals.css 90 全局基础样式
│ ├── assessment/page.tsx 350 Holland RIAS 测试
│ ├── consultations/page.tsx 253 咨询记录
│ ├── plans/page.tsx 308 我的方案
│ ├── plans/[id]/page.tsx 167 方案详情
│ ├── plans/compare/page.tsx 154 方案对比
│ └── about/page.tsx 232 关于/帮助
├── components/ 7 组件
│ ├── FormCard.tsx 374 分步表单
│ ├── AuditReportCard.tsx 159 审核报告卡
│ ├── PlanCard.tsx 192 方案卡
│ ├── ChatMessage.tsx 93 消息路由
│ ├── UploadBar.tsx 141 上传条
│ ├── CareerCard.tsx 70 职业卡
│ ├── FileUploadPrompt.tsx 36 上传提示
│ ├── navigation/ 3
│ │ ├── Sidebar.tsx 115
│ │ ├── MobileNav.tsx 75
│ │ └── ModeIndicator.tsx 74 + deriveMode
│ └── shared/ 3
│ ├── ThemeToggle.tsx 50
│ ├── SafeMarkdown.tsx 103
│ └── ProgressSteps.tsx 89
├── lib/ 8 hooks + 1 utility
│ ├── theme.ts 68
│ ├── useChat.ts 543 编排 6 个子 hook
│ ├── useConsultation.ts 167
│ ├── useMessages.ts 88
│ ├── usePlan.ts 89
│ ├── useProfile.ts 87
│ ├── useAudit.ts ?
│ └── useSimulation.ts 43
└── styles/
└── design-system.css 247 设计系统 token
```
### 2.2 强项(不可破坏)
1. **完整 design system**247 行 CSS 变量 + Tailwind 4 @theme 映射)
2. **三态主题**`initThemeScript` 同步注入 + `data-theme` 属性 + `prefers-color-scheme` 兜底)
3. **消息路由模式**`ChatMessage``message.type` 派发,避免单组件膨胀)
4. **XSS 防护**`SafeMarkdown` + `rehype-sanitize``dangerouslySetInnerHTML` 仅 1 处受控字符串)
5. **WCAG 起步**`focus-visible` + `prefers-reduced-motion` + ARIA roles
6. **`tsconfig.strict`** + `moduleResolution: "bundler"` + `@/*` 路径别名
7. **桌面/移动 双视图**`Sidebar` + `MobileNav` + header 响应式导航)
### 2.3 弱项(必须修复)
| 弱项 | 影响 | 严重度 |
|---|---|---|
| **零测试覆盖** | 重构无安全网 | **P0** |
| **零后端对接**`grep fetch/axios` 0 命中) | 迁到生产后所有 hook 要重写 | **P0** |
| **localStorage 存敏感信息**`useConsultation` / `plans/page.tsx` | 跨设备不同步 / 用户清除数据丢失 | **P1** |
| **组件库不完整**(无 Button / Input / Dialog / Toast | 重复实现、风格漂移 | **P1** |
| **mock 数据硬编码**`useChat.ts:42-90` 广东省 620 物理类) | 无法演示其他场景 | **P2** |
| **无 i18n 框架**(文案硬编码中文) | 海外/多语种扩展困难 | **P2** |
| **无 Zustand / Jotai**8 hook 还撑得住20+ 时混乱) | 状态管理可维护性 | **P2** |
| **无 CI**`typecheck / lint / build` 失败无门禁) | 协作摩擦 | **P0** |
---
## 3. 目标架构
### 3.1 Monorepo 结构
```
gaokao-volunteer-system/
├── apps/
│ ├── admin/ # FastAPI 后端(不变)
│ └── web/ # Next.js 前端(收编自 前端原型代码/
├── packages/
│ ├── ui/ # 设计系统 + 组件库
│ │ ├── tokens/ # design-system.css + theme.ts
│ │ ├── components/ # Button/Input/Card/Dialog/Toast/...
│ │ ├── navigation/ # Sidebar/MobileNav/ModeIndicator
│ │ ├── chat/ # ChatMessage/PlanCard/CareerCard/...
│ │ └── forms/ # FormCard
│ ├── api-client/ # OpenAPI 生成的 TS 客户端
│ │ ├── src/
│ │ ├── openapi.json # 来自 FastAPI /openapi.json
│ │ └── codegen.config.ts
│ ├── hooks/ # 跨页业务 hookuseChat/useProfile/usePlan/...
│ ├── store/ # Zustand storestheme/ui/compare
│ ├── i18n/ # next-intl 配置 + 词条
│ ├── test-utils/ # Vitest 配置 + RTL 包装 + MSW handlers
│ └── tsconfig/ # 共享 tsconfig.base.json
├── turbo.json
├── pnpm-workspace.yaml
└── package.json
```
**关键决策**
- ✅ 收编 `前端原型代码/``apps/web/`(删 `前端原型代码/` 目录)
- ✅ design-system.css 提到 `packages/ui/tokens/`,被 `apps/web` + 未来 `apps/admin-panel` 共享
- ✅ 业务 hookuseChat 编排的 6 个子 hook放进 `packages/hooks/`
- ❌ **不**用 nx/lerna —— Turborepo 任务编排已够用
### 3.2 数据流(真实后端对接后)
```
┌──────────────┐ fetch ┌──────────────────┐ TanStack ┌────────────┐
│ React 组件 │ ──────→ │ packages/api- │ Query │ FastAPI │
│ (apps/web) │ ←────── │ client (类型化) │ ←────────→ │ (admin/) │
└──────────────┘ 缓存 └──────────────────┘ mutation └────────────┘
│ │
│ localStorage │
▼ (offline cache only) ▼
┌──────────────┐ ┌────────────┐
│ Zustand UI │ │ SQLite / │
│ Store │ │ Postgres │
└──────────────┘ └────────────┘
```
**关键原则**
- 服务端数据**单一真相源** = FastAPI DB
- localStorage 仅作"乐观更新回滚"和"离线浏览缓存"
- Zustand 只承担"跨页 UI 状态"(主题 / 侧边栏折叠 / 方案对比选择)
- TanStack Query 管理"服务端状态的客户端缓存"
### 3.3 路由对照表(前端原型 vs 后端现状)
| 前端原型路由 | 对应后端 | 优先级 |
|---|---|---|
| `/`(对话) | **新增** `POST /api/chat/send` `GET /api/chat/history` | **P0**(首屏) |
| `/assessment`Holland 测试) | **新增** `POST /api/assessment` | P2 |
| `/consultations` | **新增** `GET /api/consultations` | P1 |
| `/plans` | **新增** `GET /api/plans` | P1 |
| `/plans/[id]` | **新增** `GET /api/plans/{id}` | P1 |
| `/plans/compare` | 客户端计算(不调后端) | P1 |
| `/about` | 静态文案 | P2 |
| **(后端已有)`/pricing`** | `GET /api/public/services` | P1 |
| **(后端已有)`/checkout/{v}`** | `POST /api/public/orders` `POST /api/public/payments/*` | P1 |
| **(后端已有)`/portal/{token}/info`** | `POST /portal/{token}/info` | P1 |
| **(后端已有)`/portal/{token}/status`** | `GET /portal/{token}/status` | P1 |
| **(后端已有)`/portal/{token}/report`** | `GET /portal/{token}/report` | P1 |
**说明**后端已有但前端原型没有的页面portal 流程),**不**在第一阶段收编——属于商业化产品,不属于"AI 志愿助手"主轴。
---
## 4. 5 阶段实施路径(与 V2 报告衔接)
> 本节是对 `REVIEW_REPORT_2026-07-02_SENIOR_DEVELOPER.md` §9 5 阶段方案的前端**细化版**。所有"页面/组件/hook"粒度任务都已落到本方案。
### 4.1 阶段 A — 基础设施10-12 人天)
#### A.1 Monorepo 收编1 人天)
- 创建 `apps/web/` 目录,把 `前端原型代码/src/` 整目录移入
- 根目录加 `pnpm-workspace.yaml` + `turbo.json`
- 加根 `package.json`devDependencies: turbo, prettier, eslint-config-prettier
- 验证:`pnpm --filter web dev` 启动原 dev server 无错
#### A.2 共享 tsconfig0.5 人天)
- 创建 `packages/tsconfig/base.json`strict + bundler resolution + paths
- `apps/web/tsconfig.json` extends base
- `packages/ui/tsconfig.json` extends base再加 `composite: true`
-`tsc --noEmit` 到 web 的 lint 脚本
#### A.3 设计系统提取2 人天)
-`src/styles/design-system.css` 移到 `packages/ui/tokens/design-system.css`
-`src/lib/theme.ts` 移到 `packages/ui/tokens/theme.ts`
-`packages/ui/package.json` 暴露 `tokens.css` + `theme.ts`
-`apps/web/app/globals.css` `@import "@gaokao/ui/tokens.css"`
-`apps/web/app/layout.tsx` 调用 `import { initThemeScript } from "@gaokao/ui/tokens/theme"`
#### A.4 UI 组件库起步2.5 人天)
新增 5 个基础组件(**不**改外观,仅抽公共样式):
| 组件 | 来源 | 说明 |
|---|---|---|
| `Button` | 原型分散在多处 | primary/secondary/ghost/danger 四种 variant |
| `Input` | FormCard 文本框 | 含 error/helpText 状态 |
| `Select` | FormCard 下拉 | 同上 |
| `Card` | PlanCard/AuditReportCard/CareerCard 共享 | 圆角 2xl + 阴影 sm + border |
| `Badge` | 风险徽章 | 三色 + 三等级 |
| `Tabs` | PlanCard Tab | 复用 role="tablist" |
每个组件必须配套:① Vitest 单测;② Storybook storyCSF 3.0);③ tsdoc 注释。
#### A.5 测试基础设施2 人天)
- `pnpm add -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom`
- 创建 `packages/test-utils/`
- `vitest.config.ts`jsdom + globals + setup
- `setup.ts`jest-dom matchers + MSW server
- `renderWithProviders.tsx`QueryClient + Theme + Router 包装)
- 加 3 个示范单测:
- `ModeIndicator.test.tsx`4 模式渲染)
- `deriveMode.test.ts`(决策树全覆盖)
- `SafeMarkdown.test.tsx`XSS 注入:`<script>alert(1)</script>` 不执行)
#### A.6 Playwright e2e 骨架1.5 人天)
- `pnpm add -D @playwright/test`
- 创建 `apps/web/e2e/`
- `playwright.config.ts`chromium + webkit + 移动 viewport
- `chat.spec.ts`**示范用例**:发送消息 → 收到 AI 回复 → 切换主题)
- `theme.spec.ts`(验证三种模式切换无闪白)
- `navigation.spec.ts`(桌面 Sidebar / 移动 MobileNav 切换)
#### A.7 OpenAPI 类型化1.5 人天)
- `pnpm add -D openapi-typescript-codegen`
- 创建 `packages/api-client/codegen.config.ts`
- 流程FastAPI 启动 → `GET /openapi.json` → 写入 `packages/api-client/openapi.json` → codegen
- `turbo.json``generate-api-client` 任务,依赖 `^build-admin`
- 加 1 个示范调用:`apps/web/lib/api/chat.ts` 用生成的 client 调 `POST /api/chat/send`**先用 mock server阶段 B 切真实**
#### A.8 CI 工作流1 人天)
- `.github/workflows/web-ci.yml`
- pnpm install
- turbo run lint typecheck test
- turbo run e2ePlaywright
- turbo run buildNext.js
- 加 Codecov 前端 target
- 加 bundle size 预算(`@next/bundle-analyzer`> 200KB warn
**A 阶段交付物**
- ✅ monorepo 可 `pnpm install && pnpm dev` 一键启动
- ✅ 设计系统 + 5 个基础组件 + 8 hooks + 8 页面 + 247 行 CSS 全部就位
- ✅ 测试覆盖率:原型组件 ≥ 60% line coverage
- ✅ CI 红绿信号在 PR 上可见
### 4.2 阶段 B — 真实后端对接25-35 人天)
> **核心目标**:把 prototype 的 8 个 mock hook 全部替换为真实 API 调用UI/UX **零变化**。
#### B.1 后端 API 补全5 人天,先于 B.2
后端需要新增以下端点(**FastAPI 后端**任务,前端不实现):
| 端点 | 方法 | 入参 | 出参 |
|---|---|---|---|
| `/api/chat/send` | POST | `{message, consultation_id?}` | `{user_message, assistant_message, consultation_id}` |
| `/api/chat/history` | GET | `?consultation_id=...` | `{messages: [...]}` |
| `/api/consultations` | GET | - | `{consultations: [{id, title, updated_at, profile}]}` |
| `/api/consultations/{id}` | GET | - | `{id, messages, profile, plan, audit}` |
| `/api/consultations/{id}` | DELETE | - | `{ok: true}` |
| `/api/plans` | GET | - | `{plans: [{id, name, created_at, rush_count, ...}]}` |
| `/api/plans/{id}` | GET | - | `{id, name, plan, profile, created_at}` |
| `/api/plans/{id}` | PATCH | `{name}` | `{ok: true}` |
| `/api/plans/{id}` | DELETE | - | `{ok: true}` |
| `/api/assessment` | POST | `{answers: [...]}` | `{riasec: {R, I, A, S, E, C}, recommendations: [...]}` |
| `/api/audit/upload` | POST | `multipart/form-data` | `{report_id, overall_score, risk_items}` |
**注意**:所有端点**必须**走 OpenAPI 文档 + 类型化错误响应(沿用 `admin/errors/registry.py`)。
#### B.2 useChat 真实化4 人天)
`useChat.ts`543 行)的 mock 拆解为:
```ts
// packages/hooks/useChat.ts
export function useChat(consultationId?: string) {
const queryClient = useQueryClient();
// 1. 历史查询
const { data: messages = [], isLoading } = useQuery({
queryKey: ['chat', consultationId],
queryFn: () => apiClient.chat.getHistory(consultationId),
staleTime: 30_000,
});
// 2. 发送 mutation
const sendMutation = useMutation({
mutationFn: (text: string) => apiClient.chat.send(consultationId, { message: text }),
onMutate: async (text) => {
// 乐观更新
await queryClient.cancelQueries({ queryKey: ['chat', consultationId] });
const previous = queryClient.getQueryData<Message[]>(['chat', consultationId]);
queryClient.setQueryData<Message[]>(['chat', consultationId], (old) => [
...(old ?? []),
{ role: 'user', type: 'text', content: text, timestamp: new Date() },
]);
return { previous };
},
onError: (err, text, context) => {
// 回滚
if (context?.previous) {
queryClient.setQueryData(['chat', consultationId], context.previous);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['chat', consultationId] });
},
});
// 3. 文件上传
const uploadMutation = useMutation({
mutationFn: (file: File) => apiClient.audit.upload(file),
onSuccess: (report) => {
queryClient.setQueryData<Message[]>(['chat', consultationId], (old) => [
...(old ?? []),
{ role: 'assistant', type: 'audit_report', data: report, timestamp: new Date() },
]);
},
});
return { messages, isLoading, send: sendMutation.mutate, upload: uploadMutation.mutate };
}
```
**UI 端**`app/page.tsx``const { messages, send, isLoading } = useChat(activeRecordId)`,其余代码不动。
#### B.3 useProfile 真实化1.5 人天)
profile 信息从后端 `user.id` 关联的 `user_profile` 表读取,新增 `/api/profile` 端点。
#### B.4 usePlan / useConsultation 真实化4 人天)
- 删 localStorage 写入路径
- 改用 TanStack Query 读 `/api/plans` `/api/consultations`
- PATCH重命名走 mutation
- DELETE 走 mutation + invalidate
#### B.5 离线缓存策略2 人天)
保留 localStorage **仅**作"乐观更新回滚"和"网络断线时的最后消息缓存"
- TanStack Query `persistQueryClient` 把 query cache 落盘
- 网络恢复时自动 refetch
- 失败时 fallback 到 last-known state
#### B.6 表单提交真实化1.5 人天)
`FormCard` `onSubmit``apiClient.profile.update(...)` → invalidate `['profile']`
#### B.7 错误处理1.5 人天)
-`admin/errors/registry.py` 的 5 字段结构在 `apiClient` 包装错误
- 错误码 → 用户文案映射(`packages/i18n/zh-CN/errors.json`
- 5xx 走 `ErrorBoundary` → 兜底页
- 429 / 网络错 → 顶部 Toast 提示(**新增** Toast 组件,**不**改 FormCard
#### B.8 e2e 真实化2 人天)
- 替换 Playwright `chat.spec.ts` 的 mock service worker 为真实 FastAPI
- 加 5 个关键路径 e2e
- 用户注册 → 咨询 → 收到 AI 回复
- 上传方案 → 收到审核报告
- 切换方案 Tab
- 调整主题(含 system 模式)
- 重启浏览器后历史恢复
#### B.9 性能预算验证1.5 人天)
-`@next/bundle-analyzer`
- 跑 Lighthouse CI 5 次取 P75
- LCP < 2.5s / INP < 200ms / CLS < 0.1
- Bundle sizefirst load JS < 200KB
**B 阶段交付物**
- ✅ 11 个端点全部对接(前端 8 页 + 后端 3 页新端点)
- ✅ 8 个 hook 全部真实化,零 mock 数据
- ✅ e2e 5 路径全绿
- ✅ Lighthouse P75 ≥ 90
### 4.3 阶段 C — 架构整合8-10 人天)
#### C.1 部署架构3 人天)
- 选其一:
- **方案 1推荐**CloudStudio 静态部署 + 反代 `/api/*` 到 FastAPI
- **方案 2**Vercel 部署 + Vercel Function 反代
- **方案 3**自托管PM2 / systemd
- CORS 配置:`allow_origins=["https://web.gaokao.example.com"]`
- CSP 头:基于 `secure_headers``script-src 'self'``style-src 'self' 'unsafe-inline'` 允许 Tailwind
#### C.2 监控2 人天)
- 前端 Sentry`@sentry/nextjs`
- Web Vitals 上报到 Vercel Analytics / 自托管 Prometheus
- 后端 Sentry + OpenTelemetry trace
- 前端 trace 透传到后端(`x-trace-id` header
#### C.3 安全响应头1.5 人天)
- FastAPI 加 `secure_headers` 中间件:
- `Strict-Transport-Security: max-age=63072000; includeSubDomains`
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: DENY`
- `Content-Security-Policy: default-src 'self'; img-src 'self' data:; ...`
- `Referrer-Policy: strict-origin-when-cross-origin`
#### C.4 反代 / 域名1.5 人天)
- 前端:`https://web.gaokao.example.com`
- 后端 API`https://api.gaokao.example.com`
- 短链接:`https://s.gaokao.example.com`(已有 `/s/{code}` 路由)
- 反代nginx / caddy 统一收敛
**C 阶段交付物**
- ✅ 前端可独立部署
- ✅ CSP / HSTS / X-Frame-Options 全套就位
- ✅ Sentry 前后端 trace 打通
### 4.4 阶段 D — 设计 / a11y 加固8-12 人天)
#### D.1 基础组件补齐4 人天)
新增 10 个组件(每个含 Vitest + Storybook
| 组件 | 用途 | 来源 |
|---|---|---|
| `Dialog` | 模态对话框 | 上传确认 / 删除确认 |
| `Toast` | 顶部提示 | 错误反馈 / 保存成功 |
| `Tooltip` | 悬浮提示 | Tab 角标含义 |
| `Dropdown` | 下拉菜单 | Sidebar 头像菜单 |
| `Avatar` | 用户头像 | ChatMessage AI / User |
| `Skeleton` | 骨架屏 | 加载占位 |
| `ProgressBar` | 进度条 | 通用(已存在 ProgressSteps独立 |
| `Switch` | 开关 | 偏好设置 |
| `Checkbox` | 复选框 | FormCard 选科 |
| `Radio` | 单选 | ThemeToggle 已用 radiogroup独立抽 |
#### D.2 a11y 全量审计2 人天)
-`axe-core` 自动扫描:目标 0 critical / 0 serious
- 手动测试屏幕阅读器NVDA / VoiceOver+ 键盘 Tab 导航
- 色彩对比度:所有文本 ≥ 4.5:1WCAG 2.1 AA
- Skip link所有页面顶部 "跳到主内容"
- Landmark roles`<header> <nav> <main> <aside> <footer>` 完整
#### D.3 暗色主题全覆盖1.5 人天)
- 验证每个组件的暗色变体
-`dark:` 前缀到必要 Tailwind 类
- 图表(如未来引入 echarts`prefers-color-scheme` 切换主题
#### D.4 国际化基础2 人天)
- `pnpm add next-intl`
- 抽所有中文文案到 `packages/i18n/zh-CN/`
- 占位 `packages/i18n/en-US/`(暂留空字典,未来填充)
- 路由级 locale 切换
#### D.5 Storybook + Chromatic1.5 人天)
- 30+ 组件 stories
- Chromatic 视觉回归(每组件 3 个状态light / dark / 移动)
#### D.6 性能深度优化1 人天)
- 图片:自动转 WebP / AVIF`next/image`
- 字体:`next/font` + `display: swap`
- 代码分割:`React.lazy` 路由级 + 组件级PlanCard、AuditReportCard 异步加载)
- prefetch`<Link prefetch>` 关键路径
**D 阶段交付物**
- ✅ 组件库 15+ 组件,每个有 test + story
- ✅ axe-core 0 critical
- ✅ Lighthouse P75 ≥ 95
- ✅ zh-CN / en-US 字典就位
### 4.5 阶段 E — 内部页面 React 化15-20 人天,可选)
`admin/static/dashboard.html` + `admin/routes/web_public.py` 的"运营管理"内部页 React 化:
| 页面 | 来源 | 复杂度 |
|---|---|---|
| 登录页 | `admin/routes/auth.py` | 低 |
| Dashboard | `admin/static/dashboard.html` | 中echarts 集成) |
| 订单列表 | `admin/routes/orders.py` | 中 |
| 订单详情 | 同上 | 中 |
| 案例列表 | `admin/routes/cases.py` | 中 |
| 分享页 editor | `data/share/permission.py` | 高(权限矩阵) |
| 短链接管理 | `data/share/short_link.py` | 低 |
**复用**:所有页面用 `packages/ui/` 组件 + `packages/api-client/` 类型化调用 + `packages/hooks/` 业务 hook。
**E 阶段交付物**
- ✅ 运营后台从静态 HTML / Python f-string 全部迁移到 React
- ✅ 与前端原型共享设计系统
---
## 5. 关键决策点(必须由 PM / Lead 拍板)
| # | 决策点 | 推荐 | 备选 |
|---|---|---|---|
| 1 | Monorepo 工具 | **Turborepo + pnpm** | Nx + pnpm |
| 2 | 客户端状态 | **Zustand 4.x** | Jotai / Redux Toolkit |
| 3 | 服务端状态 | **TanStack Query 5.x** | SWR |
| 4 | 表单库 | **React Hook Form + Zod** | Formik + Yup |
| 5 | 视觉回归 | **Chromatic** | Loki / Percy |
| 6 | 部署平台 | **CloudStudio** | Vercel / 自托管 |
| 7 | i18n 框架 | **next-intl 3.x** | react-i18next |
| 8 | 监控 | **Sentry** | 自托管 GlitchTip |
| 9 | 路由是否改 RSC | **保持客户端**useChat 状态复杂) | 局部 RSClayout |
| 10 | 公开门户是否纳入 | **否**(商业化产品,单独预算) | 合并做 |
---
## 6. 风险与缓解
| 风险 | 概率 | 影响 | 缓解 |
|---|---|---|---|
| 真实 API 与 mock 数据结构不一致 | 高 | 中 | OpenAPI Codegen 第一天就跑通,**强制类型对齐** |
| Next.js 16 + React 19 与 TanStack Query 兼容性 | 中 | 中 | 提前 1 天 spike备选 SWR |
| 移动端断点768 / 1024与原型不一致 | 低 | 高 | Playwright 移动 viewport 强制 e2e 覆盖 |
| Bundle size 爆炸react-markdown 很大) | 中 | 中 | `next/dynamic` 异步 + markdown 拆 chunk |
| localStorage 缓存策略导致数据冲突 | 中 | 中 | TanStack Query `persistQueryClient` + 时间戳同步 |
| 老用户浏览器数据迁移 | 高 | 低 | `version` 字段 + 一次性迁移逻辑 |
| ChatMessage 路由扩展性 | 中 | 中 | 加 `MessageRenderer` 抽象层,预留自定义 type |
---
## 7. 立即可执行5 条,本周启动)
1. **创建 monorepo 骨架**1 人天)
- `pnpm-workspace.yaml` + `turbo.json` + 根 `package.json`
- `apps/web/` 收编 `前端原型代码/src/`
- 验证 `pnpm dev` 可启动
2. **抽 design-system.css 到 `packages/ui/tokens/`**0.5 人天)
- 同步 `lib/theme.ts`
- `apps/web/app/globals.css``@import "@gaokao/ui/tokens.css"`
-`pnpm dev` 视觉 0 回归
3. **搭测试基础设施**1.5 人天)
- Vitest + RTL + MSW
- 3 个示范单测ModeIndicator / deriveMode / SafeMarkdown
- 1 个示范 Playwright e2e
4. **OpenAPI Codegen 试跑**1 人天)
- 起 FastAPI → 拉 `/openapi.json` → codegen
- 加 1 个示范调用(不切真实,仅验证类型)
5. **CI 工作流起步**0.5 人天)
- `.github/workflows/web-ci.yml`
- `pnpm install + turbo run lint typecheck test build`
- PR 上跑通
---
## 8. 工期与里程碑
| 阶段 | 人天 | 累计 | 里程碑 |
|---|---|---|---|
| A. 基础设施 | 10-12 | 10-12 | monorepo 跑通 + 5 组件 + e2e 骨架 + CI |
| B. 真实后端对接 | 25-35 | 35-47 | 11 端点对接 + Lighthouse ≥ 90 |
| C. 架构整合 | 8-10 | 43-57 | 独立部署 + Sentry + CSP |
| D. 设计/a11y 加固 | 8-12 | 51-69 | 15 组件 + axe 0 critical + i18n |
| E. 内部页面 React 化 | 15-20 | 66-89 | 运营后台全 React 化 |
| **总计** | **66-89** | - | **与 V2 报告一致** |
---
## 9. 不要做的事(避坑清单)
| ❌ 禁 | 理由 |
|---|---|
| 把 `前端原型代码/` 直接复制到 `apps/web/` 后**改任何样式 / 交互** | 违反用户"严格采用"约束 |
| 把 Tailwind 4 降到 3 找"稳定" | 团队已用 4 个月,无回退必要 |
| 改 `design-system.css` 的 token 名 | 全站 200+ 引用,破坏所有组件 |
| 去掉 `initThemeScript` 改用 `next-themes` | 闪白防护是硬需求next-themes 有 hydration 延迟 |
| 拆 `PlanCard` 三 Tab 为单一 list | 用户认知核心 |
| 删 `*focus-visible` | a11y 唯一键盘反馈 |
| 用 `dangerouslySetInnerHTML` 渲染 AI 消息 | 已有 `SafeMarkdown` 防护,绕过去 = XSS |
| 把 `useChat` 拆为多个 useState | 已 543 行,复杂状态机必须留 hook |
| 把 `'use client'` 提到 layout | 主题 init 必须在 server避免 hydration race |
| 用 emoji 替换 SVG 图标(除特定位置) | prototype 已经是混合,沿用 |
---
## 10. 验收标准Definition of Done
每个阶段结束必须满足:
- [ ] 所有原 prototype 页面**像素级无回归**Chromatic diff < 0.1%
- [ ] 三态主题切换无闪白Playwright `theme.spec.ts` 全绿)
- [ ] 移动端 768 / 1024 断点行为与原型一致
- [ ] `pnpm lint typecheck test build e2e` 全绿
- [ ] Lighthouse P75 ≥ 90性能 / 可访问性 / 最佳实践 / SEO
- [ ] axe-core 扫描 0 critical / 0 serious
- [ ] 新增依赖经过 Lead 评审(禁止无意识添加)
- [ ] OpenAPI 契约变更同步更新 `packages/api-client/`
- [ ] PR 包含截图 / 录屏(如有 UI 改动)
---
## 附录 A原型文件保留清单
迁移后**必须 1:1 保留**的文件:
```
[核心组件 - 不允许改 UI/交互]
apps/web/src/components/PlanCard.tsx # 192 行
apps/web/src/components/AuditReportCard.tsx # 159 行
apps/web/src/components/CareerCard.tsx # 70 行
apps/web/src/components/ChatMessage.tsx # 93 行
apps/web/src/components/UploadBar.tsx # 141 行
apps/web/src/components/FormCard.tsx # 374 行
apps/web/src/components/FileUploadPrompt.tsx # 36 行
apps/web/src/components/navigation/Sidebar.tsx # 115 行
apps/web/src/components/navigation/MobileNav.tsx # 75 行
apps/web/src/components/navigation/ModeIndicator.tsx # 74 行
apps/web/src/components/shared/ThemeToggle.tsx # 50 行
apps/web/src/components/shared/SafeMarkdown.tsx # 103 行
apps/web/src/components/shared/ProgressSteps.tsx # 89 行
[设计系统 - 不允许改 token 值]
apps/web/src/styles/design-system.css # 247 行
apps/web/src/lib/theme.ts # 68 行
[页面布局 - 不允许改容器结构]
apps/web/src/app/layout.tsx # 33 行
apps/web/src/app/globals.css # 90 行
[业务逻辑 - 允许重构内部实现,对外接口保持稳定]
apps/web/src/lib/useChat.ts # 543 行
apps/web/src/lib/useConsultation.ts # 167 行
apps/web/src/lib/useMessages.ts # 88 行
apps/web/src/lib/usePlan.ts # 89 行
apps/web/src/lib/useProfile.ts # 87 行
apps/web/src/lib/useAudit.ts
apps/web/src/lib/useSimulation.ts # 43 行
```
**外部接口稳定**的意思是:`useChat` 返回的 `{ messages, isTyping, sendMessage, submitForm, handleFileUpload, savePlan, ... }` 字段名和类型不能变。否则 `app/page.tsx` 全部重写。
---
## 附录 BOpenAPI Codegen 工作流
```
┌──────────────────┐
│ admin/app.py │
│ (FastAPI) │
└────────┬─────────┘
│ uvicorn admin.app:app
┌──────────────────┐
│ /openapi.json │ ← 自动生成
└────────┬─────────┘
│ curl > packages/api-client/openapi.json
┌──────────────────┐
│ openapi-typescript│ ← 每日 CI 跑
│ -codegen │
└────────┬─────────┘
│ 生成 TS 客户端
┌──────────────────┐
│ packages/api- │
│ client/src/ │ ← 类型安全
└────────┬─────────┘
│ import { apiClient } from '@gaokao/api-client'
┌──────────────────┐
│ apps/web 代码 │
└──────────────────┘
```
**关键约束**
- FastAPI 后端**不允许**在 `api_client/` 目录手写类型(避免漂移)
- 每次 FastAPI 改 Pydantic schema 后必须重跑 codegen
- CI 加 `api-client-types-check`:生成的类型 vs 实际代码使用 → 不一致则失败
---
## 附录 C与 V2 Senior Developer 报告的衔接
| V2 报告条目 | 本方案对应 |
|---|---|
| §8.3.1 零测试覆盖 | §4.1 A.5 + A.6 + B.8 |
| §8.3.2 后端 API 未对接 | §4.2 B.2-B.9 |
| §8.3.3 localStorage 存敏感信息 | §4.2 B.4 + B.5 |
| §8.3.4 Mock 数据真实性 | §4.2 B.2(替换为真实数据) |
| §8.3.5 国际化 | §4.4 D.4 |
| §8.3.6 状态管理 | §3.1 引入 Zustand |
| §8.3.7 构建与 CI | §4.1 A.8 |
| §8.3.8 可访问性深度 | §4.4 D.2 |
| §8.3.9 性能预算 | §4.2 B.9 + §4.4 D.6 |
| §8.3.10 与后端类型契约 | §4.1 A.7 + 附录 B |
| §9.2 5 阶段方案 | §4 本方案 5 阶段(细化版) |
| §9.3 工期估算 66-89 人天 | §8 一致 |
---
**评审完成。技术栈决策附在 §0UI/交互不变量在 §15 阶段实施路径在 §4立即可执行在 §7。**
**Frontend Developer 建议**
- 严格遵循 §1 不变量(这是用户的硬约束)
- §0 选型在 7 天内 spike 验证(特别是 TanStack Query + Zustand 与 Next 16 协同)
- §7 5 件事本周启动1 周后决定是否进入 §4.1 完整 A 阶段