Files
gaokao-volunteer-system/FRONTEND_REFACTOR_PLAN_2026-07-02.md
Frontend Developer fa7c22e8ed chore(s1): monorepo skeleton + 30-file prototype first commit
Sprint 1 G0 闸门通过 (pnpm install / typecheck / lint / build / turbo 全部 exit 0)

实际完成:
- T-A-01 monorepo 根骨架: pnpm-workspace.yaml + turbo.json + 根 package.json
- T-A-02 收编原型: apps/web/ 30 个 src/ 文件 (4948 行) 首次入库
- T-A-03 lint/format: eslint flat config (根 + apps/web) + prettier
- T-A-23 web-ci.yml: GitHub Actions 4-step CI (typecheck/lint/build/bundle)

勘误 (前置假设错误):
- 原型 4948 行 (不是 4114 行)
- 原型含 33 个 any + 16 个未用变量 (Sprint 2 修)
- 原型从未 commit, 用 cp -r + git add, 不是 git mv

推迟到 Sprint 2+:
- T-A-04/05 tsconfig packages 抽离
- T-A-06/07/08 design tokens 抽离
- T-A-09~13 5 基础组件

依赖隔离:
- .gitignore 加 11 行 monorepo 规则
- apps/web/.gitignore 补 .npm-cache/next-dev.log
- 删除 apps/web 内嵌 .git/ (cp -r 复制时携带)

验证:
- pnpm install: 1m55s, 456 packages
- typecheck: 0 errors
- lint: 0 errors, 49 warnings (any + unused)
- next build: 8 routes compiled, 9/9 static pages
- turbo run build: 1 successful, 12.9s
2026-07-03 09:22:03 +08:00

906 lines
43 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.
# 前端重构方案 · 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 阶段