框架切换:Next.js 16 → Vite 5 + React 19 + React Router 7 状态管理:7 手写 hook → 4 Zustand slice 数据获取:直接 fetch → TanStack Query 5 (15 hooks) 表单:3-step 状态机 → RHF 7 + Zod 测试:0 → 25 单测 + 20 e2e (4 浏览器) Bundle:192 KB gzip (目标 < 300 KB) Lint:49 warning → 0 warning Type:33 any → 0 any S2-T-01 切 Vite 5 + React 19 框架 S2-T-02 Zustand 4 slice 替代 7 手写 hook S2-T-03 TanStack Query 5 + 15 hooks S2-T-04 Vitest + RTL + MSW + Chromatic 配置 S2-T-05 OpenAPI Codegen + Zod schema 复用 S2-T-06 RHF 7 + Zod 重写 FormCard S2-T-07 Playwright 视觉基线 4 浏览器 5 spec S2-T-08 Vite build + bundle < 300KB gzip G1 闸门 (全部通过): - typecheck: 0 error - lint: 0 error 0 warning - test (Vitest): 25/25 passed - test:e2e (Playwright): 20/20 passed - build (Vite): 326 modules 4.14s - codegen:check: OpenAPI types 非占位 Sprint 2 收口报告:SPRINT_2_CLOSEOUT_2026-07-03.md
13 KiB
13 KiB
Sprint 2 任务拆解(W2-3 · 9 人天 · 56 子任务)— V10 选项 B
技术栈:Vite 5 + React 19 + TypeScript 5 + Tailwind 4 + Zustand 4 + TanStack Query 5 + React Hook Form 7 + Vitest 2 + Playwright 1.55 目标:整体重写 7 个手写 hook + 引入现代状态管理 + 后端 5 端点对接 闸门:G1(OpenAPI 类型生成无
any+ ESLint 0 warning) V10 变化:从 18d 缩到 9d(节省 9d = 删 33 any 修复 + 7 hook 重构 + 6 文件重写) PM 决策(2026-07-03):整体重写为新实现;Playwright 视觉回归 + Chromatic 验收
⚠️ V10 关键变化(与 V2 对比)
| 维度 | V2 | V10 选项 B |
|---|---|---|
| 框架 | Next.js 16(保留) | Vite 5 + React 19(重写) |
| 状态管理 | 自定义 7 个手写 hook | Zustand 4(4 个 slice) |
| 数据获取 | 自写 useChat + fetch | TanStack Query 5 |
| 表单 | 受控 + 3-step 状态机 | React Hook Form 7 + Zod |
| 测试 | Vitest + Playwright(保留) | 同 |
| 视觉回归 | 无 | + Chromatic(V10 新增) |
| 总子任务 | 58 | 56(少 2:合并到 Zustand store) |
| 总估时 | 18d | 9d |
0. Sprint 2 概览
| 任务 | 主任务 | 估时 | 子任务数 |
|---|---|---|---|
| T-A-14 | Vitest + RTL + MSW + Chromatic 配置 | 0.5d | 4 |
| T-A-15 | 示范单测:ModeIndicator | 0.25d | 3 |
| T-A-16 | 示范单测:SafeMarkdown | 0.25d | 3 |
| T-A-17 | 示范单测:FormCard(RHF 版) | 0.5d | 4 |
| T-A-18 | Playwright + Chromatic 配置 | 0.5d | 3 |
| T-A-19 | 示范 e2e:theme.spec | 0.5d | 3 |
| T-A-20 | 示范 e2e:navigation.spec | 0.5d | 3 |
| T-A-21 | OpenAPI Codegen + Zod schema 复用 | 1.0d | 5 |
| T-A-22 | 示范调用:MSW mock server | 0.5d | 4 |
| T-B-01 | Zustand store + 5 query hooks(chat) | 1.0d | 5 |
| T-B-02 | TanStack Query 3 个 consultations 端点 | 0.5d | 3 |
| T-B-03 | TanStack Query 4 个 plans 端点 | 0.5d | 3 |
| T-B-04 | RHF 重写 FormCard + assessment | 1.0d | 5 |
| T-B-05 | audit/upload 端点(V10 重写) | 1.0d | 4 |
| T-B-06 | Chromatic 视觉基线(V10 新增) | 0.5d | 3 |
| T-B-07 | Vite build + bundle 体积验证 | 0.5d | 4 |
| 合计 | 16 任务 | 9.5d | 56 |
T-A-14 · Vitest + RTL + MSW + Chromatic 配置(0.5d · 4 子任务)
ST-S2-A-14.1 写 vitest.config.ts(0.125d)
- 产出:
apps/web/vitest.config.ts(jsdom env + setup 文件 + alias) - V10 变化:保留 jsdom;alias 指向
packages/ui和packages/lib
ST-S2-A-14.2 写 setup.ts(0.125d)
- 产出:
@testing-library/jest-dom/vitest+ MSW server 启动/关闭 + Zustand store reset helper
ST-S2-A-14.3 写 renderWithProviders.tsx(0.125d)
- 产出:
packages/test-utils/renderWithProviders.tsx- QueryClient(测试用,禁用 retry)
- ThemeProvider
- Router(MemoryRouter)
- Zustand 注入 reset 函数
- 验收:
pnpm --filter web test跑通
ST-S2-A-14.4 占位测试验证(0.125d)
- 产出:
Math.test.ts(1+1=2) - 验收:
- 占位测试 1 绿
T-A-15 · 示范单测:ModeIndicator(0.25d · 3 子任务)
ST-S2-A-15.1 写 4 mode 渲染测试(0.125d)
- 覆盖:explore / generating / auditing / adjusting
- 验收:
- 4 模式 label 正确
ST-S2-A-15.2 写 deriveMode 决策树测试(0.0625d → 0d)
- 覆盖:
- isAuditActive → auditing
- currentPlan + no audit → adjusting
- province + score → generating
- 其他 → explore
- 验收:
- 100% branch coverage
ST-S2-A-15.3 验证 100% 覆盖(0.0625d → 0d)
- 命令:
pnpm test --coverage - 验收:
ModeIndicator.tsxline + branch 各 100%
T-A-16 · 示范单测:SafeMarkdown(0.25d · 3 子任务)
ST-S2-A-16.1 写 XSS 注入测试(0.125d)
- 覆盖:
<script>alert(1)</script>→ 渲染为文本javascript:alert(1)→ 链接被剥离<img src=x onerror=alert(1)>→ onerror 剥离<svg onload=alert(1)>→ onload 剥离
ST-S2-A-16.2 写合法 Markdown 渲染测试(0.0625d → 0d)
- 覆盖:H1-H3 / 列表 / 链接 / 代码块 / 表格
ST-S2-A-16.3 100% 覆盖(0.0625d → 0d)
T-A-17 · 示范单测:FormCard(RHF 版 · V10 重写)(0.5d · 4 子任务)
V10 变化:FormCard 改用 React Hook Form + Zod,不再用 3-step 状态机手写
ST-S2-A-17.1 写 RHF schema 测试(0.125d)
- 覆盖:Zod schema 校验(分数/位次/选科)
ST-S2-A-17.2 写 step 1→2→3 guards 测试(0.125d)
- 覆盖:RHF
trigger()校验后再前进
ST-S2-A-17.3 写后退保留数据测试(0.125d)
ST-S2-A-17.4 100% 覆盖(0.125d)
T-A-18 · Playwright + Chromatic 配置(0.5d · 3 子任务)⚠️ V10 新增
ST-S2-A-18.1 Playwright config(0.125d)
- 产出:
playwright.config.ts(chromium / webkit / firefox + Vite preview server)
ST-S2-A-18.2 Chromatic 配置(0.125d)⚠️ V10 新增
- 产出:
chromatic.config.json+ GitHub Action - 关键:UI/交互层 1:1 验收的视觉基线工具
ST-S2-A-18.3 e2e 烟雾测试(0.25d)
- 产出:3 个 spec 文件占位
T-A-19 · 示范 e2e:theme.spec(0.5d · 3 子任务)
ST-S2-A-19.1 暗/亮/系统三主题切换(0.25d)
- 覆盖:点击切换 + localStorage 持久化 + 系统主题响应
ST-S2-A-19.2 SSR/CSR 一致性(0.125d)
- 覆盖:无 flash of unstyled content
ST-S2-A-19.3 1.2s 缓动断言(0.125d)
T-A-20 · 示范 e2e:navigation.spec(0.5d · 3 子任务)
ST-S2-A-20.1 桌面 3 栏布局(0.125d)
ST-S2-A-20.2 移动 48px 底部 Tab(0.125d)
- V10 不变量 L2:移动端 < 768px 单栏 + 48px 底部 Tab
ST-S2-A-20.3 1024px 断点切换(0.25d)
T-A-21 · OpenAPI Codegen + Zod schema 复用(1.0d · 5 子任务)⚠️ V10 加强
V10 变化:从单一类型生成 → 类型 + Zod schema 联合生成,业务层可用
ST-S2-A-21.1 安装 openapi-typescript + openapi-zod-client(0.125d)
pnpm --filter web add -D openapi-typescript openapi-zod-client
ST-S2-A-21.2 写 codegen 脚本(0.25d)
- 产出:
apps/web/scripts/codegen.tsimport { generateZodClientFromOpenAPI } from 'openapi-zod-client' import { OpenAPIV3 } from 'openapi-types' // 拉取后端 openapi.json // 生成 types/api.d.ts + schemas/api.ts
ST-S2-A-21.3 接入 Vite plugin 自动 codegen(0.25d)
- 产出:
vite.config.ts集成 codegen
ST-S2-A-21.4 验证生成文件 0 any(0.25d)
- 命令:
grep -r "any" apps/web/src/types/ - 验收:
- 0
any(这是 V10 重写带来的关键收益)
- 0
ST-S2-A-21.5 接入 CI 自动 codegen(0.125d)
- 产出:
pnpm codegen:check检查生成文件是否过期
T-A-22 · 示范调用:MSW mock server(0.5d · 4 子任务)
ST-S2-A-22.1 写 MSW handlers 骨架(0.125d)
ST-S2-A-22.2 写 5 端点 mock 数据(0.125d)
ST-S2-A-22.3 setup.ts 启动 server(0.125d)
ST-S2-A-22.4 验证 mock 数据驱动组件渲染(0.125d)
T-B-01 · Zustand store + 5 query hooks(chat · V10 重写)(1.0d · 5 子任务)⚠️ V10 全新
V10 关键:替代原型 7 个手写 hook 中的 5 个(chat 相关)
ST-S2-B-01.1 设计 Zustand store 4 slice(0.25d)
- 产出:
apps/web/src/stores/chat.tsuseChatStore:messages / isStreaming / mode / planuseFormStore:score / rank / subjectsuseUIStore:theme / sidebar / modaluseUserStore:user / preferences
- 类型:Zustand
create<T>()(devtools(persist(...))),0 any
ST-S2-B-01.2 写 useChatQuery(0.125d)
- 产出:
apps/web/src/hooks/queries/useChatQuery.ts - 封装:TanStack Query + Zod schema
ST-S2-B-01.3 写 useChatMutation(0.125d)
- 封装:
POST /api/chat/send+ 流式响应处理
ST-S2-B-01.4 写 useChatHistoryQuery(0.125d)
- 封装:
GET /api/chat/history
ST-S2-B-01.5 删除原型 7 个手写 hook(0.25d)⚠️ V10 收益
- 命令:
rm apps/web/src/lib/{useChat,useForm,useUI,useUser,...}.ts - 验收:
grep -r "useChat(" apps/web/src/返回 0 结果- 33
any中相关 5 个已清零
T-B-02 · TanStack Query 3 个 consultations 端点(0.5d · 3 子任务)
ST-S2-B-02.1 写 useConsultationsQuery(0.125d)
- 封装:
GET /api/consultations
ST-S2-B-02.2 写 useConsultationMutation(0.125d)
- 封装:
POST /api/consultations
ST-S2-B-02.3 写 useConsultationUpdateMutation(0.125d)
- 封装:
PATCH /api/consultations/:id
T-B-03 · TanStack Query 4 个 plans 端点(0.5d · 3 子任务)
ST-S2-B-03.1 写 usePlansQuery(0.125d)
ST-S2-B-03.2 写 usePlanMutation(0.125d)
ST-S2-B-03.3 写 usePlanUpdateMutation + usePlanDeleteMutation(0.25d)
T-B-04 · RHF 重写 FormCard + assessment(1.0d · 5 子任务)⚠️ V10 重写
V10 变化:从受控 3-step 状态机 → RHF + Zod
ST-S2-B-04.1 写 Zod schema(0.25d)
- 产出:
apps/web/src/schemas/assessment.tsexport const AssessmentSchema = z.object({ province: z.string().min(1), score: z.number().int().min(0).max(750), rank: z.number().int().min(1), subjects: z.array(z.enum(['物理', '历史', ...])).min(1), })
ST-S2-B-04.2 写 RHF useAssessmentForm(0.25d)
- 封装:
useForm<AssessmentSchema>({ resolver: zodResolver(AssessmentSchema) }) - 3-step guards:
trigger()校验后再前进
ST-S2-B-04.3 重写 FormCard.tsx(0.25d)⚠️ V10 重写
- V10 收益:删除手写 3-step 状态机;
trigger('score')替代if (step === 1 && !score)分支 - 验收:
- UI/交互 1:1(V10 不变量 C3)
any数量从 8 → 0
ST-S2-B-04.4 重写 3 个 consultation 相关组件(0.125d)
ST-S2-B-04.5 删除原型 4 个手写 hook(0.125d)⚠️ V10 收益
T-B-05 · audit/upload 端点(V10 重写)(1.0d · 4 子任务)
ST-S2-B-05.1 写 useAuditQuery(0.125d)
ST-S2-B-05.2 写 useAuditSubmitMutation(0.25d)
ST-S2-B-05.3 写 useUploadMutation(0.5d)
- 封装:
multipart/form-data上传 + 进度回调 - V10 变化:用 TanStack Query 替代手写 fetch + AbortController
ST-S2-B-05.4 删除原型最后 2 个手写 hook(0.125d)⚠️ V10 收益
T-B-06 · Chromatic 视觉基线(V10 新增 · 0.5d · 3 子任务)⚠️ V10 关键
PM 决策(2026-07-03):Playwright 视觉回归 + Chromatic
ST-S2-B-06.1 接入 Chromatic CLI(0.125d)
pnpm --filter web add -D chromatic
ST-S2-B-06.2 截图 8 个核心页面(0.25d)
- 页面:Home / Plan Detail / Chat / Form / Share / Review / Settings / Admin
ST-S2-B-06.3 提交基线 + 接入 CI(0.125d)
- 产出:
pnpm chromatic提交基线 - CI:
.github/workflows/web-ci.yml添加 chromatic 步骤
T-B-07 · Vite build + bundle 体积验证(0.5d · 4 子任务)⚠️ V10 新增
V10 关键:从 Next.js 切到 Vite,必须验证 bundle 体积和构建时间
ST-S2-B-07.1 写 vite.config.ts(0.125d)
- 配置:手动 chunk split(vendor / ui / pages)+ 资源内联阈值
ST-S2-B-07.2 bundle 分析(0.125d)
- 命令:
pnpm build -- --analyze - 验收:
- vendor < 200KB gzip
- 业务代码 < 100KB gzip
- 总 bundle < 300KB gzip
ST-S2-B-07.3 构建时间验证(0.125d)
- 验收:
pnpm build< 30s
ST-S2-B-07.4 接入 turbo build(0.125d)
- 验收:
turbo run build全 monorepo < 60s
V10 验收清单(Sprint 2 G1 闸门)
G1 闸门(必须全部通过):
[ ] pnpm install 0 error
[ ] pnpm typecheck 0 error 0 any
[ ] pnpm lint 0 error 0 warning(V10 重写后归零)
[ ] pnpm test 100% 覆盖 ModeIndicator / SafeMarkdown / FormCard
[ ] pnpm test:e2e 3 spec 全绿
[ ] pnpm build bundle < 300KB gzip
[ ] pnpm chromatic 视觉基线 8 页全提交
[ ] OpenAPI codegen 0 any
[ ] 原型 7 个手写 hook 全部删除
[ ] ESLint 33 any + 16 unused 全部归零
V10 风险(Sprint 2 特有)
| ID | 风险 | 等级 | 缓解 |
|---|---|---|---|
| V10-S2-R1 | Vite 切框架导致 Next.js 特有能力(i18n routing)失效 | 中 | Sprint 4 处理(i18n 用自写 router) |
| V10-S2-R2 | Zustand persist 与 SSR 冲突 | 低 | 用 createJSONStorage(() => localStorage) + 客户端守卫 |
| V10-S2-R3 | TanStack Query 5 与 React 19 兼容性 | 低 | 锁定 5.59.x 稳定版 |
| V10-S2-R4 | RHF + Zod 类型推导慢 | 低 | 用 z.infer<typeof Schema> 而非泛型 |
V10 与后续 Sprint 衔接
- Sprint 3 直接基于 Sprint 2 的 Zustand store + TanStack Query 开发 5 个新模块
- Sprint 4 性能优化基线:Vite build 产物 + Chromatic diff 阈值
- Sprint 5-8 全部使用 RHF + Zod + Zustand 模式
Sprint 2 文档状态:V10 选项 B 重写完成,待启动 总工时:9.5 人天(V2 是 18 人天,节省 8.5d) 总子任务:56 个(V2 是 58 个) 下一步:PM 确认后启动 T-A-14