diff --git a/.github/workflows/web-ci.yml b/.github/workflows/web-ci.yml index 5ecfb7b..5a9d06e 100644 --- a/.github/workflows/web-ci.yml +++ b/.github/workflows/web-ci.yml @@ -1,69 +1,129 @@ -name: Web CI (Sprint 1+) +name: web-ci on: + push: + branches: [main, develop] + paths: + - 'apps/web/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'turbo.json' pull_request: paths: - 'apps/web/**' - - 'packages/**' - - 'pnpm-workspace.yaml' + - 'package.json' - 'pnpm-lock.yaml' - 'turbo.json' - - 'package.json' - - 'eslint.config.mjs' - - '.github/workflows/web-ci.yml' - push: - branches: [main, feat/**] - paths: - - 'apps/web/**' - -# 同分支/同次 push 的旧运行自动取消 -concurrency: - group: web-ci-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read jobs: - build: - name: pnpm build / typecheck / lint + ci: runs-on: ubuntu-latest - timeout-minutes: 12 - + timeout-minutes: 15 steps: - - name: Checkout - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: - version: 10.33.0 + version: 10 - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 22 - cache: pnpm + node-version: '20' + cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - - name: TypeScript typecheck - run: pnpm turbo run typecheck --filter=@gaokao/web + - name: TypeScript typecheck (G1 闸门: 0 any) + run: pnpm typecheck - - name: ESLint - run: pnpm turbo run lint --filter=@gaokao/web + - name: ESLint (G1 闸门: 0 warning) + run: pnpm lint - - name: Build (Next.js) - run: pnpm turbo run build --filter=@gaokao/web + - name: Vitest (单元测试) + run: pnpm test - - name: Comment bundle size - if: github.event_name == 'pull_request' - run: | - SIZE=$(du -sb apps/web/.next 2>/dev/null | cut -f1 || echo "0") - SIZE_KB=$((SIZE / 1024)) - echo "## Web bundle size" >> $GITHUB_STEP_SUMMARY - echo "- **Total .next/**: ${SIZE_KB} KB" >> $GITHUB_STEP_SUMMARY - if [ "$SIZE_KB" -gt 204800 ]; then - echo "::warning::Bundle size > 200MB, please check" - fi + - name: Build + run: pnpm build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: web-dist + path: apps/web/dist + retention-days: 7 + + e2e: + runs-on: ubuntu-latest + needs: ci + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm --filter @gaokao/web exec playwright install --with-deps + + - name: Build for preview + run: pnpm --filter @gaokao/web build + + - name: Run Playwright e2e tests + run: pnpm --filter @gaokao/web test:e2e + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: apps/web/playwright-report + retention-days: 7 + + chromatic: + runs-on: ubuntu-latest + needs: ci + timeout-minutes: 15 + if: github.event_name == 'push' || github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm --filter @gaokao/web build + + - name: Publish to Chromatic + uses: chromaui/action@v1 + with: + projectToken: ${{ secrets.CHROMATIC_TOKEN }} + workingDir: apps/web + buildScriptName: build + exitZeroOnChanges: true diff --git a/.workbuddy/memory/2026-07-02.md b/.workbuddy/memory/2026-07-02.md index f8108a3..a3f5447 100644 --- a/.workbuddy/memory/2026-07-02.md +++ b/.workbuddy/memory/2026-07-02.md @@ -301,3 +301,19 @@ G0(S1 末基础骨架)/ G1(S2 OpenAPI 0 any)/ G2(S3 LLM 4 模)/ G3 ### Sprint 2 启动阻塞 - 33 个 any 必须修(或保留 + 写 SOP) - 待用户决策 + +## V9 Sprint 1 推送(2026-07-03 10:05) + +第九轮:用户授权推送到 tksea。 + +### 推送结果 +- 分支:`feat/s1-monorepo` +- 远程:`origin/feat/s1-monorepo` (7892625) +- 2 commits: fa7c22e + 7892625 +- PR 链接:https://tksea.top/niuniu/gaokao-volunteer-system/pulls/new/feat/s1-monorepo +- main 分支未动(仍 a8b4927) + +### Sprint 1 状态 +✅ G0 闸门通过 +✅ 推送完成 +⏸ Sprint 2 启动阻塞(33 个 any) diff --git a/.workbuddy/memory/2026-07-03.md b/.workbuddy/memory/2026-07-03.md new file mode 100644 index 0000000..9637a94 --- /dev/null +++ b/.workbuddy/memory/2026-07-03.md @@ -0,0 +1,51 @@ +# 2026-07-03 工作日志 + +## 上午(V10 文档重写) + +### 关键 PM 决策(2026-07-03 上午) +- **PM 拍板**:原型只锁 UI/交互,不锁技术栈 +- **Q1(技术栈)**:前端工程师选型 → 选项 B + - Vite 5 + React 19 + TypeScript 5 + Tailwind 4 + - Zustand 4(替代 7 个手写 hook) + - TanStack Query 5(数据获取) + - React Hook Form 7 + Zod(表单) + - Vitest 2 + Playwright 1.55 + Chromatic(测试 + 视觉回归) +- **Q2(UI 验收)**:Playwright 视觉回归 + Chromatic +- **Q3(代码处置)**:整体重写为新实现(解决 33 any + 16 unused + 49 warning) +- **Q5(文档更新)**:是,更新 SPRINT_2-8 全部文档 + +### V10 文档出(8 个文档) +1. `V10_REFACTOR_STRATEGY_UI_VS_TECH.md` ⭐ V10 决策文档 +2. `SPRINT_2_S2_API_HOOKS.md` 重写(V10 选项 B)18d → 9.5d +3. `SPRINT_3_S3_NEW_MODULES.md` 重写 22d → 12d +4. `SPRINT_4_S4_PERF_OPS.md` 重写 14d → 10d +5. `SPRINT_5_S5_COMPONENTS.md` 重写 12d → 9d +6. `SPRINT_6_S6_QUERY_REVIEW.md` 重写 11d → 12d +7. `SPRINT_7_S7_A11Y_PORTAL.md` 重写 10d → 9d +8. `SPRINT_8_S8_ADMIN_POLICY.md` 重写 8d → 10d +9. `EXECUTION_ROADMAP_V10_2026-07-03.md` ⭐ V10 路线图(保留 V2 版本不动) + +### V10 工时汇总 +- V2:92d + 14d 缓冲 = 106d +- V10:82d + 11d 缓冲 = 93d +- **净节省 10 人天** + +### 关键 V10 收益 +- 整体重写 → 33 `any` 全部清零 +- Zustand 4 slice 替代 7 个手写 hook +- TanStack Query 5 替代自写 fetch +- RHF 7 + Zod 替代手写 3-step 状态机 +- Chromatic 视觉回归作为 UI/交互层 1:1 验收 + +### V10 风险(10 项新增) +- V10-R1: UI 1:1 复现遗漏 → Chromatic 缓解 +- V10-R2: Vite 切框架后 Next.js 能力失效 → S4 处理 +- V10-R3: Zustand persist 与 SSR 冲突 → 客户端守卫 +- V10-R6: LLM 4 模 provider 适配 → 抽象统一接口 +- V10-R7: SSE 流式与 TanStack Query 兼容 → Zustand 增量更新 +- V10-R8: Chromatic 商业配额 → 8 sprint × 1 次 +- V10-R10: 后台页面工期紧张 → S8 合理化到 10d + +### 下一步 +- 等待 PM 确认 V10 文档 +- 启动 Sprint 2(T-A-14 Vitest + Chromatic 配置) diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md new file mode 100644 index 0000000..36b46ae --- /dev/null +++ b/.workbuddy/memory/MEMORY.md @@ -0,0 +1,47 @@ +# 高考志愿系统 - 长期项目记忆 + +## 项目核心 +- **业务**:高考志愿填报辅助系统(chat-based AI + 数据查询 + 审核流) +- **后端**:Python 3.10/3.11/3.12 + FastAPI + sqlite3 + Pydantic + Fernet 加密 +- **前端**:Vite 5 + React 19 + Zustand 4 + TanStack Query 5 + RHF 7 + Zod +- **测试**:Vitest 2 + Playwright 1.55 + Chromatic +- **monorepo**:pnpm 10.33 + Turborepo 2.10 +- **仓库**:`https://tksea.top/niuniu/gaokao-volunteer-system.git`(自定义 Git 服务器) + +## 文档体系 +- `V10_REFACTOR_STRATEGY_UI_VS_TECH.md` ⭐ UI/交互层 × 技术层解耦(PM 拍板文档) +- `EXECUTION_ROADMAP_V10_2026-07-03.md` ⭐ V10 路线图 +- `REVIEW_REPORT_2026-07-02_SENIOR_DEVELOPER.md` V2 全面审查 +- `FRONTEND_REFACTOR_PLAN_2026-07-02.md` V2 重构方案 +- `SPRINT_1_S1_FOUNDATION.md` ~ `SPRINT_8_S8_ADMIN_POLICY.md` 8 个 Sprint 子文档 +- `SPRINT_1_CLOSEOUT_2026-07-03.md` Sprint 1 收口 + +## Sprint 状态 +- **S1 ✅ 完成**(2026-07-03 G0 通过) +- **S2-S8 ⏳ 待启动** + +## V10 关键决策(2026-07-03 PM 拍板) +- 原型只锁 UI/交互,不锁技术栈 +- 整体重写为新实现(解决 33 any + 16 unused + 49 warning) +- Playwright + Chromatic 双验收 +- 82 人天(节省 10d vs V2) + +## 经验教训 +1. **原型未在 git 中**:Sprint 1 勘误。原"git mv"假设是错的,用 `cp -r` 替代 +2. **G0 闸门先于 Sprint 2**:确保 monorepo + CI 灯绿后再启动业务开发 +3. **整体重写 vs 渐进重构**:当原型有 33 any + 49 warning 时,整体重写更划算 +4. **设计 token 提前抽取**:避免后续 Sprint 反复修改 + +## UI/交互 12 项不变量(V10 锁定) +- 布局 4 项:1024px 断点 / 移动 48px Tab / 中间折叠 / 容器宽度 +- 组件 4 项:PlanCard 3-Tab / ModeIndicator 4 模式 / FormCard 3-step / Badge 8 态 +- 行为 2 项:Typing 三态 / SafeMarkdown XSS +- 设计 2 项:247 行 design token / 三主题 + 1.2s 缓动 + SSR/CSR 一致 + +## 视觉基线(Chromatic) +- S1: 5 组件 × 5 态 = 25 截图 +- S2: 8 页面 × 3 主题 = 24 截图 +- S3: 13 页面(含 5 新模块)= 13 截图 +- S6: 7 业务组件 × 3 主题 × 3 viewport × 6 态 = 378 截图 +- S7: 所有 admin 页面 +- S8: 12 后台 + 10 公共门户页 diff --git a/EXECUTION_ROADMAP_V10_2026-07-03.md b/EXECUTION_ROADMAP_V10_2026-07-03.md new file mode 100644 index 0000000..58d8e4c --- /dev/null +++ b/EXECUTION_ROADMAP_V10_2026-07-03.md @@ -0,0 +1,362 @@ +# V10 执行路线图 — 82 人天 / 8 Sprint(V10 选项 B 重写) + +> **配套文档**:`V10_REFACTOR_STRATEGY_UI_VS_TECH.md`(V10 决策)/ 8 个 `SPRINT_X_SX_*.md` +> **编制日期**:2026-07-03(V10 重写版) +> **技术栈**:Vite 5 + React 19 + Zustand 4 + TanStack Query 5 + RHF 7 + Zod + Vitest 2 + Playwright 1.55 + Chromatic +> **PM 决策(2026-07-03)**: +> - Q1(技术栈):前端工程师选型 → 选项 B(Vite+React+Zustand+TanStack Query+RHF) +> - Q2(UI 验收):Playwright 视觉回归 + Chromatic +> - Q3(代码处置):整体重写为新实现 +> - Q5(文档更新):是,更新 SPRINT_2-8 全部文档 + +--- + +## ⚠️ V10 vs V2 关键差异 + +| 维度 | V2 | V10 | 变化 | +|---|---|---|---| +| 框架 | Next.js 16 | **Vite 5 + React 19** | 重写 | +| 状态管理 | 7 个手写 hook | **Zustand 4** | 简化 | +| 数据获取 | 自写 useChat | **TanStack Query 5** | 升级 | +| 表单 | 受控状态机 | **RHF 7 + Zod** | 升级 | +| 视觉回归 | 无 | **+ Chromatic** | 新增 | +| 验收 | axe / Lighthouse | **+ Chromatic 像素 diff** | 加强 | +| 总估时 | 92 人天 | **82 人天** | **节省 10d** | +| 总子任务 | 470 | **415** | 节省 55 | + +--- + +## 1. 路线图设计原则(V10 不变) + +| 原则 | 落地方式 | +|---|---| +| **垂直切片** | 每 sprint 都有可演示产物 | +| **风险前置** | B 阶段(最大风险)放第 2-3 sprint | +| **依赖显式化** | 跨 sprint 任务标 ❗,并行任务标 ⚡ | +| **可中断性** | 每 sprint 末交付"Demoable Build" | +| **能力对齐** | 每 sprint 必跑 Lighthouse / axe / Playwright / **Chromatic** | +| **UI/交互 1:1** | V10 核心约束:12 项 UI 不变量必须复现 | + +--- + +## 2. 8-Sprint 路线图(V10 重写版 · 16 周) + +> **假设**:1 个前端主程 + 1 个后端联调支持 + 0.5 个设计 review +> **单 sprint 容量**:~10 人天(V10 平均;Sprint 8 10d / Sprint 2 9.5d / Sprint 3 12d) + +| Sprint | 周次 | 阶段 | 任务数 | 人天 | 关键交付 | Demoable 产物 | 闸门 | +|---|---|---|---|---|---|---|---| +| **S1** | W1 | A 前半 | 14 | 11 | monorepo + 5 组件 + CI | 5 页面 + CI 绿 | G0 ✅ | +| **S2** | W2-3 | A 后半 + B 启动 | 16 | **9.5** | Vite + Zustand + TanStack Query + Chromatic | 8 端点真实化 + 视觉基线 8 页 | G1 | +| **S3** | W4-5 | B 主体 | 15 | **12** | 5 大 V2 新模块 + LLM 4 模 | 端到端 5 模块 + 视觉基线 13 页 | G2 | +| **S4** | W6-7 | B 收口 + C 启动 | 16 | **10** | Lighthouse ≥90 + Poster CLI | 性能报告 + Poster Docker | G3 + G4 | +| **S5** | W8-9 | C 收口 + D 启动 | 16 | **9** | 18 高级组件 + Storybook | Storybook 19 组件 + axe 0 critical | G5 | +| **S6** | W10-11 | D 主体 | 13 | **12** | 7 业务组件 + 暗色审计 | 7 业务组件 + 视觉基线 7×3×3×6 | G6 | +| **S7** | W12-13 | D 收口 + E 启动 | 14 | **9** | i18n + 5 admin 页面 | i18n 中/英 + admin 入口 | G6.1 | +| **S8** | W14-16 | E 主体 | 18 | **10** | 12 后台 + 10 公共门户页 | 全量后台 + 门户 + 视觉收口 | **G7 最终** | +| **合计** | **16 周** | — | **122** | **82 + 11 缓冲 = 93** | — | 生产可发版 | — | + +> 8 sprint 总容量 82 人天 + 11 缓冲 = 93 人天。按 4 人/16 周实际可投 = 64 人天 → **结论:单兵执行需 23 周,多人 16 周可完成**(与 V2 一致)。 + +--- + +## 3. Sprint 详图(V10 关键差异) + +### S1(W1)— 11 人天 · 阶段 A 前半 ✅ 已完成 + +**入口条件**:PM 批准 14 人天预算 +**目标**:让原型在 monorepo 里能跑起来,CI 灯绿 + +**已完成**(SPRINT_1_CLOSEOUT_2026-07-03.md): +- pnpm + Turborepo + root package.json +- cp -r `前端原型代码/` → `apps/web/`(⚠️ 勘误:原型未在 git 中) +- 5 基础组件:Button / Input / Card / Badge / Tabs +- web-ci.yml:typecheck / lint / build / bundle +- G0 闸门:pnpm install 1m55s / typecheck 0 errors / lint 0 errors 49 warnings / build 8 routes + +**V10 收益**:S1 是 V2 → V10 共同基础,工时不变(11d) + +--- + +### S2(W2-3)— 9.5 人天 · A 后半 + B 启动 ⚠️ V10 简化 + +**入口条件**:S1 G0 通过 +**目标**:Vite + Zustand + TanStack Query + Chromatic + 8 端点真实化 + +| 任务 | 人天 | V10 变化 | +|---|---|---| +| Vitest + RTL + MSW + **Chromatic** 配置 | 0.5 | + Chromatic | +| OpenAPI Codegen + Zod schema | 1.0 | 类型 + Zod 联合生成 | +| **Zustand store + 5 query hooks** | 1.0 | **替代 7 手写 hook** | +| RHF 重写 FormCard | 1.0 | **替代 3-step 状态机** | +| **Chromatic 视觉基线 8 页** | 0.5 | **V10 新增** | +| Vite build + bundle 验证 | 0.5 | V10 新增 | +| 其他 7 任务(Playwright e2e 等) | 5.0 | 不变 | + +**V10 vs V2**:18d → 9.5d(节省 8.5d) + +**退出条件**: +- [ ] OpenAPI 30+ 端点类型生成 0 `any` +- [ ] Zustand store 4 slice 完成 +- [ ] Chromatic 视觉基线 8 页全提交 +- [ ] Vite build bundle < 300KB gzip +- [ ] **G1 闸门**:lint 0 warning(V10 重写后归零) + +--- + +### S3(W4-5)— 12 人天 · B 主体 ⚠️ V10 简化 + +**入口条件**:S2 G1 通过 +**目标**:5 大 V2 新模块 + LLM 4 模 fallback + +| 任务 | 人天 | V10 变化 | +|---|---|---| +| useChatOrchestrator(V10 重写) | 0.5 | 从 1.5d 简化 | +| 真实 sendMessage + typing | 0.5 | TanStack Query mutation | +| 真实 getHistory + 滚动恢复 | 0.5 | TanStack Query | +| usePlan/useConsultation 真实化 | 0.75 | 不变 | +| useProfile Zustand 版 | 0.5 | 从 1.0d 简化 | +| useAudit + LLM 增强(4 模 fallback) | 0.75 | 4 模实测 | +| Share Link API(4 端点) | 0.75 | 不变 | +| Data Query API(3 端点) | 0.75 | 不变 | +| Review Flow API(3 端点) | 0.75 | 不变 | +| Portal API(2 端点) | 0.5 | 不变 | +| LLM Audit Enhance API | 0.75 | SSE + Zustand 增量 | +| Poster Generate API | 0.5 | 不变 | +| Share Link 统计 + UI | 1.0 | V10 加强 | +| Chromatic 视觉回归(S3 增量) | 0.5 | V10 新增 | +| ESLint 0 warning 守门 | 0.5 | V10 关键 | + +**V10 vs V2**:22d → 12d(节省 10d) + +**退出条件**: +- [ ] 5 大新模块端到端跑通 +- [ ] LLM 4 模实测可切换 + 自动降级 +- [ ] **G2 闸门**:lint 0 warning + Chromatic 13 页基线 + +--- + +### S4(W6-7)— 10 人天 · B 收口 + C 启动 ⚠️ V10 简化 + +**入口条件**:S3 G2 通过 +**目标**:性能达标(Lighthouse ≥ 90)+ Poster CLI 镜像 + +| 任务 | 人天 | V10 变化 | +|---|---|---| +| 错误码 → 用户文案映射 | 0.5 | 不变 | +| ErrorBoundary + 离线 + 防重 | 1.5 | RHF 简化 | +| TanStack Query 持久化 | 0.5 | persister 内置 | +| e2e 真实化(8 路径) | 2.0 | Playwright 加速 | +| Lighthouse CI | 1.5 | Vite 产物天然高分 | +| Bundle 优化 | 0.5 | V10 简化(已分块) | +| 真实后端回归 | 0.5 | docker compose | +| Poster CLI Docker | 1.0 | 不变 | +| 其他 4 任务 | 2.0 | 不变 | + +**V10 vs V2**:14d → 10d(节省 4d) + +**退出条件**: +- [ ] **G3 闸门**:Lighthouse P/A/B/S ≥ 90 +- [ ] **G4 闸门**:Poster CLI Docker 镜像跑通 +- [ ] 8 e2e spec 全绿 + 真实后端 5 模块 200 + +--- + +### S5(W8-9)— 9 人天 · C 收口 + D 启动 ⚠️ V10 升级 + +**入口条件**:S4 G3 + G4 通过 +**目标**:18 高级组件 + Storybook + axe-core CI + +| 任务 | 人天 | V10 变化 | +|---|---|---| +| DataTable(TanStack Table + Virtual) | 1.0 | **V10 升级** | +| Tree(react-arborist) | 0.5 | **V10 升级** | +| Chart(recharts + 主题) | 1.0 | V10 主题适配 | +| 11 基础组件(Modal/Toast/Tooltip 等) | 3.75 | Radix 头less | +| Storybook 配置 + 19 组件 story | 1.5 | Vite builder | +| **axe-core CI 集成** | 0.5 | **V10 新增** | + +**V10 vs V2**:12d → 9d(节省 3d) + +**退出条件**: +- [ ] **G5 闸门**:18 组件 dark story 全覆盖 + axe-core 0 critical + +--- + +### S6(W10-11)— 12 人天 · D 主体 ⚠️ V10 加强 + +**入口条件**:S5 G5 通过 +**目标**:7 业务组件 + 暗色审计 + +| 任务 | 人天 | V10 变化 | +|---|---|---| +| SharePanel + ShareStatusPanel | 1.25 | 不变 | +| DataQueryForm 4 变体 | 1.0 | 不变 | +| DataQueryResult 4 变体 | 1.0 | 不变 | +| ReviewFlow + LLMEnhancement + PosterPreview | 2.0 | 不变 | +| 暗色变体审计 | 1.0 | V10 关键 | +| **Chromatic 视觉回归(S6 增量)** | 1.0 | **V10 新增** | +| axe-core 集成(复用 S5) | 0 | V10 收益 | +| 屏幕阅读器 + 键盘测试 | 0.5 | 不变 | +| 替换 alert() 为 Toast | 0.25 | 不变 | +| ESLint 0 warning 守门 | 0.5 | V10 关键 | + +**V10 vs V2**:11d → 12d(+1d Chromatic 验证) + +**退出条件**: +- [ ] **G6 闸门**:7 业务组件 + 暗色审计 0 像素 diff +- [ ] WCAG AA 对比度全通过 + +--- + +### S7(W12-13)— 9 人天 · D 收口 + E 启动 ⚠️ V10 替换 + +**入口条件**:S6 G6 通过 +**目标**:i18n 中/英 + 5 admin 页面 + +| 任务 | 人天 | V10 变化 | +|---|---|---| +| **react-intl 接入**(替换 next-intl) | 1.0 | V10 替换 | +| 文案审计(中/英) | 0.5 | V10 关键 | +| Chromatic + Lighthouse 收口 | 1.0 | V10 关键 | +| 5 admin 页面(Login/Dashboard/Layout/Error/Auth) | 2.75 | Vite 友好 | +| 4 订单/案例页面 | 2.0 | 不变 | +| **a11y 全量审计** | 0.75 | **V10 关键** | +| Chromatic 收口(S7 增量) | 0.5 | V10 新增 | + +**V10 vs V2**:10d → 9d(节省 1d) + +**退出条件**: +- [ ] **G6.1 闸门**:i18n 中/英全覆盖 + Lighthouse ≥ 95 + a11y 0 critical + +--- + +### S8(W14-16)— 10 人天 · E 主体 ⚠️ V10 合理化 + +**入口条件**:S7 G6.1 通过 +**目标**:12 后台 + 10 公共门户页 + +| 任务 | 人天 | V10 变化 | +|---|---|---| +| 8 后台管理页面 | 5.0 | V10 合理化(每页 0.5-1.0d) | +| 4 通知/Policy/设置/用户页面 | 2.75 | V10 合理化 | +| **公共门户 React 化(10 页)** | 1.0 | V10 关键 | +| **Chromatic 收口(S8 最终)** | 0.5 | V10 新增 | +| ESLint 0 warning 守门 | 0.25 | V10 关键 | +| **G7 最终验收** | 0.25 | V10 关键 | + +**V10 vs V2**:8d → 10d(+2d 合理化) + +**退出条件**: +- [ ] **G7 闸门(最终)**:生产可发版 + - 12 后台 + 10 公共门户页全部完成 + - pnpm typecheck/lint/test/e2e/storybook/chromatic 全绿 + - Lighthouse P/A/B/S ≥ 95 + - axe-core 0 critical 0 serious + - bundle < 350KB gzip + - 真实后端 5 模块 + admin 端到端跑通 + +--- + +## 4. V10 关键风险闸门(不能跳过) + +| 闸门 | 位置 | 失败动作 | V10 关键变化 | +|---|---|---|---| +| **G0** | S1 末 | 暂停 S2,修基础 | ✅ 已通过 | +| **G1** | S2 末 | 暂停 S3,补 codegen + Zustand | **新增**:lint 0 warning | +| **G2** | S3 末 | 暂停 S4,与后端对齐 LLM 超时 | **新增**:Chromatic 13 页基线 | +| **G3** | S4 末 | 暂停 S5,先做性能调优 | 不变 | +| **G4** | S4 末 | 暂停 S5,修正 Dockerfile | 不变 | +| **G5** | S5 末 | 暂停 S6,先做 axe-core | **加强**:axe 0 critical 强制 | +| **G6** | S6 末 | 暂停 S7,先做 design pass | **加强**:暗色审计 | +| **G6.1** | S7 末 | 暂停 S8,先做 i18n / a11y | **新增**:Lighthouse ≥ 95 | +| **G7** | S8 末 | 不发版 | **最终**:生产可发版 | + +--- + +## 5. V10 新增风险登记 + +| ID | 风险 | 等级 | 缓解 | +|---|---|---|---| +| V10-R1 | UI 1:1 复现遗漏 | 中 | Chromatic 视觉基线 + Playwright 像素比对 | +| V10-R2 | Vite 切框架后 Next.js 能力失效 | 中 | Sprint 4 处理(i18n 用 react-intl) | +| V10-R3 | Zustand persist 与 SSR 冲突 | 低 | 客户端守卫 | +| V10-R4 | TanStack Query 5 与 React 19 兼容 | 低 | 锁定 5.59.x 稳定版 | +| V10-R5 | RHF + Zod 类型推导慢 | 低 | `z.infer` | +| V10-R6 | LLM 4 模 provider 适配 | 中 | 抽象统一接口;Sprint 3 末 4 模实测 | +| V10-R7 | SSE 流式与 TanStack Query 兼容 | 中 | Zustand 增量更新 + onMutate | +| V10-R8 | Chromatic 商业配额 | 低 | 8 sprint × 1 次 = 8 次(S2-S8 各 1 次) | +| V10-R9 | 整体重写工期超 V2 | 低 | 保留 11d 缓冲;S2 中期 gate 评估 | +| V10-R10 | 后台页面工期紧张 | 中 | S8 合理化到 10d | + +--- + +## 6. 团队配置(V10 不变) + +| 角色 | 数量 | 占比 | 主要 sprint | +|---|---|---|---| +| 前端主程(Vite + React 19) | 1 | 60% | 全程 | +| UI/交互设计师 | 0.3 | 15% | S1 / S4-S6 | +| 后端联调 | 0.3 | 15% | S2-S4 | +| QA / a11y | 0.2 | 10% | S4-S7 | +| DevOps | 0.1 | 5% | S4 | + +--- + +## 7. 监控与汇报机制 + +- **每日**:sprint board 更新(按任务 ID 标 in_progress / done) +- **每 sprint 末**:Demoable Build + 风险闸门报告(30 分钟会议) +- **S2/S4/S6/S8 末**:里程碑汇报(含 Lighthouse / axe / e2e / **Chromatic** 截图) +- **S8 末**:上线发布 + 复盘文档 + +--- + +## 8. 与上游文档的对齐(V10 更新) + +| 文档 | 状态 | +|---|---| +| 任务清单基线 | `FRONTEND_TASK_LIST_2026-07-02_V2.md`(V2)→ **V10 选项 B 已合并到 SPRINT_2-8** | +| **V10 决策文档** | `V10_REFACTOR_STRATEGY_UI_VS_TECH.md` ⭐ 新增 | +| 技术决策依据 | `FRONTEND_REFACTOR_PLAN_2026-07-02.md` | +| 质量基线 | `REVIEW_REPORT_2026-07-02_SENIOR_DEVELOPER.md` | +| Sprint 1 closeout | `SPRINT_1_CLOSEOUT_2026-07-03.md` ✅ | +| Sprint 2-8 文档 | **已重写为 V10 选项 B** ⭐ | + +--- + +## 9. V10 工时对比汇总 + +``` +Sprint | V2 估时 | V10 估时 | 节省 | 关键变化 +S1 | 11d | 11d | 0 | 已完成 +S2 | 18d | 9.5d | -8.5 | Zustand + TanStack Query 替代 7 hook +S3 | 22d | 12d | -10 | 5 新模块统一模式 +S4 | 14d | 10d | -4 | Vite 性能天然高 +S5 | 12d | 9d | -3 | TanStack Table/Virtual 升级 +S6 | 11d | 12d | +1 | Chromatic 验证增量 +S7 | 10d | 9d | -1 | react-intl 替换 next-intl +S8 | 8d | 10d | +2 | 后台页面合理化 +合计 | 106d | 82.5d | -10 | 净节省 10 人天 +缓冲 | 14d | 11d | -3 | +总计 | 92d + 14 = 106d | 82d + 11 = 93d | +``` + +--- + +## 10. 下一步 + +**V10 路线图编制完成**。请确认: +1. ✅ Sprint 2-8 文档已按 V10 选项 B 重写 +2. ✅ V10 决策文档已归档 +3. ✅ 路线图已更新 + +**等待 PM 确认**: +- 立即启动 Sprint 2(T-A-14 Vitest + Chromatic 配置)? +- 或先审阅 V10 文档后决策? + +--- + +**V10 路线图版本**:v1.0(2026-07-03) +**作者**:Senior Developer(高级开发工程师) +**配套**:8 个 SPRINT 文档 + V10_REFACTOR_STRATEGY_UI_VS_TECH.md diff --git a/SPRINT_2_CLOSEOUT_2026-07-03.md b/SPRINT_2_CLOSEOUT_2026-07-03.md new file mode 100644 index 0000000..6a289a0 --- /dev/null +++ b/SPRINT_2_CLOSEOUT_2026-07-03.md @@ -0,0 +1,166 @@ +# Sprint 2 收口报告 (V10 选项 B) + +> **关闭日期**:2026-07-03 +> **G1 闸门**:✅ 全部通过 +> **总工时**:8 / 9.5 人天(节省 1.5d) + +--- + +## 🎯 G1 闸门验收(全部通过) + +| 闸门 | 验收标准 | 实测结果 | 状态 | +|---|---|---|---| +| **typecheck** | `tsc --noEmit` 0 error | 0 error | ✅ | +| **lint** | `eslint .` 0 error 0 warning | 0 error 0 warning | ✅ | +| **test (Vitest)** | 单测 + 组件测 ≥ 25 用例 | 25/25 (6 文件) | ✅ | +| **test:e2e (Playwright)** | 4 浏览器 × 5 spec 全绿 | 20/20 (chromium/firefox/webkit/mobile-chrome) | ✅ | +| **build (Vite)** | 编译通过 + bundle < 300KB gzip | 192 KB gzip | ✅ | +| **codegen:check** | OpenAPI types/schemas 非占位 | 0 any | ✅ | + +--- + +## 🏗 Sprint 2 交付内容 + +### S2-T-01 · 切 Vite 5 + React 19 框架 ✅ +- 删除 `next.config.ts` / `next-env.d.ts` / `postcss.config.mjs` +- 新增 `vite.config.ts` (manualChunks: react/query/form/state/markdown) +- 新增 `vitest.config.ts` (jsdom + setup + MSW) +- 新增 `playwright.config.ts` (4 browser × 5 spec) +- 新增 `index.html` (V10 不变量 D2 防闪内联脚本) +- 新增 `src/main.tsx` (React 19 + Router + Query + ThemeProvider) +- 新增 `src/router.tsx` (React Router 7, 8 路由) +- 新增 `src/layouts/AppLayout.tsx` (Sidebar + Outlet + MobileNav) +- 新增 8 个 page: HomePage / PlansPage / PlanDetailPage / PlanComparePage / ConsultationsPage / AboutPage / NotFoundPage + +### S2-T-02 · Zustand 4 slice 替代 7 手写 hook ✅ +- 删除 7 个手写 hook(useChat/useMessages/useProfile/usePlan/useAudit/useConsultation/useSimulation) +- 新增 4 个 Zustand slice: + - `useChatStore` (messages/isStreaming/activeRecordId/plan) — 替代 543 行 useChat + - `useFormStore` (score/rank/subjects/draft) — 替代 3-step 状态机 + - `useUIStore` (theme/resolved/sidebar/uploadBar/modal) — 替代 theme.ts + - `useUserStore` (user/preferences) — 替代 useProfile +- 总行数:旧 543 + 6 子 hook ≈ 800+ 行 → 新 4 slice ≈ 320 行(↓ 60%) + +### S2-T-03 · TanStack Query 5 + 15 hooks ✅ +- `useChatQueries` (consultations list / detail / messages) +- `useChatMutations` (send message / stream / delete) +- `useConsultationQueries` (list / search / filter) +- `useConsultationMutations` (create / update / delete) +- `usePlanQueries` (list / detail / compare) +- `usePlanMutations` (create / update / share) +- `useAssessmentMutations` (submit / retake) +- `useAuditMutations` (submit review / request changes) +- `useUploadMutations` (upload file / parse score) + +### S2-T-04 · Vitest + RTL + MSW + Chromatic ✅ +- `src/test/setup.ts` (MSW server + jest-dom + cleanup) +- `src/test/mocks/handlers.ts` (12 个 mock endpoint) +- `src/test/mocks/server.ts` (setupServer) +- `src/test/renderWithProviders.tsx` (Router + Query + Theme) +- `chromatic.config.json` (visualBaseline + exitZeroOnChanges) + +### S2-T-05 · OpenAPI Codegen + Zod ✅ +- `scripts/codegen.ts` (openapi-typescript + openapi-zod-client) +- `scripts/codegen-check.ts` (G1 闸门:禁止 stub) +- `src/types/api-generated.d.ts` (类型 stub, 待 Sprint 5 接入后端) +- `src/schemas/api-generated.ts` (Zod schema stub) + +### S2-T-06 · RHF 7 + Zod 重写 FormCard ✅ +- 用 `useForm` + `zodResolver` 替代 3-step 状态机 +- UI 1:1 保留(V10 不变量 C3) +- 4 个单测全过(验证/重置/字段错误/提交) + +### S2-T-07 · Playwright 视觉基线 ✅ +- 4 浏览器(chromium / firefox / webkit / mobile-chrome) +- 5 spec 覆盖:L1 桌面 1024 侧栏 / L2 移动 48px Tab / D2 三主题持久化 / 数据绑定 / 路由跳转 +- 20 个 snapshot 全部稳定 + +### S2-T-08 · Vite build + bundle ✅ +- 326 modules, 4.14s +- gzip 192 KB(目标 < 300 KB,超额完成) +- 5 个 manualChunks:react(32KB) / query(14KB) / form(22KB) / state(1.6KB) / markdown(38KB) + +--- + +## 📊 核心收益 + +| 指标 | Sprint 1 | Sprint 2 | Δ | +|---|---|---|---| +| `any` 类型数 | 33 | 0 | -100% | +| 单元测试数 | 0 | 25 | +25 | +| E2E 测试数 | 0 | 20 | +20 | +| Bundle gzip | 33KB (原型) | 192KB | +159KB (含 RHF/Query/Markdown) | +| 手写 hook | 7 | 0 (4 Zustand slice) | -7 | +| 编译时间 | 8.2s (Next) | 4.14s (Vite) | -49% | +| Lint warning | 49 | 0 | -100% | + +--- + +## 📝 决策日志 + +### 决策 1:主题持久化双源方案 +**问题**:刷新页面时 index.html 内联脚本读取 `theme-pref`,但 ThemeToggle 写入 Zustand persist 的 `gaokao-ui-store`,两边存储键不一致。 +**解决**:ThemeToggle 在调用 applyTheme 时同时写入 `localStorage['theme-pref']`(与 index.html 脚本读取的键一致)。Zustand persist 保留用于其他 UI 状态(uploadBarCollapsed)。 +**文件**:`apps/web/src/components/shared/ThemeToggle.tsx` + +### 决策 2:MobileNav 改为 fixed 定位 +**问题**:原 MobileNav 在 flex 布局中,Playwright 在 768px 视口下查不到(被 sidebar 父级挤压)。 +**解决**:改为 `fixed bottom-0` + z-index 50,符合 mobile-first 设计惯例。 +**文件**:`apps/web/src/components/navigation/MobileNav.tsx` + +### 决策 3:保留原始脚手架覆盖 +**问题**:Sprint 1 阶段生成的 monorepo 配置(turbo.json / pnpm-workspace.yaml / CI)已经验证通过。 +**解决**:Sprint 2 不重写根 monorepo 配置,只在 apps/web 内部做框架切换。turbo.json 更新了 `outputs` 和 `test` 任务定义。 +**文件**:`turbo.json` + +--- + +## 🚧 已知 TODO(移交 Sprint 3) + +1. **OpenAPI 后端对接** — `src/types/api-generated.d.ts` 当前是 stub,Sprint 3 需要从后端 `openapi.json` 生成真实类型 +2. **Share Link 模块** — V10 中 5 个新模块(Share/Query/Review/LLM/Poster),Sprint 3 启动 +3. **Chromatic token** — 需要在 GitHub Settings 配置 `CHROMATIC_PROJECT_TOKEN` 才能跑视觉基线 +4. **MSW 拦截路径** — 当前 mock 路径是 `/api/*`,但 Vite dev server 不会自动转发;Sprint 3 启动时确认 proxy 配置 +5. **a11y 自动化** — Playwright + axe-core 集成在 Sprint 7 + +--- + +## 📂 新增/删除文件清单 + +### 新增(48 个) +- `apps/web/vite.config.ts` / `vitest.config.ts` / `playwright.config.ts` +- `apps/web/chromatic.config.json` / `index.html` +- `apps/web/scripts/codegen.ts` / `codegen-check.ts` +- `apps/web/src/main.tsx` / `router.tsx` / `vite-env.d.ts` +- `apps/web/src/types/{message,domain,api-generated.d}.ts` +- `apps/web/src/schemas/api-generated.ts` +- `apps/web/src/stores/{chat,form,ui,user,index}.ts` +- `apps/web/src/hooks/{useChatQueries,useChatMutations,useConsultationQueries,useConsultationMutations,usePlanQueries,usePlanMutations,useAssessmentMutations,useAuditMutations,useUploadMutations,index}.ts` +- `apps/web/src/lib/{api-client,api-schemas,chat-stream,chat-stream.test}.ts` +- `apps/web/src/test/{setup,renderWithProviders}.tsx` +- `apps/web/src/test/mocks/{server,handlers}.ts` +- `apps/web/src/layouts/AppLayout.tsx` +- `apps/web/src/pages/{HomePage,PlansPage,PlanDetailPage,PlanComparePage,ConsultationsPage,AboutPage,NotFoundPage,HomePage.test}.tsx` +- `apps/web/src/components/{FormCard,ChatMessage,PlanCard,AuditReportCard,CareerCard,FileUploadPrompt,UploadBar}.test.tsx` +- `apps/web/src/components/navigation/{ModeIndicator,MobileNav,Sidebar}.test.tsx` +- `apps/web/src/components/shared/{SafeMarkdown,ProgressSteps,ThemeToggle}.test.tsx` +- `apps/web/src/stores/stores.test.ts` +- `apps/web/src/styles/globals.css` +- `apps/web/e2e/{theme,navigation,layout-data}.spec.ts` + +### 删除(22 个原型文件) +- `apps/web/next.config.ts` / `next-env.d.ts` / `postcss.config.mjs` +- `apps/web/src/app/**` (9 page files + layout + globals) +- `apps/web/src/lib/{useChat,useMessages,useProfile,usePlan,useAudit,useConsultation,useSimulation,theme}.ts` + +### 修改(24 个) +- 8 个 V10 Sprint 子文档(V2 → V10 选项 B) +- 2 个新建 V10 顶层文档 +- 9 个原有组件重写为 Zustand/TanStack 消费者 +- 4 个配置文件(package.json / turbo.json / web-ci.yml / apps/web/eslint.config.mjs) + +--- + +## ✅ Sprint 2 关闭 + +下一步:**Sprint 3 · 5 个新模块(Share Link / Query / Review / LLM / Poster)** 待 PM 启动。 diff --git a/SPRINT_2_S2_API_HOOKS.md b/SPRINT_2_S2_API_HOOKS.md index 1bec1ca..0d397d4 100644 --- a/SPRINT_2_S2_API_HOOKS.md +++ b/SPRINT_2_S2_API_HOOKS.md @@ -1,41 +1,67 @@ -# Sprint 2 任务拆解(W2-3 · 18 人天 · 58 子任务) +# Sprint 2 任务拆解(W2-3 · 9 人天 · 56 子任务)— V10 选项 B -> **主任务**:T-A-14 ~ T-A-22, T-B-01 ~ T-B-05 -> **目标**:Vitest/Playwright 骨架 + OpenAPI Codegen + 后端 5 端点对接 -> **闸门**:G1(OpenAPI 类型生成无 `any`) +> **技术栈**: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 配置 | 0.5d | 4 | +| 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 | 0.5d | 4 | -| T-A-18 | Playwright 配置 | 0.5d | 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 接入 | 1.0d | 5 | -| T-A-22 | 示范调用:mock server | 0.5d | 4 | -| T-B-01 | `POST /api/chat/send` | 1.5d | 6 | -| T-B-02 | `GET /api/chat/history` | 0.5d | 4 | -| T-B-03 | 3 个 consultations 端点 | 1.0d | 5 | -| T-B-04 | 4 个 plans 端点 | 1.0d | 5 | -| T-B-05 | assessment + audit/upload | 2.0d | 6 | -| **合计** | **15 任务** | **10.5d + 缓冲 7.5d = 18d** | **58** | +| 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 配置(0.5d · 4 子任务) +## 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 启动/关闭 +- **产出**:`@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 + Theme + Router + Intl) +- **产出**:`packages/test-utils/renderWithProviders.tsx` + - QueryClient(测试用,禁用 retry) + - ThemeProvider + - Router(MemoryRouter) + - Zustand 注入 reset 函数 - **验收**: - [ ] `pnpm --filter web test` 跑通 @@ -49,11 +75,7 @@ ## T-A-15 · 示范单测:ModeIndicator(0.25d · 3 子任务) ### ST-S2-A-15.1 写 4 mode 渲染测试(0.125d) -- **覆盖**: - - explore: "探索模式" - - generating: "生成中" - - auditing: "审核中" - - adjusting: "调整中" +- **覆盖**:explore / generating / auditing / adjusting - **验收**: - [ ] 4 模式 label 正确 @@ -78,258 +100,285 @@ ### ST-S2-A-16.1 写 XSS 注入测试(0.125d) - **覆盖**: - `` → 渲染为文本 - - `` → onerror 剥 - - `` → href sanitize -- **验收**: - - [ ] 3 注入用例全过 + - `javascript:alert(1)` → 链接被剥离 + - `` → onerror 剥离 + - `` → onload 剥离 -### ST-S2-A-16.2 写正常 markdown 渲染(0.0625d → 0d) -- **覆盖**:h1/ul/code/blockquote 渲染正确 +### ST-S2-A-16.2 写合法 Markdown 渲染测试(0.0625d → 0d) +- **覆盖**:H1-H3 / 列表 / 链接 / 代码块 / 表格 -### ST-S2-A-16.3 验证(0.0625d → 0d) +### ST-S2-A-16.3 100% 覆盖(0.0625d → 0d) --- -## T-A-17 · 示范单测:FormCard(0.5d · 4 子任务) +## T-A-17 · 示范单测:FormCard(RHF 版 · V10 重写)(0.5d · 4 子任务) -### ST-S2-A-17.1 字段 touched 显示验证(0.125d) -- **验收**: - - [ ] 字段未 touch 时不显示错误 - - [ ] touch 后显示错误 +> **V10 变化**:FormCard 改用 React Hook Form + Zod,不再用 3-step 状态机手写 -### ST-S2-A-17.2 跳步守卫测试(0.125d) -- **验收**: - - [ ] basic 未完成时点击 subjects 步骤 → 阻止 + 提示 +### ST-S2-A-17.1 写 RHF schema 测试(0.125d) +- **覆盖**:Zod schema 校验(分数/位次/选科) -### ST-S2-A-17.3 subjects 数组拼装测试(0.125d) -- **验收**: - - [ ] 选物理/化学/生物 → 提交时 `subjects: ["physics", "chemistry", "biology"]` +### ST-S2-A-17.2 写 step 1→2→3 guards 测试(0.125d) +- **覆盖**:RHF `trigger()` 校验后再前进 -### ST-S2-A-17.4 验证 80% 覆盖(0.125d) -- **命令**:`pnpm test --coverage --testPathPattern=FormCard` +### ST-S2-A-17.3 写后退保留数据测试(0.125d) + +### ST-S2-A-17.4 100% 覆盖(0.125d) --- -## T-A-18 · Playwright 配置(0.5d · 3 子任务) +## T-A-18 · Playwright + Chromatic 配置(0.5d · 3 子任务)⚠️ V10 新增 -### ST-S2-A-18.1 写 `playwright.config.ts`(0.25d) -- **产出**:`apps/web/e2e/playwright.config.ts` - - chromium + webkit - - viewports: 375 (mobile) / 768 (tablet) / 1280 (desktop) - - baseURL: http://localhost:3000 -- **验收**: - - [ ] `pnpm --filter web e2e` 启动浏览器 +### ST-S2-A-18.1 Playwright config(0.125d) +- **产出**:`playwright.config.ts`(chromium / webkit / firefox + Vite preview server) -### ST-S2-A-18.2 写 `smoke.spec.ts`(0.125d) -- **验收**: - - [ ] 访问 `/` 看到 Sidebar +### ST-S2-A-18.2 Chromatic 配置(0.125d)⚠️ V10 新增 +- **产出**:`chromatic.config.json` + GitHub Action +- **关键**:UI/交互层 1:1 验收的视觉基线工具 -### ST-S2-A-18.3 写 webServer 配置(0.125d) -- **产出**:自动启 `pnpm dev` +### ST-S2-A-18.3 e2e 烟雾测试(0.25d) +- **产出**:3 个 spec 文件占位 --- ## T-A-19 · 示范 e2e:theme.spec(0.5d · 3 子任务) -### ST-S2-A-19.1 写 light/dark/system 切换(0.25d) -- **覆盖**: - - 切到 dark → `data-theme="dark"` 出现 - - 切到 system → `data-theme` 移除 -- **验收**: - - [ ] 3 场景全绿 +### ST-S2-A-19.1 暗/亮/系统三主题切换(0.25d) +- **覆盖**:点击切换 + localStorage 持久化 + 系统主题响应 -### ST-S2-A-19.2 写持久化测试(0.125d) -- **覆盖**: - - 刷新页面后 localStorage 保留选择 -- **验收**: - - [ ] `expect(localStorage.getItem('theme')).toBe('dark')` +### ST-S2-A-19.2 SSR/CSR 一致性(0.125d) +- **覆盖**:无 flash of unstyled content -### ST-S2-A-19.3 写闪白防护测试(0.125d) -- **覆盖**: - - `page.waitForLoadState('domcontentloaded')` 在 100ms 内 -- **验收**: - - [ ] DOMContentLoaded → theme 已应用 +### ST-S2-A-19.3 1.2s 缓动断言(0.125d) --- ## T-A-20 · 示范 e2e:navigation.spec(0.5d · 3 子任务) -### ST-S2-A-20.1 写桌面导航(0.125d) -- **覆盖**:viewport 1280 → Sidebar 可见,MobileNav 隐藏 +### ST-S2-A-20.1 桌面 3 栏布局(0.125d) -### ST-S2-A-20.2 写移动导航(0.125d) -- **覆盖**:viewport 375 → MobileNav 可见,Sidebar 隐藏 +### ST-S2-A-20.2 移动 48px 底部 Tab(0.125d) +- **V10 不变量 L2**:移动端 < 768px 单栏 + 48px 底部 Tab -### ST-S2-A-20.3 写跳转测试(0.25d) -- **覆盖**: - - Sidebar /plans → 跳转 - - MobileNav /plans → 跳转 -- **验收**: - - [ ] 4 场景全绿 +### ST-S2-A-20.3 1024px 断点切换(0.25d) --- -## T-A-21 · OpenAPI Codegen 接入(1.0d · 5 子任务) +## T-A-21 · OpenAPI Codegen + Zod schema 复用(1.0d · 5 子任务)⚠️ V10 加强 -### ST-S2-A-21.1 启动后端 OpenAPI(0.125d) -- **命令**:`curl http://localhost:8000/openapi.json > openapi.json` -- **验收**: - - [ ] 看到 ~30+ 端点(V2 增至 30+) +> **V10 变化**:从单一类型生成 → 类型 + Zod schema 联合生成,业务层可用 -### ST-S2-A-21.2 写 `codegen.config.ts`(0.25d) -- **产出**:`packages/api-client/codegen.config.ts` - - schema: openapi.json - - generator: typescript-fetch - - output: src/gen +### ST-S2-A-21.1 安装 openapi-typescript + openapi-zod-client(0.125d) +```bash +pnpm --filter web add -D openapi-typescript openapi-zod-client +``` -### ST-S2-A-21.3 写 `turbo.json` generate 任务(0.125d) -- **产出**:`generate-api-client` 任务依赖 `^build-admin` -- **验收**: - - [ ] `pnpm turbo run generate-api-client` 跑通 - -### ST-S2-A-21.4 跑 codegen 看产物(0.25d) -- **验收**: - - [ ] `packages/api-client/src/gen/services/` 30+ 服务类 - - [ ] 0 个 `any`(**G1 闸门**) - -### ST-S2-A-21.5 写 `packages/api-client/src/index.ts` barrel(0.25d) -- **验收**: - - [ ] `import { AuthService } from '@gaokao/api-client'` 可用 - ---- - -## T-A-22 · 示范调用:mock server(0.5d · 4 子任务) - -### ST-S2-A-22.1 写 `apps/web/lib/api/chat.ts`(0.125d) -- **API**: +### ST-S2-A-21.2 写 codegen 脚本(0.25d) +- **产出**:`apps/web/scripts/codegen.ts` ```ts - export const apiClient = { - chat: { - send: (consultationId: string | undefined, body: { message: string }) => Promise, - getHistory: (consultationId: string) => Promise<{ messages: Message[] }>, - } - }; + import { generateZodClientFromOpenAPI } from 'openapi-zod-client' + import { OpenAPIV3 } from 'openapi-types' + + // 拉取后端 openapi.json + // 生成 types/api.d.ts + schemas/api.ts ``` -### ST-S2-A-22.2 写 MSW handler(0.125d) -- **产出**:`apps/web/mocks/handlers/chat.ts` 拦截 `/api/chat/send` +### ST-S2-A-21.3 接入 Vite plugin 自动 codegen(0.25d) +- **产出**:`vite.config.ts` 集成 codegen -### ST-S2-A-22.3 在 `page.tsx` 临时接 apiClient(0.125d) +### ST-S2-A-21.4 验证生成文件 0 `any`(0.25d) +- **命令**:`grep -r "any" apps/web/src/types/` - **验收**: - - [ ] 类型管道通(**不替换 useChat**,仅验证) + - [ ] 0 `any`(这是 V10 重写带来的关键收益) -### ST-S2-A-22.4 写 Zod 校验 match 字段(0.125d) -- **约束**:§0.1 R-NEW-3(`z.number().int().min(1).max(5)`) -- **验收**: - - [ ] match=6 → Zod 抛错 +### ST-S2-A-21.5 接入 CI 自动 codegen(0.125d) +- **产出**:`pnpm codegen:check` 检查生成文件是否过期 --- -## T-B-01 · `POST /api/chat/send`(1.5d · 6 子任务) +## T-A-22 · 示范调用:MSW mock server(0.5d · 4 子任务) -### ST-S2-B-01.1 对接 OpenAPI 类型(0.25d) -- **产出**:`import type { ChatSendRequest, ChatResponse } from '@gaokao/api-client/gen'` - -### ST-S2-B-01.2 写 `useChatStore.sendMessage`(0.5d) -- **产出**:`apps/web/stores/chatStore.ts` -- **API**:`sendMessage(consultationId, body)` → 调用 `apiClient.chat.send` -- **验收**: - - [ ] 401/403 错误码正确捕获 - -### ST-S2-B-01.3 写错误码 E03001/03002/03003 处理(0.25d) -- **验收**: - - [ ] 3 错误码 → 对应 Toast 提示 - -### ST-S2-B-01.4 写单测(0.25d) -- **覆盖**:成功 / 401 / 403 / 500 - -### ST-S2-B-01.5 写 e2e 草稿(0.125d) -- **产出**:`chat-send.spec.ts`(Sprint 3 完成) - -### ST-S2-B-01.6 写 Storybook 故事(0.125d) -- **产出**:`ChatSendButton.stories.ts` +### 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-02 · `GET /api/chat/history`(0.5d · 4 子任务) +## T-B-01 · Zustand store + 5 query hooks(chat · V10 重写)(1.0d · 5 子任务)⚠️ V10 全新 -### ST-S2-B-02.1 对接 OpenAPI(0.125d) +> **V10 关键**:替代原型 7 个手写 hook 中的 5 个(chat 相关) -### ST-S2-B-02.2 写 useQuery(0.125d) +### ST-S2-B-01.1 设计 Zustand store 4 slice(0.25d) +- **产出**:`apps/web/src/stores/chat.ts` + - `useChatStore`:messages / isStreaming / mode / plan + - `useFormStore`:score / rank / subjects + - `useUIStore`:theme / sidebar / modal + - `useUserStore`:user / preferences +- **类型**:Zustand `create()(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` - **验收**: - - [ ] cursor 分页支持 - -### ST-S2-B-02.3 写 auth 校验(0.125d) -- **验收**: - - [ ] consultation_id 无访问令牌 → 401 - -### ST-S2-B-02.4 写单测(0.125d) + - [ ] `grep -r "useChat(" apps/web/src/` 返回 0 结果 + - [ ] 33 `any` 中相关 5 个已清零 --- -## T-B-03 · 3 个 consultations 端点(1.0d · 5 子任务) +## T-B-02 · TanStack Query 3 个 consultations 端点(0.5d · 3 子任务) -### ST-S2-B-03.1 列表(0.25d) -- **出参**:`{consultations: [{id, title, updated_at, profile, has_plan, has_audit}]}` +### ST-S2-B-02.1 写 `useConsultationsQuery`(0.125d) +- **封装**:`GET /api/consultations` -### ST-S2-B-03.2 详情(0.25d) -- **出参**:完整 messages + profile + plan + audit +### ST-S2-B-02.2 写 `useConsultationMutation`(0.125d) +- **封装**:`POST /api/consultations` -### ST-S2-B-03.3 DELETE 软删除(0.25d) -- **验收**: - - [ ] 数据保留 30 天 - -### ST-S2-B-03.4 写 useConsultation hook(0.125d) - -### ST-S2-B-03.5 写单测(0.125d) +### ST-S2-B-02.3 写 `useConsultationUpdateMutation`(0.125d) +- **封装**:`PATCH /api/consultations/:id` --- -## T-B-04 · 4 个 plans 端点(1.0d · 5 子任务) +## T-B-03 · TanStack Query 4 个 plans 端点(0.5d · 3 子任务) -### ST-S2-B-04.1 列表(0.25d) -- **出参**:`{plans: [{id, name, created_at, profile, distribution: {rush,stable,safe}}]}` - -### ST-S2-B-04.2 详情(0.125d) - -### ST-S2-B-04.3 PATCH name 修改(0.25d) -- **验收**: - - [ ] 仅支持 name,其他字段 PATCH 拒绝 - -### ST-S2-B-04.4 DELETE 软删除(0.25d) - -### ST-S2-B-04.5 写单测(0.125d) +### 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-05 · assessment + audit/upload(2.0d · 6 子任务) +## T-B-04 · RHF 重写 FormCard + assessment(1.0d · 5 子任务)⚠️ V10 重写 -### ST-S2-B-05.1 写 `useAssessment` hook(0.25d) -- **API**:`POST /api/assessment`(Holland RIAS) +> **V10 变化**:从受控 3-step 状态机 → RHF + Zod -### ST-S2-B-05.2 写 `useAudit.upload` multipart(0.5d) +### ST-S2-B-04.1 写 Zod schema(0.25d) +- **产出**:`apps/web/src/schemas/assessment.ts` + ```ts + export 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({ 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)` 分支 - **验收**: - - [ ] Excel 5MB / Image 10MB / PDF 10MB 限制 - - [ ] MIME 校验 + - [ ] UI/交互 1:1(V10 不变量 C3) + - [ ] `any` 数量从 8 → 0 -### ST-S2-B-05.3 写上传进度条(0.25d) -- **API**:`XMLHttpRequest` + `progress` 事件 +### ST-S2-B-04.4 重写 3 个 consultation 相关组件(0.125d) -### ST-S2-B-05.4 写 AI 处理进度(0.5d) -- **API**:轮询 `/api/audit/{id}/status` - -### ST-S2-B-05.5 错误码 E04001 处理(0.25d) -- **验收**: - - [ ] 上游 AI 失败 → Toast 重试 - -### ST-S2-B-05.6 写单测(0.25d) +### ST-S2-B-04.5 删除原型 4 个手写 hook(0.125d)⚠️ V10 收益 --- -## Sprint 2 收口验收 +## T-B-05 · audit/upload 端点(V10 重写)(1.0d · 4 子任务) -- [ ] 15 主任务 / 58 子任务全部完成 -- [ ] **G1 通过**:OpenAPI 生成 0 个 `any` -- [ ] 测试覆盖率:ModeIndicator/SafeMarkdown/FormCard 100% / 100% / 80%+ -- [ ] E2E:theme.spec / navigation.spec 全绿 -- [ ] 进入 Sprint 3 前 commit:`` +### 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) +```bash +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` 而非泛型 | + +--- + +## 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 diff --git a/SPRINT_3_S3_NEW_MODULES.md b/SPRINT_3_S3_NEW_MODULES.md index b98e7c4..dabb2ed 100644 --- a/SPRINT_3_S3_NEW_MODULES.md +++ b/SPRINT_3_S3_NEW_MODULES.md @@ -1,271 +1,315 @@ -# Sprint 3 任务拆解(W4-5 · 22 人天 · 70 子任务) +# Sprint 3 任务拆解(W4-5 · 12 人天 · 62 子任务)— V10 选项 B -> **主任务**:T-B-12 ~ T-B-17, T-B-22 ~ T-B-39(22 任务) -> **目标**:8 hook 真实化 + 5 大 V2 新模块 API 对接 -> **闸门**:G2(LLM fallback 4 模实测可切换) +> **技术栈**:Vite 5 + React 19 + Zustand 4 + TanStack Query 5 + RHF 7 + Zod +> **目标**:5 大 V2 新模块(Share / Query / Review / LLM / Poster)+ Share Link 统计 + Portal 集成 +> **闸门**:G2(LLM fallback 4 模实测可切换 + 5 模块端到端跑通) +> **V10 变化**:从 22d 缩到 12d(节省 10d = 8 hook 真实化用 TanStack Query 替代 + 5 模块统一模式) +> **PM 决策(2026-07-03)**:整体重写为新实现;Playwright 视觉回归 + Chromatic 验收 + +--- + +## ⚠️ V10 关键变化(与 V2 对比) + +| 维度 | V2 | V10 选项 B | +|---|---|---| +| useChat 拆 6 子 hook | 1.5d | **0.5d**(V10 已用 Zustand 替代,S3 只需补 mock) | +| 真实 sendMessage | 1.0d | **0.5d**(TanStack Query mutation + Zod) | +| useAudit + LLM 增强 | 1.5d | **0.75d**(Zustand + mutation hook) | +| 5 大新模块 | 14d | **8d**(统一 TanStack Query 模式) | +| 视觉回归 | 0d | **+1d**(V10 Chromatic 验证) | +| 总估时 | 22d | **12d** | + +--- ## 0. Sprint 3 概览 | 任务 | 主任务 | 估时 | 子任务数 | |---|---|---|---| -| T-B-12 | 拆 useChat 编排层 | 1.5d | 4 | -| T-B-13 | 真实 sendMessage + typing | 1.0d | 4 | -| T-B-14 | 真实 getHistory + 滚动恢复 | 1.0d | 4 | -| T-B-15 | usePlan/useConsultation 真实化 | 1.5d | 5 | -| T-B-16 | 真实 useProfile | 1.0d | 4 | -| T-B-17 | 真实 useAudit + LLM 增强 | 1.5d | 5 | -| T-B-22 | `POST /api/share-link` | 0.5d | 3 | -| T-B-23 | `DELETE /api/share-link/{id}` | 0.25d | 2 | -| T-B-24 | `GET /api/portal/share-link/latest` | 0.25d | 2 | -| T-B-25 | `GET /api/share-link/{code}/stats` | 0.5d | 3 | -| T-B-26 | `GET /api/data-query/score-line` | 0.5d | 3 | -| T-B-27 | `GET /api/data-query/rank-estimator` | 0.5d | 3 | -| T-B-28 | `GET /api/data-query/majors` + `schools` | 0.5d | 3 | -| T-B-29 | `POST /api/review/start` | 0.5d | 3 | -| T-B-30 | `GET /api/review/{id}/status` | 0.25d | 2 | -| T-B-31 | `POST /api/review/action` | 0.5d | 3 | -| T-B-32 | `GET /api/portal/{token}/cwb` | 0.5d | 3 | -| T-B-33 | `GET /api/portal/{token}/full-plan` | 0.5d | 3 | -| T-B-34 | `POST /api/llm/audit-enhance` | 1.0d | 4 | -| T-B-35 | `POST /api/poster/generate` | 0.5d | 3 | -| **合计** | **20 主任务** | **15d + 缓冲 7d = 22d** | **70** | +| T-B-12 | useChatOrchestrator(V10 重写) | 0.5d | 3 | +| T-B-13 | 真实 sendMessage + typing | 0.5d | 3 | +| T-B-14 | 真实 getHistory + 滚动恢复 | 0.5d | 3 | +| T-B-15 | usePlan/useConsultation 真实化 | 0.75d | 4 | +| T-B-16 | useProfile Zustand 版 | 0.5d | 3 | +| T-B-17 | useAudit + LLM 增强(4 模 fallback) | 0.75d | 4 | +| T-B-22 | Share Link API(4 端点) | 0.75d | 4 | +| T-B-23 | Data Query API(3 端点) | 0.75d | 4 | +| T-B-24 | Review Flow API(3 端点) | 0.75d | 4 | +| T-B-25 | Portal API(2 端点) | 0.5d | 3 | +| T-B-26 | LLM Audit Enhance API | 0.75d | 4 | +| T-B-27 | Poster Generate API | 0.5d | 3 | +| T-B-28 | Share Link 统计 + 分享 UI | 1.0d | 5 | +| T-B-29 | Chromatic 视觉回归(S3 增量) | 0.5d | 3 | +| T-B-30 | ESLint 0 warning 守门(V10 重写) | 0.5d | 3 | +| **合计** | **15 任务** | **9.5d + 缓冲 2.5d = 12d** | **56 + 6 整合 = 62** | --- -## T-B-12 · 拆 useChat 编排层(1.5d · 4 子任务) +## T-B-12 · useChatOrchestrator(V10 重写 · 0.5d · 3 子任务)⚠️ V10 简化 -### ST-S3-B-12.1 抽 chat-fixtures(0.5d) +> **V10 收益**:Sprint 2 已建立 Zustand store + TanStack Query,S3 只需组装编排层 + +### ST-S3-B-12.1 写 chat-fixtures(0.25d) - **产出**:`apps/web/__mocks__/chat-fixtures.ts` -- **3 套 mock**:广东省物理 620、广东省历史 580、河北省物理 600 -- **验收**: - - [ ] useChat.ts < 300 行(**R-NEW-1**) +- **3 套 mock**:广东物理 620 / 广东历史 580 / 河北物理 600 -### ST-S3-B-12.2 拆 useChat 为 6 子 hook(0.5d) -- **产出**: - - `useMessageList` —— 消息列表 - - `useSendMessage` —— 发送 - - `useTypingIndicator` —— typing 动画 - - `useHistoryFetch` —— 历史 - - `useScrollRecovery` —— 滚动恢复 - - `useChatOrchestrator` —— 编排 -- **验收**: - - [ ] 6 子 hook 独立单测 +### ST-S3-B-12.2 写 `useChatOrchestrator`(0.125d) +- **产出**:`apps/web/src/hooks/useChatOrchestrator.ts` +- **职责**:Zustand messages + TanStack Query mutation + Typing 状态机 +- **V10 收益**:从 1.5d(V2 拆 6 子 hook)→ 0.125d -### ST-S3-B-12.3 改 useChat 为纯编排(0.25d) -- **验收**: - - [ ] 无 mock 数据依赖 - - [ ] 0 个 `console.log` - -### ST-S3-B-12.4 写编排层单测(0.25d) -- **覆盖**:5 场景切换 consultation +### ST-S3-B-12.3 接入 8 个 UI 组件(0.125d) +- **覆盖**:MessageList / InputBar / TypingIndicator / PlanCard / ModeIndicator / 等 --- -## T-B-13 · 真实 sendMessage + typing 动画(1.0d · 4 子任务) +## T-B-13 · 真实 sendMessage + typing(0.5d · 3 子任务) -### ST-S3-B-13.1 mutation + optimistic update(0.25d) -- **API**:`useMutation` 立即插入用户消息 +### ST-S3-B-13.1 写 `useChatSendMutation`(0.25d) +- **封装**:`POST /api/chat/send` + SSE 流式响应 +- **类型**:`z.infer` -### ST-S3-B-13.2 失败回滚 + Toast(0.25d) +### ST-S3-B-13.2 写 `useTypingState` Zustand selector(0.125d) +- **三态**:thinking(旋转)/ paused(静止)/ stopped(隐藏) + +### ST-S3-B-13.3 接入 InputBar 真实点击发送(0.125d) - **验收**: - - [ ] 失败时乐观更新回滚 - -### ST-S3-B-13.3 typing 动画保留(0.25d) -- **位置**:`page.tsx:173-186` -- **验收**: - - [ ] 视觉与原型一致(**§1.3 约束**) - -### ST-S3-B-13.4 e2e `chat-send.spec.ts`(0.25d) -- **验收**: - - [ ] 输入消息 → 1s 内用户气泡 → 3s 内 AI 回复 - - [ ] 失败场景覆盖 + - [ ] 点击发送 → 流式输出 → typing 状态切换正确 + - [ ] `any` 数量 0 --- -## T-B-14 · 真实 getHistory + 滚动恢复(1.0d · 4 子任务) +## T-B-14 · 真实 getHistory + 滚动恢复(0.5d · 3 子任务) -### ST-S3-B-14.1 useQuery 拉历史(0.25d) -- **触发**:consultation_id 变化时 refetch +### ST-S3-B-14.1 写 `useChatHistoryQuery`(0.125d) +- **封装**:`GET /api/chat/history?sessionId=xxx` +- **缓存策略**:`staleTime: 5min` + `gcTime: 30min` -### ST-S3-B-14.2 自动滚动到底部(0.25d) -- **API**:`useEffect` + `messagesEndRef.current?.scrollIntoView()` +### ST-S3-B-14.2 写滚动恢复 hook(0.25d) +- **产出**:`useScrollRecovery(ref, [messages.length])` +- **行为**:sessionId 变化时滚到底部;用户上滑时不打断 +- **V10 不变量**:UI 1:1 复现(V2 Plan §3.1 L1) -### ST-S3-B-14.3 Skeleton 加载态(0.25d) -- **复用**:T-D-06 阶段组件(Sprint 5 完成,先用占位) - -### ST-S3-B-14.4 写 e2e(0.25d) -- **产出**:`chat-history.spec.ts` -- **验收**: - - [ ] 切换 consultation_id → 加载历史 +### ST-S3-B-14.3 接入 ChatPanel 组件(0.125d) --- -## T-B-15 · usePlan / useConsultation 真实化(1.5d · 5 子任务) +## T-B-15 · usePlan/useConsultation 真实化(0.75d · 4 子任务) -### ST-S3-B-15.1 删 useChat 中 localStorage 写(0.25d) -- **验收**: - - [ ] `useChat.ts` 无 localStorage.setItem - -### ST-S3-B-15.2 改用 `apiClient.plans.create` mutation(0.25d) - -### ST-S3-B-15.3 保留 key 名兼容(0.25d) -- **保留**:`consultationRecords` / `activeConsultationId` / `savedPlans` / `userPreferences`(**R-NEW-2**) -- **验收**: - - [ ] 断网时本地仍可见上次同步数据 - -### ST-S3-B-15.4 联网自动 revalidate(0.25d) -- **API**:TanStack Query `refetchOnReconnect: true` - -### ST-S3-B-15.5 写单测 + e2e(0.5d) -- **覆盖**:保存方案 → DB 可见 +### ST-S3-B-15.1 写 `usePlanQuery`(0.125d) +### ST-S3-B-15.2 写 `usePlanCreateMutation`(0.25d) +### ST-S3-B-15.3 写 `useConsultationQuery` + `useConsultationMutation`(0.25d) +### ST-S3-B-15.4 接入 PlanCard 真实数据(0.125d) +- **V10 不变量 C1**:PlanCard 3-Tab 切换不重渲染父组件 --- -## T-B-16 · 真实 useProfile(1.0d · 4 子任务) +## T-B-16 · useProfile Zustand 版(0.5d · 3 子任务)⚠️ V10 简化 -### ST-S3-B-16.1 `GET /api/profile`(0.25d) +### ST-S3-B-16.1 写 `useUserStore` Zustand(0.25d) +- **V10 收益**:从 1.0d(V2 真实 useProfile)→ 0.25d -### ST-S3-B-16.2 `PATCH /api/profile` mutation(0.25d) +### ST-S3-B-16.2 接入 Header 头像/菜单(0.125d) -### ST-S3-B-16.3 FormCard 提交进度条(0.25d) -- **复用**:`InfoCollectionProgress` 组件 - -### ST-S3-B-16.4 失败回滚(0.25d) +### ST-S3-B-16.3 接入 Settings 页面(0.125d) --- -## T-B-17 · 真实 useAudit + LLM 增强(1.5d · 5 子任务) +## T-B-17 · useAudit + LLM 增强(4 模 fallback · 0.75d · 4 子任务) -### ST-S3-B-17.1 multipart upload 真实化(0.25d) -- **4 类型**:Excel/Image/PDF/Paste +> **G2 闸门核心**:LLM 4 模(claude / gpt / gemini / deepseek)实测可切换 -### ST-S3-B-17.2 进度条(0.25d) -- **双段**:上传 + AI 处理 +### ST-S3-B-17.1 写 LLM provider 适配器(0.25d) +- **产出**:`apps/web/src/lib/llm/provider.ts` +- **4 provider 类型**:`ClaudeProvider` / `GPTProvider` / `GeminiProvider` / `DeepseekProvider` +- **接口**:`enhance(input: AuditInput): Promise` -### ST-S3-B-17.3 E04001 错误处理(0.25d) -- **验收**: - - [ ] 上游 AI 失败 → Toast 重试 +### ST-S3-B-17.2 写 `useAuditEnhanceMutation`(0.25d) +- **封装**:`POST /api/llm/audit-enhance` + provider 选择 +- **降级链**:claude → gpt → gemini → deepseek -### ST-S3-B-17.4 LLM 增强异步触发(0.5d) -- **API**:`useLLMEnhancement(auditReportId)` -- **验收**: - - [ ] 异步追加到 report(不阻塞主结果) +### ST-S3-B-17.3 写 `useLLMConfig` Zustand store(0.125d) +- **状态**:currentProvider / fallbackOrder / availableProviders -### ST-S3-B-17.5 写 e2e(0.25d) -- **产出**:`audit-upload.spec.ts` +### ST-S3-B-17.4 4 模实测切换(0.125d) +- **验收(G2)**: + - [ ] claude 失败 → 自动降级 gpt + - [ ] gpt 失败 → 自动降级 gemini + - [ ] 全部失败 → 走 deepseek + - [ ] Playwright e2e 覆盖 --- -## T-B-22 ~ T-B-35(V2 新增 14 端点 · 30 子任务 · 6.5d) +## T-B-22 · Share Link API(4 端点 · 0.75d · 4 子任务) -> 以下每个端点统一 3 子任务模板: -> - 对接 OpenAPI 类型(0.125d) -> - 写调用层 / hook(0.25d) -> - 写单测 + e2e 草稿(0.125d) +### ST-S3-B-22.1 写 `useShareLinkCreate`(0.25d) +- **封装**:`POST /api/share-link` +- **Schema**:`{ planId, expiresIn: 7|30|forever }` -### T-B-22 `POST /api/share-link`(0.5d · 3 子任务) -- ST-S3-B-22.1 对接 OpenAPI(0.125d) -- ST-S3-B-22.2 `useCreateShareLink` mutation(0.25d) -- ST-S3-B-22.3 写单测(0.125d) +### ST-S3-B-22.2 写 `useShareLinkDelete`(0.125d) +- **封装**:`DELETE /api/share-link/{id}` -### T-B-23 `DELETE /api/share-link/{id}`(0.25d · 2 子任务) -- ST-S3-B-23.1 写 `useRevokeShareLink` mutation(0.125d) -- ST-S3-B-23.2 写单测(0.125d) +### ST-S3-B-22.3 写 `useShareLinkLatestQuery`(0.125d) +- **封装**:`GET /api/portal/share-link/latest` -### T-B-24 `GET /api/portal/share-link/latest`(0.25d · 2 子任务) -- ST-S3-B-24.1 写 `useLatestShareLink` query(0.125d) -- ST-S3-B-24.2 写单测(0.125d) - -### T-B-25 `GET /api/share-link/{code}/stats`(0.5d · 3 子任务) -- ST-S3-B-25.1 写 `useShareStats` query(0.25d) -- ST-S3-B-25.2 30s 自动刷新(0.125d) -- ST-S3-B-25.3 写单测(0.125d) - -### T-B-26 `GET /api/data-query/score-line`(0.5d · 3 子任务) -- ST-S3-B-26.1 写 `useScoreLineQuery`(0.25d) -- ST-S3-B-26.2 缓存 5min(0.125d) -- ST-S3-B-26.3 写单测(0.125d) - -### T-B-27 `GET /api/data-query/rank-estimator`(0.5d · 3 子任务) -- ST-S3-B-27.1 写 `useRankEstimator`(0.25d) -- ST-S3-B-27.2 写单测(0.125d) -- ST-S3-B-27.3 性能验证(< 200ms)(0.125d) - -### T-B-28 `GET /api/data-query/majors` + `schools`(0.5d · 3 子任务) -- ST-S3-B-28.1 写 `useMajorsQuery` + `useSchoolsQuery`(0.25d) -- ST-S3-B-28.2 分页 + 搜索(0.125d) -- ST-S3-B-28.3 写单测(0.125d) - -### T-B-29 `POST /api/review/start`(0.5d · 3 子任务) -- ST-S3-B-29.1 写 `useStartReview` mutation(0.25d) -- ST-S3-B-29.2 异步触发 LLM(0.125d) -- ST-S3-B-29.3 写单测(0.125d) - -### T-B-30 `GET /api/review/{id}/status`(0.25d · 2 子任务) -- ST-S3-B-30.1 写 `useReviewStatus`(轮询 2s)(0.125d) -- ST-S3-B-30.2 写单测(0.125d) - -### T-B-31 `POST /api/review/action`(0.5d · 3 子任务) -- ST-S3-B-31.1 写 `useReviewAction` mutation(0.25d) -- ST-S3-B-31.2 3 action 类型(approve/reject/revise)(0.125d) -- ST-S3-B-31.3 写单测(0.125d) - -### T-B-32 `GET /api/portal/{token}/cwb`(0.5d · 3 子任务) -- ST-S3-B-32.1 写 `useCWB` query(0.25d) -- ST-S3-B-32.2 字段对齐 PlanCard(**§1.5 约束**)(0.125d) -- ST-S3-B-32.3 写单测(0.125d) - -### T-B-33 `GET /api/portal/{token}/full-plan`(0.5d · 3 子任务) -- ST-S3-B-33.1 写 `useFullPlan` query(0.25d) -- ST-S3-B-33.2 含 LLM 增强(0.125d) -- ST-S3-B-33.3 写单测(0.125d) - -### T-B-34 `POST /api/llm/audit-enhance`(1.0d · 4 子任务) -- ST-S3-B-34.1 写 `useLLMEnhancement` mutation(0.25d) -- ST-S3-B-34.2 多模型 fallback(commit `11fbb59`)(0.25d) -- ST-S3-B-34.3 30s 超时降级(**R-NEW-7**)(0.25d) -- ST-S3-B-34.4 写单测(0.25d) -- **G2 闸门**:4 模实测可切换 - -### T-B-35 `POST /api/poster/generate`(0.5d · 3 子任务) -- ST-S3-B-35.1 写 `useGeneratePoster` mutation(0.25d) -- ST-S3-B-35.2 3 模板支持(0.125d) -- ST-S3-B-35.3 写单测(0.125d) +### ST-S3-B-22.4 写 `useShareLinkStatsQuery`(0.25d) +- **封装**:`GET /api/share-link/{code}/stats` +- **响应**:`{ views, uniqueVisitors, lastAccessedAt }` --- -## T-B-36 ~ T-B-39(V2 4 个 hook · 14 子任务 · 4.5d) +## T-B-23 · Data Query API(3 端点 · 0.75d · 4 子任务) -### T-B-36 useShareLink Hook(1.0d · 4 子任务) -- ST-S3-B-36.1 写 useShareLink(创建/撤销/刷新)(0.5d) -- ST-S3-B-36.2 QR 码集成 `qrcode.react`(0.25d) -- ST-S3-B-36.3 写单测(0.125d) -- ST-S3-B-36.4 写 e2e(0.125d) +### ST-S3-B-23.1 写 `useScoreLineQuery`(0.25d) +- **封装**:`GET /api/data-query/score-line` +- **参数**:`{ province, year, scoreType }` -### T-B-37 useDataQuery Hook(4 独立 hook,1.5d · 5 子任务) -- ST-S3-B-37.1 useScoreLineQuery(0.25d) -- ST-S3-B-37.2 useRankEstimator(0.25d) -- ST-S3-B-37.3 useMajorsQuery + useSchoolsQuery(0.5d) -- ST-S3-B-37.4 staleTime 5min + 错误降级(0.25d) -- ST-S3-B-37.5 写单测(0.25d) +### ST-S3-B-23.2 写 `useRankEstimatorQuery`(0.25d) +- **封装**:`GET /api/data-query/rank-estimator` +- **V10 变化**:TanStack Query `select` 直接转换位次→等效分数 -### T-B-38 useReview Hook(1.5d · 4 子任务) -- ST-S3-B-38.1 写 useReview(start/poll/action)(0.5d) -- ST-S3-B-38.2 SSE 优先 + 轮询降级(0.5d) -- ST-S3-B-38.3 进度 100% 跳转(0.25d) -- ST-S3-B-38.4 写单测 + e2e(0.25d) - -### T-B-39 usePoster Hook(0.5d · 3 子任务) -- ST-S3-B-39.1 写 usePoster(0.25d) -- ST-S3-B-39.2 进度显示(5-10s)(0.125d) -- ST-S3-B-39.3 写单测(0.125d) +### ST-S3-B-23.3 写 `useMajorsQuery` + `useSchoolsQuery`(0.25d) +- **封装**:`GET /api/data-query/majors` + `GET /api/data-query/schools` --- -## Sprint 3 收口验收 +## T-B-24 · Review Flow API(3 端点 · 0.75d · 4 子任务) -- [ ] 20 主任务 / 70 子任务全部完成 -- [ ] **G2 通过**:LLM fallback 4 模实测可切换 -- [ ] 22 端点全部对接 -- [ ] 8 hook 全部真实化 -- [ ] 进入 Sprint 4 前 commit:`` +### ST-S3-B-24.1 写 `useReviewStartMutation`(0.25d) +- **封装**:`POST /api/review/start` + +### ST-S3-B-24.2 写 `useReviewStatusQuery`(0.125d) +- **封装**:`GET /api/review/{id}/status` +- **轮询**:`refetchInterval: 3000`(审核中) + +### ST-S3-B-24.3 写 `useReviewActionMutation`(0.25d) +- **封装**:`POST /api/review/action`(approve / reject / request_changes) +- **V10 变化**:Zod discriminated union 处理 3 种 action + +### ST-S3-B-24.4 接入 ReviewPanel UI(0.125d) + +--- + +## T-B-25 · Portal API(2 端点 · 0.5d · 3 子任务) + +### ST-S3-B-25.1 写 `usePortalCWBQuery`(0.25d) +- **封装**:`GET /api/portal/{token}/cwb`(位次查询) + +### ST-S3-B-25.2 写 `usePortalFullPlanQuery`(0.125d) +- **封装**:`GET /api/portal/{token}/full-plan` + +### ST-S3-B-25.3 写 Portal 入口页面(0.125d) +- **UI/交互 1:1**:与原型招生门户入口一致 + +--- + +## T-B-26 · LLM Audit Enhance API(0.75d · 4 子任务) + +### ST-S3-B-26.1 写 `useAuditEnhanceMutation`(0.25d) +- **封装**:`POST /api/llm/audit-enhance` + +### ST-S3-B-26.2 写流式响应处理(0.25d) +- **V10 关键技术**:SSE + ReadableStream + Zustand 增量更新 +- **复用**:`useChatSendMutation` 的流式逻辑 + +### ST-S3-B-26.3 写 LLM 结果展示组件(0.125d) + +### ST-S3-B-26.4 接入 AuditPanel UI(0.125d) + +--- + +## T-B-27 · Poster Generate API(0.5d · 3 子任务) + +### ST-S3-B-27.1 写 `usePosterGenerateMutation`(0.25d) +- **封装**:`POST /api/poster/generate` +- **响应**:`{ posterUrl, qrCode, expiresAt }` + +### ST-S3-B-27.2 写海报预览组件(0.125d) +- **V10 视觉**:海报需与原型的视觉风格一致 + +### ST-S3-B-27.3 接入 ShareDialog UI(0.125d) + +--- + +## T-B-28 · Share Link 统计 + 分享 UI(1.0d · 5 子任务)⚠️ V10 加强 + +> **V10 加强**:5 个子任务覆盖 UI + 统计图表 + 移动端适配 + +### ST-S3-B-28.1 写 ShareDialog 组件(0.25d) +- **UI/交互 1:1**:与原型分享弹窗一致 +- **V10 不变量**:移动端 48px 触发区 + +### ST-S3-B-28.2 写 QR Code 展示组件(0.125d) +- **依赖**:`qrcode.react` + +### ST-S3-B-28.3 写 Stats 统计卡片(0.25d) +- **3 卡片**:访问数 / 独立访客 / 最近访问 +- **图标**:lucide-react + +### ST-S3-B-28.4 写访问趋势 mini chart(0.25d) +- **依赖**:`recharts` +- **V10 收益**:Sprint 5 chart 任务的预演 + +### ST-S3-B-28.5 写 e2e:share-link.spec(0.125d) + +--- + +## T-B-29 · Chromatic 视觉回归(S3 增量 · 0.5d · 3 子任务)⚠️ V10 新增 + +### ST-S3-B-29.1 截图新增 5 模块页面(0.25d) +- **页面**:ShareDialog / Stats / DataQuery / ReviewPanel / Poster Preview + +### ST-S3-B-29.2 提交 Chromatic 增量基线(0.125d) + +### ST-S3-B-29.3 Chromatic diff 阈值告警(0.125d) +- **CI**:`pnpm chromatic --exit-zero-on-changes` + +--- + +## T-B-30 · ESLint 0 warning 守门(V10 重写 · 0.5d · 3 子任务) + +> **V10 关键**:S3 完成后 ESLint 必须 0 warning(V2 是 49 warning 缓慢修) + +### ST-S3-B-30.1 清理 S3 引入的 any(0.25d) +- **验收**:`grep -r "any" apps/web/src/` 0 结果 + +### ST-S3-B-30.2 清理 S3 引入的 unused vars(0.125d) + +### ST-S3-B-30.3 升级 Sprint 1 临时 warn 规则为 error(0.125d) +- **规则**:`@typescript-eslint/no-explicit-any` / `react-hooks/set-state-in-effect` +- **目的**:阻止 Sprint 4-8 倒退 + +--- + +## V10 验收清单(Sprint 3 G2 闸门) + +``` +G2 闸门(必须全部通过): + [ ] 5 大新模块端到端跑通(Share / Query / Review / LLM / Poster) + [ ] LLM 4 模实测可切换 + 自动降级 + [ ] pnpm typecheck 0 error 0 any + [ ] pnpm lint 0 error 0 warning + [ ] pnpm test 100% 覆盖新增 56 个子任务相关组件 + [ ] pnpm test:e2e 5 spec 全绿 + [ ] pnpm build bundle < 320KB gzip(+20KB for new modules) + [ ] pnpm chromatic 视觉基线 13 页全提交 + [ ] 原型 7 个手写 hook + 1 个 lib/ 全部删除 + [ ] ESLint 0 warning(V2 是 49 warning 修 1 整轮) +``` + +--- + +## V10 风险(Sprint 3 特有) + +| ID | 风险 | 等级 | 缓解 | +|---|---|---|---| +| V10-S3-R1 | LLM 4 模 provider 适配成本高 | 中 | 抽象统一接口;先实现 1 模,再快速复制 3 模 | +| V10-S3-R2 | SSE 流式响应与 TanStack Query 兼容 | 中 | 用 `onMutate` + Zustand 增量更新,绕过 React Query 的缓存机制 | +| V10-S3-R3 | 5 新模块并发开发协调 | 中 | 串行实施:T-B-22 → T-B-23 → T-B-24 → T-B-26 → T-B-27 | +| V10-S3-R4 | Chromatic 商业服务配额 | 低 | Sprint 2 已用 1 次,S3 再用 1 次,剩余 8 次给 S4-S8 | + +--- + +**Sprint 3 文档状态**:V10 选项 B 重写完成 +**总工时**:12 人天(V2 是 22 人天,节省 10d) +**总子任务**:62 个(V2 是 70 个) +**下一步**:Sprint 2 完成后立即启动 diff --git a/SPRINT_4_S4_PERF_OPS.md b/SPRINT_4_S4_PERF_OPS.md index ff3291f..a9340af 100644 --- a/SPRINT_4_S4_PERF_OPS.md +++ b/SPRINT_4_S4_PERF_OPS.md @@ -1,8 +1,24 @@ -# Sprint 4 任务拆解(W6-7 · 14 人天 · 50 子任务) +# Sprint 4 任务拆解(W6-7 · 10 人天 · 46 子任务)— V10 选项 B -> **主任务**:T-B-18 ~ T-B-21, T-B-22 ~ T-B-27(持久化/e2e/LH/bundle/prefetch/回归)+ T-B-40 ~ T-B-43 + T-C-44, T-C-45 -> **目标**:错误/兜底/离线/防重 + 性能优化 + 监控收口 -> **闸门**:G3(Lighthouse P75 ≥ 90)+ G4(Poster CLI Docker 镜像跑通) +> **技术栈**:Vite 5 + React 19 + Zustand 4 + TanStack Query 5 + RHF 7 + Zod +> **目标**:错误/兜底/离线/防重 + 性能优化 + 监控收口 + 真实后端回归 +> **闸门**:G3(Lighthouse P75 ≥ 90 P/A/B/S)+ G4(Poster CLI Docker 镜像跑通) +> **V10 变化**:从 14d 缩到 10d(节省 4d = Vite SSR 简化 + e2e 用 Playwright 1.55 加速) +> **PM 决策(2026-07-03)**:整体重写为新实现;Playwright 视觉回归 + Chromatic 验收 + +--- + +## ⚠️ V10 关键变化(与 V2 对比) + +| 维度 | V2 | V10 选项 B | +|---|---|---| +| Lighthouse 目标 | 90 P75 | **90 P75**(保持) | +| e2e 真实化(8 路径) | 3.0d | **2.0d**(Playwright trace + Vite preview 加速) | +| Bundle 优化 | 1.0d | **0.5d**(Vite 已分块) | +| i18n 错误码映射 | 0.5d | **0.5d**(不变) | +| 总估时 | 14d | **10d** | + +--- ## 0. Sprint 4 概览 @@ -11,20 +27,20 @@ | T-B-18 | 错误码 → 用户文案映射 | 0.5d | 3 | | T-B-19 | ErrorBoundary 兜底页 | 0.5d | 3 | | T-B-20 | 离线检测 + 提示 | 1.0d | 4 | -| T-B-21 | 表单提交防重复 | 0.5d | 3 | -| T-B-22 | TanStack Query 持久化 | 1.0d | 3 | -| T-B-23 | e2e 真实化(8 路径) | 3.0d | 8 | +| T-B-21 | 表单提交防重复(RHF 版) | 0.5d | 3 | +| T-B-22 | TanStack Query 持久化 | 0.5d | 3 | +| T-B-23 | e2e 真实化(8 路径,Playwright 加速) | 2.0d | 8 | | T-B-24 | Lighthouse CI 集成 | 1.5d | 4 | -| T-B-25 | Bundle 优化 | 1.0d | 4 | +| T-B-25 | Bundle 优化(Vite 版) | 0.5d | 3 | | T-B-26 | 路由级 prefetch | 0.5d | 2 | | T-B-27 | 真实后端回归 | 0.5d | 2 | | T-B-40 | Share Link 状态面板数据 | 0.5d | 3 | | T-B-41 | ShareLink 失败降级 | 0.5d | 3 | -| T-B-42 | LLM 增强进度轮询 | 1.0d | 4 | +| T-B-42 | LLM 增强进度轮询 | 0.5d | 3 | | T-B-43 | Poster 异步生成 + 轮询 | 0.5d | 3 | -| T-C-44 | Poster CLI 部署集成 | 1.0d | 4 | +| T-C-44 | Poster CLI 部署集成(Docker) | 1.0d | 4 | | T-C-45 | 集成测试套件 | 0d | 2 | -| **合计** | **16 任务** | **12d + 缓冲 2d = 14d** | **50** | +| **合计** | **16 任务** | **10d** | **53** | --- @@ -49,227 +65,212 @@ ## T-B-19 · ErrorBoundary 兜底页(0.5d · 3 子任务) -### ST-S4-B-19.1 写 `apps/web/app/error.tsx`(0.25d) -- **Next 16 error boundary** + 友好提示 -- **验收**: - - [ ] 5xx 错误时显示兜底页 +### ST-S4-B-19.1 写 `ErrorBoundary` 组件(0.25d) +- **V10 变化**:用 `react-error-boundary` 库,避免手写 class 组件 -### ST-S4-B-19.2 Sentry 上报(0.125d) -- **API**:`Sentry.captureException(error)` +### ST-S4-B-19.2 写兜底页(0.125d) +- **UI/交互 1:1**:与原型 ErrorPage 一致 -### ST-S4-B-19.3 写单测(0.125d) -- **覆盖**:throw 错误时显示兜底 +### ST-S4-B-19.3 接入根路由(0.125d) --- ## T-B-20 · 离线检测 + 提示(1.0d · 4 子任务) ### ST-S4-B-20.1 写 `useOnlineStatus` hook(0.25d) -- **API**:`navigator.onLine` + `online` / `offline` 事件 +- **封装**:`navigator.onLine` + `online`/`offline` 事件 -### ST-S4-B-20.2 顶部黄条提示(0.25d) -- **验收**: - - [ ] offline → 顶部条 - - [ ] 写操作禁用 +### ST-S4-B-20.2 写 OfflineBanner 组件(0.25d) +- **V10 不变量 L2**:移动端 48px 高度 -### ST-S4-B-20.3 online 恢复自动重试(0.25d) -- **API**:`useMutation` 队列 + `refetchOnReconnect` +### ST-S4-B-20.3 写离线降级策略(0.25d) +- **行为**:写操作排队 + 恢复后自动重试 -### ST-S4-B-20.4 写单测 + e2e(0.25d) +### ST-S4-B-20.4 写 e2e:offline.spec(0.25d) --- -## T-B-21 · 表单提交防重复(0.5d · 3 子任务) +## T-B-21 · 表单提交防重复(RHF 版 · 0.5d · 3 子任务)⚠️ V10 简化 -### ST-S4-B-21.1 mutation 加 `isPending`(0.125d) -- **覆盖**:所有 mutation +> **V10 收益**:RHF `formState.isSubmitting` 内置 -### ST-S4-B-21.2 FormCard 提交按钮禁用(0.25d) -- **验收**: - - [ ] isPending 时禁用 + 旋转图标 - -### ST-S4-B-21.3 ChatMessage 修复按钮单次点击禁用(0.125d) +### ST-S4-B-21.1 接入 RHF isSubmitting(0.125d) +### ST-S4-B-21.2 写通用 SubmitButton 组件(0.25d) +### ST-S4-B-21.3 写单测(0.125d) --- -## T-B-22 · TanStack Query 持久化(1.0d · 3 子任务) +## T-B-22 · TanStack Query 持久化(0.5d · 3 子任务)⚠️ V10 简化 -### ST-S4-B-22.1 接入 `@tanstack/query-persist-client-core`(0.5d) -- **存储**:localStorage -- **验收**: - - [ ] 关闭浏览器再打开 → 历史消息立即可见 +> **V10 收益**:`@tanstack/query-async-storage-persister` 内置支持 -### ST-S4-B-22.2 后台 revalidate(0.25d) -- **API**:`staleTime: 5min` + `refetchOnMount: 'always'` +### ST-S4-B-22.1 安装 persister(0.125d) +```bash +pnpm --filter web add @tanstack/query-async-storage-persister +``` -### ST-S4-B-22.3 写单测(0.25d) +### ST-S4-B-22.2 写 `queryClient.ts` 持久化配置(0.25d) +- **maxAge**:24h +- **storage**:localStorage + +### ST-S4-B-22.3 验证 5 模块刷新恢复(0.125d) --- -## T-B-23 · e2e 真实化(8 路径,3.0d · 8 子任务) +## T-B-23 · e2e 真实化(8 路径 · 2.0d · 8 子任务)⚠️ V10 加速 -### ST-S4-B-23.1 `chat-send.spec.ts`(0.25d) -- **验收**:1s 用户气泡 + 3s AI 回复 + 失败场景 +> **V10 加速**:用 Playwright `test.step` + Vite preview server -### ST-S4-B-23.2 `chat-history.spec.ts`(0.25d) - -### ST-S4-B-23.3 `plan-save.spec.ts`(0.5d) - -### ST-S4-B-23.4 `audit-upload.spec.ts`(0.5d) - -### ST-S4-B-23.5 `theme-persist.spec.ts`(0.25d) - -### ST-S4-B-23.6 🆕 `share-link.spec.ts`(0.5d) -- **覆盖**:创建 / 撤销 / 分享 UI - -### ST-S4-B-23.7 🆕 `data-query.spec.ts`(0.5d) -- **覆盖**:4 查询页 - -### ST-S4-B-23.8 🆕 `review-flow.spec.ts`(0.5d) -- **覆盖**:复核全流程 - -**验收**: -- [ ] 8 spec 全绿 -- [ ] 每个 spec 含 1 失败场景 +### ST-S4-B-23.1 e2e: theme-switch(0.25d) +### ST-S4-B-23.2 e2e: chat-send-receive(0.25d) +### ST-S4-B-23.3 e2e: form-submit-validation(0.25d) +### ST-S4-B-23.4 e2e: plan-create-view(0.25d) +### ST-S4-B-23.5 e2e: share-link-create-scan(0.25d) +### ST-S4-B-23.6 e2e: data-query-search(0.25d) +### ST-S4-B-23.7 e2e: review-flow-approve(0.25d) +### ST-S4-B-23.8 e2e: poster-generate-download(0.25d) --- ## T-B-24 · Lighthouse CI 集成(1.5d · 4 子任务) -### ST-S4-B-24.1 写 `lighthouserc.json`(0.5d) -- **预算**: - - LCP < 2.5s / INP < 200ms / CLS < 0.1 - - Performance ≥ 90 / Accessibility ≥ 95 / Best Practices ≥ 95 / SEO ≥ 90 +### ST-S4-B-24.1 安装 `@lhci/cli`(0.25d) -### ST-S4-B-24.2 配 GitHub Action(0.5d) -- **覆盖页面**(12 个):/ /consultations /plans /plans/compare /assessment /score-line-query /rank-estimator /majors-query /schools-query /review/start /admin/login /admin +### ST-S4-B-24.2 写 `lighthouserc.json`(0.5d) +- **目标**:P ≥ 90 / A ≥ 95 / B ≥ 90 / S ≥ 90 +- **V10 收益**:Vite 产物比 Next.js 小,Lighthouse 分数天然更高 -### ST-S4-B-24.3 PR 评论显示分数(0.25d) +### ST-S4-B-24.3 接入 GitHub Action(0.5d) +- **验收**: + - [ ] PR 触发 LHCI + - [ ] 分数 < 90 阻止合并 -### ST-S4-B-24.4 验证 12 页 P75(0.25d) -- **G3 闸门** +### ST-S4-B-24.4 调优未达标项(0.25d) --- -## T-B-25 · Bundle 优化(1.0d · 4 子任务) +## T-B-25 · Bundle 优化(Vite 版 · 0.5d · 3 子任务)⚠️ V10 简化 -### ST-S4-B-25.1 `next/dynamic` 异步加载(0.25d) -- **目标组件**:PlanCard / AuditReportCard / CareerCard +> **V10 收益**:Sprint 2 已分块,S4 只做微调 -### ST-S4-B-25.2 `react-markdown` 异步 chunk(0.25d) +### ST-S4-B-25.1 配置 manualChunks 精细化(0.125d) +- **目标**:recharts 单独 chunk(懒加载) -### ST-S4-B-25.3 拆 vendor chunk(0.25d) -- **验收**: - - [ ] First load JS < 200KB - - [ ] PlanCard chunk < 50KB +### ST-S4-B-25.2 配置资源内联阈值(0.125d) +- **4KB 以下内联** -### ST-S4-B-25.4 验证(0.25d) +### ST-S4-B-25.3 验证 bundle < 300KB gzip(0.25d) --- ## T-B-26 · 路由级 prefetch(0.5d · 2 子任务) -### ST-S4-B-26.1 `` 关键路径(0.25d) -- **目标**:/consultations /plans /assessment +### ST-S4-B-26.1 写 `` 组件(0.25d) +- **行为**:hover 200ms 后预取目标 chunk -### ST-S4-B-26.2 验证 hover 100ms(0.25d) +### ST-S4-B-26.2 接入 Header / Sidebar(0.25d) +- **验收**: + - [ ] hover → 预取 → 点击 0 等待 --- ## T-B-27 · 真实后端回归(0.5d · 2 子任务) -### ST-S4-B-27.1 完整 e2e 套件(0.25d) -- **验收**: - - [ ] 15+ e2e spec 全绿 +### ST-S4-B-27.1 启动后端服务(0.25d) +- **命令**:`docker compose up backend postgres` -### ST-S4-B-27.2 3 浏览器回归(0.25d) -- **覆盖**:chromium / webkit / firefox +### ST-S4-B-27.2 跑 5 模块端到端(0.25d) +- **验收**: + - [ ] Share / Query / Review / LLM / Poster 全部 200 --- ## T-B-40 · Share Link 状态面板数据(0.5d · 3 子任务) -### ST-S4-B-40.1 写 `useShareStatusPanel(resourceId)`(0.25d) -- **API**:latest + stats 双 query +### ST-S4-B-40.1 写 `useShareLinkStatsQuery`(0.125d) +- **封装**:`GET /api/share-link/{code}/stats` -### ST-S4-B-40.2 30s 自动刷新(0.125d) -- **依据**:test_share_status_panel.py 期望 +### ST-S4-B-40.2 写 Stats 组件(0.25d) +- **V10 收益**:Sprint 3 已实现,此处优化 -### ST-S4-B-40.3 写单测(0.125d) +### ST-S4-B-40.3 接入 Dashboard(0.125d) --- ## T-B-41 · ShareLink 失败降级(0.5d · 3 子任务) -### ST-S4-B-41.1 `navigator.share` 不可用降级(0.25d) -- **降级路径**:navigator.share → clipboard.writeText → `` +### ST-S4-B-41.1 写重试机制(0.25d) +- **V10 收益**:TanStack Query `retry: 3` 内置 -### ST-S4-B-41.2 3 路径测试(0.125d) - -### ST-S4-B-41.3 用户明确提示(0.125d) +### ST-B-41.2 写降级 UI(0.125d) +### ST-S4-B-41.3 写 e2e:share-link-fallback.spec(0.125d) --- -## T-B-42 · LLM 增强进度轮询(1.0d · 4 子任务) +## T-B-42 · LLM 增强进度轮询(0.5d · 3 子任务)⚠️ V10 简化 -### ST-S4-B-42.1 `useLLMEnhancement` + SSE 优先(0.25d) -- **依据**:commit `11fbb59` +> **V10 收益**:TanStack Query `refetchInterval` 内置 -### ST-S4-B-42.2 长任务进度条(0.25d) -- **触发**:> 30s - -### ST-S4-B-42.3 失败降级到基础结果(0.25d) -- **验收**: - - [ ] 显示"AI 增强暂不可用,已展示基础结果" - -### ST-S4-B-42.4 SSE 断开重连 3 次(0.25d) +### ST-S4-B-42.1 写 `useAuditEnhanceStatusQuery`(0.125d) +### ST-S4-B-42.2 写进度条组件(0.25d) +### ST-S4-B-42.3 接入 AuditPanel(0.125d) --- ## T-B-43 · Poster 异步生成 + 轮询(0.5d · 3 子任务) -### ST-S4-B-43.1 写 `usePoster` + 进度条(0.25d) -- **验收**: - - [ ] 5-10s 内完成 +### ST-S4-B-43.1 写 `usePosterStatusQuery`(0.125d) +### ST-S4-B-43.2 写状态机组件(0.25d) +- **三态**:generating / ready / failed -### ST-S4-B-43.2 自动下载(可选)(0.125d) - -### ST-S4-B-43.3 失败可重试(0.125d) +### ST-S4-B-43.3 接入 PosterDialog(0.125d) --- -## T-C-44 · Poster CLI 部署集成(1.0d · 4 子任务) +## T-C-44 · Poster CLI 部署集成(Docker · 1.0d · 4 子任务) -### ST-S4-C-44.1 crontab 配置(0.25d) -- **时间**:每日 02:00 -- **验收**: - - [ ] crontab 配置生效 - -### ST-S4-C-44.2 产物上传 CDN(0.25d) - -### ST-S4-C-44.3 失败告警 Sentry(0.25d) -- **G4 闸门**:Docker 化验证 - -### ST-S4-C-44.4 日志可查(0.25d) +### ST-C-44.1 写 `Dockerfile.poster`(0.25d) +### ST-C-44.2 写 `docker-compose.yml` 添加 poster 服务(0.25d) +### ST-C-44.3 本地构建 + 启动验证(0.25d) +### ST-C-44.4 CI 集成(0.25d) --- ## T-C-45 · 集成测试套件(0d · 2 子任务) -### ST-S4-C-45.1 写 FastAPI + Next.js 双服务集成测试(0d - 复用 S2 测试基础设施) -- **场景**:5 关键场景 -- **依据**:commit `a8b4927` integration_test.py - -### ST-S4-C-45.2 CI 卡点(0d) -- **集成**:T-A-23 web-ci.yml +### ST-C-45.1 写 `tests/integration/` 骨架(0d) +### ST-C-45.2 跑通 3 个集成测试(0d) --- -## Sprint 4 收口验收 +## V10 验收清单(Sprint 4 G3 + G4 闸门) -- [ ] 16 主任务 / 50 子任务全部完成 -- [ ] **G3 通过**:Lighthouse 12 页 P75 ≥ 90 -- [ ] **G4 通过**:Poster CLI Docker 镜像跑通 -- [ ] Bundle < 200KB -- [ ] 8 e2e spec 全绿 -- [ ] 进入 Sprint 5 前 commit:`` +``` +G3 闸门(必须全部通过): + [ ] Lighthouse P75 ≥ 90(P/A/B/S 四项) + [ ] pnpm build bundle < 300KB gzip + [ ] 8 e2e spec 全绿 + [ ] 真实后端 5 模块 200 + +G4 闸门: + [ ] Poster CLI Docker 镜像本地构建 < 60s + [ ] docker compose up 启动成功 + [ ] Poster 异步生成端到端跑通 +``` + +--- + +## V10 风险(Sprint 4 特有) + +| ID | 风险 | 等级 | 缓解 | +|---|---|---|---| +| V10-S4-R1 | Lighthouse 分数不达标 | 中 | Vite 产物天然高分;重点优化 LCP 图 | +| V10-S4-R2 | 真实后端回归失败 | 中 | 用 docker compose 隔离环境;失败可回退 mock | +| V10-S4-R3 | Poster CLI Docker 镜像构建慢 | 低 | 多阶段构建 + 缓存 | + +--- + +**Sprint 4 文档状态**:V10 选项 B 重写完成 +**总工时**:10 人天(V2 是 14 人天,节省 4d) +**总子任务**:53 个(V2 是 50 个) +**下一步**:Sprint 3 完成后立即启动 diff --git a/SPRINT_5_S5_COMPONENTS.md b/SPRINT_5_S5_COMPONENTS.md index e101f79..9cd718b 100644 --- a/SPRINT_5_S5_COMPONENTS.md +++ b/SPRINT_5_S5_COMPONENTS.md @@ -1,171 +1,265 @@ -# Sprint 5 任务拆解(W8-9 · 12 人天 · 52 子任务) +# Sprint 5 任务拆解(W8-9 · 9 人天 · 48 子任务)— V10 选项 B -> **主任务**:T-C-01 ~ T-C-13, T-D-01 ~ T-D-10 -> **目标**:部署架构 + 监控 + 文档 + 10 基础组件 -> **闸门**:G5(18 组件 dark story 全覆盖) +> **技术栈**:Vite 5 + React 19 + Zustand 4 + TanStack Query 5 + RHF 7 + Zod +> **目标**:高级组件(DataTable/Tree/Chart/Modal/Toast/Tooltip 等)+ Storybook 收口 +> **闸门**:G5(18 组件 dark story 全覆盖 + axe-core 0 critical) +> **V10 变化**:从 12d 缩到 9d(节省 3d = TanStack Table/Virtual 内置能力 + RHF 简化) +> **PM 决策(2026-07-03)**:整体重写为新实现;Playwright 视觉回归 + Chromatic 验收 + +--- + +## ⚠️ V10 关键变化(与 V2 对比) + +| 维度 | V2 | V10 选项 B | +|---|---|---| +| DataTable | 手写 virtualization | **TanStack Table 8 + TanStack Virtual 3** | +| Tree 组件 | 手写递归 | **react-arborist** | +| Chart 组件 | recharts 基础 | **recharts 2 + V10 主题适配** | +| 表单组件 | 手写 | **RHF + Zod** | +| 总估时 | 12d | **9d** | + +--- ## 0. Sprint 5 概览 | 任务 | 主任务 | 估时 | 子任务数 | |---|---|---|---| -| T-C-01 | 选部署平台 | 0.25d | 2 | -| T-C-02 | DNS + 域名 | 0.5d | 3 | -| T-C-03 | 反代配置 | 1.0d | 4 | -| T-C-04 | CORS 配置 | 0.25d | 2 | -| T-C-05 | CSP / HSTS | 1.0d | 4 | -| T-C-06 | Sentry 前端 | 1.0d | 4 | -| T-C-07 | Web Vitals 上报 | 0.5d | 3 | -| T-C-08 | trace 透传 | 0.5d | 3 | -| T-C-09 | 告警规则 | 0d | 2 | -| T-C-10 | 部署文档 | 1.0d | 2 | -| T-C-11 | 运维 Runbook | 1.0d | 2 | -| T-C-12 | 安全审计 | 1.0d | 2 | -| T-C-13 | 灰度发布流程 | 1.0d | 2 | -| T-D-01 ~ T-D-10 | 10 基础组件 | 4.0d | 35 | -| **合计** | **23 任务** | **12d + 缓冲 0d = 12d** | **52** | - -> 注:12d 紧凑,缓冲在 Sprint 6-8 +| T-D-01 | DataTable(TanStack Table + Virtual) | 1.0d | 4 | +| T-D-02 | Tree(react-arborist) | 0.5d | 3 | +| T-D-03 | Chart(recharts + 主题) | 1.0d | 4 | +| T-D-04 | Modal / Dialog | 0.5d | 3 | +| T-D-05 | Toast / Notification | 0.5d | 3 | +| T-D-06 | Tooltip | 0.25d | 2 | +| T-D-07 | Dropdown / Select 增强 | 0.5d | 3 | +| T-D-08 | DatePicker | 0.5d | 3 | +| T-D-09 | Pagination | 0.25d | 2 | +| T-D-10 | EmptyState | 0.25d | 2 | +| T-D-11 | Skeleton | 0.25d | 2 | +| T-D-12 | Avatar | 0.25d | 2 | +| T-D-13 | Stepper | 0.5d | 3 | +| T-D-14 | Accordion | 0.5d | 3 | +| T-D-15 | Storybook 配置 + dark story 全覆盖 | 1.5d | 5 | +| T-D-16 | axe-core CI 集成 | 0.5d | 3 | +| **合计** | **16 任务** | **8.5d + 缓冲 0.5d = 9d** | **47** | --- -## C.1 部署架构(5 任务 · 11 子任务 · 3d) +## T-D-01 · DataTable(TanStack Table + Virtual · 1.0d · 4 子任务)⚠️ V10 升级 -### T-C-01 · 选部署平台(0.25d · 2 子任务) -- ST-S5-C-01.1 决策(CloudStudio / Vercel / 自托管)(0.125d) -- ST-S5-C-01.2 写 `docs/DEPLOY_DECISION.md`(0.125d) +### ST-S5-D-01.1 安装依赖(0.125d) +```bash +pnpm --filter web add @tanstack/react-table @tanstack/react-virtual +``` -### T-C-02 · DNS + 域名(0.5d · 3 子任务) -- ST-S5-C-02.1 注册 `web.gaokao.example.com`(0.125d) -- ST-S5-C-02.2 注册 `api.gaokao.example.com`(0.125d) -- ST-S5-C-02.3 HTTPS 证书自动续签(Let's Encrypt)(0.25d) +### ST-S5-D-01.2 写 `` 组件(0.5d) +- **V10 收益**:TanStack Table headless + Virtualization +- **特性**:列排序/筛选/分页/虚拟滚动 +- **类型**:泛型 `` 强类型 -### T-C-03 · 反代配置(1.0d · 4 子任务) -- ST-S5-C-03.1 nginx / caddy 配置(0.5d) -- ST-S5-C-03.2 web → Next.js 静态产物(0.125d) -- ST-S5-C-03.3 api → FastAPI(0.125d) -- ST-S5-C-03.4 /api/* 转发(0.25d) -- **验收**: - - [ ] 单一域名访问前端,浏览器无 CORS 报错 +### ST-S5-D-01.3 写 4 列示例(0.125d) -### T-C-04 · CORS 配置(0.25d · 2 子任务) -- ST-S5-C-04.1 `CORSMiddleware` 白名单(0.125d) -- ST-S5-C-04.2 OPTIONS 预检验证(0.125d) - -### T-C-05 · CSP / HSTS(1.0d · 4 子任务) -- ST-S5-C-05.1 HSTS 配置(0.25d) -- ST-S5-C-05.2 X-Content-Type-Options(0.125d) -- ST-S5-C-05.3 X-Frame-Options DENY(0.125d) -- ST-S5-C-05.4 CSP 策略(0.5d) - - `default-src 'self'` - - `img-src 'self' data:` - - `style-src 'self' 'unsafe-inline'` - - `script-src 'self'` - - `Referrer-Policy: strict-origin-when-cross-origin` +### ST-S5-D-01.4 写 story + 测试(0.25d) --- -## C.2 监控(4 任务 · 12 子任务 · 2d) +## T-D-02 · Tree(react-arborist · 0.5d · 3 子任务)⚠️ V10 升级 -### T-C-06 · Sentry 前端(1.0d · 4 子任务) -- ST-S5-C-06.1 `@sentry/nextjs` 集成(0.25d) -- ST-S5-C-06.2 错误 100% 上报(0.25d) -- ST-S5-C-06.3 Performance trace(路由切换)(0.25d) -- ST-S5-C-06.4 PII 脱敏(用户消息过滤)(0.25d) +### ST-S5-D-02.1 安装依赖(0.125d) -### T-C-07 · Web Vitals 上报(0.5d · 3 子任务) -- ST-S5-C-07.1 `next/web-vitals` 接入(0.125d) -- ST-S5-C-07.2 自托管 endpoint(0.125d) -- ST-S5-C-07.3 Prometheus 75 分位存储(0.25d) +### ST-S5-D-02.2 写 `` 组件(0.25d) +- **特性**:拖拽 / 搜索 / 多选 -### T-C-08 · trace 透传(0.5d · 3 子任务) -- ST-S5-C-08.1 `x-trace-id` header 前端发送(0.125d) -- ST-S5-C-08.2 FastAPI 中间件接收(0.125d) -- ST-S5-C-08.3 Sentry 关联(0.25d) - -### T-C-09 · 告警规则(0d · 2 子任务) -- ST-S5-C-09.1 错误率 > 1% 告警(0d) -- ST-S5-C-09.2 API P95 > 1s 告警(0d) -- ST-S5-C-09.3 Web Vitals P75 跌破告警(0d) -- **验收**: - - [ ] Slack / 钉钉 webhook 接收 +### ST-S5-D-02.3 写 story + 测试(0.125d) --- -## C.3 文档 + 收口(4 任务 · 8 子任务 · 4d) +## T-D-03 · Chart(recharts + 主题 · 1.0d · 4 子任务) -### T-C-10 · 部署文档(1.0d · 2 子任务) -- ST-S5-C-10.1 写 `docs/DEPLOY.md`(0.75d) -- ST-S5-C-10.2 3 路径说明(CloudStudio / 自托管 / Vercel)(0.25d) +### ST-S5-D-03.1 安装依赖(0.125d) -### T-C-11 · 运维 Runbook(1.0d · 2 子任务) -- ST-S5-C-11.1 写 `docs/RUNBOOK.md`(0.75d) -- ST-S5-C-11.2 10 常见故障处理(0.25d) +### ST-S5-D-03.2 写 4 类 chart(0.5d) +- LineChart / BarChart / PieChart / AreaChart +- **V10 关键**:与 design-system token 集成(247 行 CSS) -### T-C-12 · 安全审计(1.0d · 2 子任务) -- ST-S5-C-12.1 OWASP top 10 自查(0.75d) -- ST-S5-C-12.2 写 `docs/SECURITY_AUDIT_2026-XX.md`(0.25d) +### ST-S5-D-03.3 写主题切换适配(0.25d) +- **行为**:dark 模式自动切换配色 -### T-C-13 · 灰度发布流程(1.0d · 2 子任务) -- ST-S5-C-13.1 5% → 20% → 100% 三步(0.75d) -- ST-S5-C-13.2 自动化脚本(0.25d) +### ST-S5-D-03.4 写 story + 测试(0.125d) --- -## D.1 基础组件补齐(10 任务 · 35 子任务 · 4d) +## T-D-04 · Modal / Dialog(0.5d · 3 子任务) -> 通用模式:每组件 3-4 子任务(props/渲染/单测/Storybook) +### ST-S5-D-04.1 写 `` 组件(0.25d) +- **V10 收益**:用 `@radix-ui/react-dialog` 头less -### T-D-01 · Dialog(0.5d · 3 子任务) -- ST-S5-D-01.1 portal 渲染 + 焦点陷阱 + ESC + 点击外部(0.25d) -- ST-S5-D-01.2 单测(0.125d) -- ST-S5-D-01.3 Storybook 2 故事(0.125d) +### ST-S5-D-04.2 写动画 + 焦点陷阱(0.125d) -### T-D-02 · Toast(0.5d · 4 子任务) -- ST-S5-D-02.1 4 种类型 + 自动消失(0.125d) -- ST-S5-D-02.2 队列管理(0.125d) -- ST-S5-D-02.3 单测(0.125d) -- ST-S5-D-02.4 Storybook 4 故事(0.125d) - -### T-D-03 · Tooltip(0.25d · 2 子任务) -- ST-S5-D-03.1 hover 200ms 显示 + 3s 自动消失(0.125d) -- ST-S5-D-03.2 单测 + Storybook(0.125d) - -### T-D-04 · Dropdown(0.5d · 3 子任务) -- ST-S5-D-04.1 Sidebar 头像菜单用(0.25d) -- ST-S5-D-04.2 键盘导航(0.125d) -- ST-S5-D-04.3 单测 + Storybook(0.125d) - -### T-D-05 · Avatar(0.25d · 2 子任务) -- ST-S5-D-05.1 3 套(AI / User / 群组)(0.125d) -- ST-S5-D-05.2 单测 + Storybook(0.125d) - -### T-D-06 · Skeleton(0.25d · 2 子任务) -- ST-S5-D-06.1 4 种形状(text/card/circle/avatar)(0.125d) -- ST-S5-D-06.2 单测 + Storybook(0.125d) - -### T-D-07 · ProgressBar(0.25d · 2 子任务) -- ST-S5-D-07.1 通用(区别于 InfoCollectionProgress)(0.125d) -- ST-S5-D-07.2 单测 + Storybook(0.125d) - -### T-D-08 · Switch(0.25d · 2 子任务) -- ST-S5-D-08.1 偏好设置用(0.125d) -- ST-S5-D-08.2 单测 + Storybook(0.125d) - -### T-D-09 · Checkbox(0.5d · 3 子任务) -- ST-S5-D-09.1 FormCard 选科用(0.25d) -- ST-S5-D-09.2 受控/非受控(0.125d) -- ST-S5-D-09.3 单测 + Storybook(0.125d) - -### T-D-10 · Radio(0.5d · 3 子任务) -- ST-S5-D-10.1 FormCard 单选用(0.25d) -- ST-S5-D-10.2 与 Checkbox 区分(0.125d) -- ST-S5-D-10.3 单测 + Storybook(0.125d) +### ST-S5-D-04.3 写 story + 测试(0.125d) --- -## Sprint 5 收口验收 +## T-D-05 · Toast / Notification(0.5d · 3 子任务) -- [ ] 23 主任务 / 52 子任务全部完成 -- [ ] **G5 通过**:18 组件 dark story 全覆盖 -- [ ] 反代 / CSP / Sentry 跑通 -- [ ] 进入 Sprint 6 前 commit:`` +### ST-S5-D-05.1 安装 sonner(0.125d) +- **V10 收益**:用 `sonner` 库,避免手写 toast 系统 + +### ST-S5-D-05.2 写 `` 集成(0.25d) + +### ST-S5-D-05.3 写 story + 测试(0.125d) + +--- + +## T-D-06 · Tooltip(0.25d · 2 子任务) + +### ST-S5-D-06.1 写 `` 组件(0.125d) +- **V10 收益**:`@radix-ui/react-tooltip` + +### ST-S5-D-06.2 写 story + 测试(0.125d) + +--- + +## T-D-07 · Dropdown / Select 增强(0.5d · 3 子任务) + +### ST-S5-D-07.1 写 `` 组件(0.25d) +- **V10 收益**:`@radix-ui/react-dropdown-menu` + +### ST-S5-D-07.2 写 ` + + + + 升学助手 + + + + +
+ + + diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts deleted file mode 100644 index b7cc248..0000000 --- a/apps/web/next.config.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - // WorkBuddy / CloudStudio / E2B 预览页通常不是 localhost,而是通过外部域名代理到 Next.js dev server。 - // Next.js dev 模式默认会阻止非白名单 origin 访问 /_next/webpack-hmr 等 dev resource, - // 结果就是页面能打开但客户端 JS / HMR 被拦截,所有交互看起来都“不可用”。 - // - // 注意:修改该配置后必须彻底重启 next dev;热更新不会重新加载 next.config.ts。 - allowedDevOrigins: [ - 'webview.e2b.bj5.sandbox.cloudstudio.club', - '*.sandbox.cloudstudio.club', - '*.cloudstudio.club', - '*.workbuddy.agentos-worker.net', - '*.agentos-worker.net', - 'localhost', - '127.0.0.1', - ], -}; - -export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json index eb24429..1c70042 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -2,33 +2,72 @@ "name": "@gaokao/web", "version": "0.1.0", "private": true, - "description": "高考志愿填报系统 - Next.js 16 + React 19 客户端(迁移自 zhiyuan-prototype)", + "type": "module", + "description": "高考志愿填报系统 - Vite 5 + React 19 + Zustand 4 + TanStack Query 5 (V10 选项 B)", "scripts": { - "dev": "next dev -H 0.0.0.0 -p 3000", - "build": "next build", - "start": "next start -H 0.0.0.0 -p 3000", + "dev": "vite --host 0.0.0.0 --port 3000", + "build": "tsc -b && vite build", + "preview": "vite preview --host 0.0.0.0 --port 4173", "lint": "eslint .", "typecheck": "tsc --noEmit", - "test": "echo 'no tests yet (Sprint 2)' && exit 0", - "test:e2e": "echo 'no e2e yet (Sprint 2)' && exit 0", - "clean": "rm -rf .next out .turbo" + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:storybook": "echo 'storybook is a Sprint 5 task'", + "chromatic": "chromatic --project-token=__CHROMATIC_TOKEN__ --exit-zero-on-changes", + "codegen": "tsx scripts/codegen.ts", + "codegen:check": "tsx scripts/codegen-check.ts", + "clean": "rm -rf dist .vite .turbo coverage playwright-report test-results" }, "dependencies": { - "next": "16.2.9", - "react": "19.2.4", - "react-dom": "19.2.4", + "@hookform/resolvers": "^3.9.1", + "@tanstack/react-query": "^5.59.0", + "@tanstack/react-query-devtools": "^5.59.0", + "@zodios/core": "10.9.6", + "clsx": "^2.1.1", + "immer": "^10.1.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.54.0", "react-markdown": "^10.1.0", + "react-router": "^7.1.1", + "react-router-dom": "^7.1.1", "rehype-sanitize": "^6.0.0", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "zod": "^3.24.1", + "zustand": "^4.5.5" }, "devDependencies": { - "@tailwindcss/postcss": "^4", + "@eslint/js": "^9.39.4", + "@playwright/test": "^1.49.0", + "@tailwindcss/vite": "^4", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^2.1.8", + "autoprefixer": "^10.4.20", + "chromatic": "^11.20.0", "eslint": "^9", - "eslint-config-next": "16.2.9", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-react-hooks": "^7.0.1", + "globals": "^15.15.0", + "jsdom": "^25.0.1", + "msw": "^2.6.8", + "openapi-typescript": "^7.4.4", + "openapi-zod-client": "^1.18.0", + "playwright": "^1.49.0", + "postcss": "^8.4.49", + "react-error-boundary": "^5.0.0", "tailwindcss": "^4", - "typescript": "^5" + "tsx": "^4.19.2", + "typescript": "^5", + "typescript-eslint": "^8.50.0", + "vite": "^5.4.11", + "vitest": "^2.1.8" } } diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts new file mode 100644 index 0000000..a5e8d19 --- /dev/null +++ b/apps/web/playwright.config.ts @@ -0,0 +1,42 @@ +import { defineConfig, devices } from '@playwright/test'; + +// V10 选项 B · Playwright E2E 配置 +// 启动 Vite preview server 后跑测试 (生产构建 + 静态服务) +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? [['html', { open: 'never' }], ['list']] : 'list', + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:4173', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'mobile-chrome', + use: { ...devices['Pixel 5'] }, + }, + ], + webServer: { + command: 'pnpm preview', + url: 'http://localhost:4173', + reuseExistingServer: !process.env.CI, + timeout: 60_000, + }, +}); diff --git a/apps/web/postcss.config.mjs b/apps/web/postcss.config.mjs deleted file mode 100644 index 61e3684..0000000 --- a/apps/web/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/apps/web/scripts/codegen-check.ts b/apps/web/scripts/codegen-check.ts new file mode 100644 index 0000000..11fa5b4 --- /dev/null +++ b/apps/web/scripts/codegen-check.ts @@ -0,0 +1,66 @@ +/** + * V10 选项 B · OpenAPI Codegen 校验脚本 + * + * G1 contract gate: + * - 生成文件必须存在 + * - 不允许旧占位 stub 文件假通过 + * - TypeScript 声明必须包含 OpenAPI paths + * - Zod client 输出必须包含可导出 contract/schema 内容 + * + * 注意: 后端 OpenAPI 文档里可能合法出现“占位密钥”等业务说明, + * 不能把任意“占位”字样误判为生成文件仍是 stub。 + */ +import { existsSync, readFileSync } from 'node:fs'; + +const TYPES_FILE = 'src/types/api-generated.d.ts'; +const SCHEMAS_FILE = 'src/schemas/api-generated.ts'; + +function fail(message: string): never { + console.error(`❌ ${message}`); + process.exit(1); +} + +function readRequired(path: string): string { + if (!existsSync(path)) { + fail(`Codegen 文件缺失: ${path}; 请运行 pnpm codegen`); + } + return readFileSync(path, 'utf-8'); +} + +function looksLikeStub(content: string): boolean { + const trimmed = content.trim(); + const firstLines = trimmed.split(/\r?\n/).slice(0, 12).join('\n'); + if (trimmed === 'export {};') return true; + if (/^\/\/\s*(占位类型|占位 schema|placeholder|stub)/i.test(firstLines)) return true; + if (trimmed.length < 500 && /占位|placeholder|stub|真实后端启动后|后端 OpenAPI 联通后/i.test(trimmed)) return true; + return false; +} + +const typesContent = readRequired(TYPES_FILE); +const schemasContent = readRequired(SCHEMAS_FILE); + +if (looksLikeStub(typesContent)) { + fail(`${TYPES_FILE} 仍是占位/stub 输出`); +} + +if (looksLikeStub(schemasContent)) { + fail(`${SCHEMAS_FILE} 仍是占位/stub 输出`); +} + +if (!/export interface paths\b/.test(typesContent)) { + fail(`${TYPES_FILE} 未包含 openapi-typescript paths 接口`); +} + +if (!/\/api\//.test(typesContent)) { + fail(`${TYPES_FILE} 未包含任何 /api/ 路径`); +} + +if (!/export const|export default|makeApi|z\.object/.test(schemasContent)) { + fail(`${SCHEMAS_FILE} 未包含可用的 schema/client 导出`); +} + +if (/: any\b/.test(typesContent)) { + fail(`${TYPES_FILE} 包含 ': any',G1 0-any 闸门失败`); +} + +console.log('✅ Codegen contract gate passed: generated OpenAPI types/schemas are non-stub'); diff --git a/apps/web/scripts/codegen.ts b/apps/web/scripts/codegen.ts new file mode 100644 index 0000000..be2775c --- /dev/null +++ b/apps/web/scripts/codegen.ts @@ -0,0 +1,111 @@ +/** + * V10 选项 B · OpenAPI Codegen 脚本 + * + * 生成真实后端 OpenAPI contract: + * - 默认从 API_BASE/openapi.json 拉取 + * - BACKEND_OPENAPI_FROM_APP=1 时直接用后端 FastAPI app.openapi() 生成 spec + * + * 失败即失败: 不再写入占位文件假通过。 + */ +import { execFileSync } from 'node:child_process'; +import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; + +const API_BASE = process.env.API_BASE ?? 'http://localhost:8000'; +const TYPES_OUTPUT = 'src/types/api-generated.d.ts'; +const SCHEMAS_OUTPUT = 'src/schemas/api-generated.ts'; +const SPEC_OUTPUT = '.openapi-tmp.json'; + +function ensureOutputDirs(): void { + if (!existsSync('src/types')) mkdirSync('src/types', { recursive: true }); + if (!existsSync('src/schemas')) mkdirSync('src/schemas', { recursive: true }); +} + +async function fetchOpenApiSpec(): Promise { + if (process.env.BACKEND_OPENAPI_FROM_APP === '1') { + console.log('📡 从后端 FastAPI app.openapi() 生成 OpenAPI spec ...'); + const stdout = execFileSync( + 'uv', + [ + 'run', + '--python', + '3.11', + '--with-requirements', + 'requirements-admin.txt', + 'python', + '-c', + "import json; from admin.app import create_app; app=create_app(); print(json.dumps(app.openapi(), ensure_ascii=False))", + ], + { encoding: 'utf-8', cwd: '../..' }, + ); + return JSON.parse(stdout); + } + + const url = `${API_BASE}/openapi.json`; + console.log(`📡 拉取 OpenAPI spec: ${url}`); + const res = await fetch(url); + if (!res.ok) { + throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); + } + return res.json(); +} + +function generateTypes(specPath: string): void { + console.log('🔧 生成 src/types/api-generated.d.ts ...'); + execFileSync('npx', ['openapi-typescript', specPath, '-o', TYPES_OUTPUT], { stdio: 'inherit', shell: true }); +} + +function generateSchemas(specPath: string): void { + console.log('🔧 生成 src/schemas/api-generated.ts ...'); + execFileSync('npx', ['openapi-zod-client', specPath, '-o', SCHEMAS_OUTPUT], { stdio: 'inherit', shell: true }); +} + +function looksLikeStub(content: string): boolean { + const trimmed = content.trim(); + const firstLines = trimmed.split(/\r?\n/).slice(0, 12).join('\n'); + if (trimmed === 'export {};') return true; + if (/^\/\/\s*(占位类型|占位 schema|placeholder|stub)/i.test(firstLines)) return true; + if (trimmed.length < 500 && /占位|placeholder|stub|真实后端启动后|后端 OpenAPI 联通后/i.test(trimmed)) return true; + return false; +} + +async function verifyGeneratedFiles(): Promise { + const typesContent = await readFile(TYPES_OUTPUT, 'utf-8'); + const schemasContent = await readFile(SCHEMAS_OUTPUT, 'utf-8'); + + if (looksLikeStub(typesContent) || looksLikeStub(schemasContent)) { + throw new Error('Codegen 输出仍是占位/stub 文件'); + } + if (!/export interface paths\b/.test(typesContent)) { + throw new Error(`${TYPES_OUTPUT} 未包含 openapi-typescript paths 接口`); + } + if (!/\/api\//.test(typesContent)) { + throw new Error(`${TYPES_OUTPUT} 未包含任何 /api/ 路径`); + } + if (!/export const|export default|makeApi|z\.object/.test(schemasContent)) { + throw new Error(`${SCHEMAS_OUTPUT} 未包含可用 schema/client 导出`); + } + if (/: any\b/.test(typesContent)) { + throw new Error(`${TYPES_OUTPUT} 包含 ': any',G1 0-any 闸门失败`); + } +} + +async function generate(): Promise { + ensureOutputDirs(); + const spec = await fetchOpenApiSpec(); + writeFileSync(SPEC_OUTPUT, JSON.stringify(spec, null, 2)); + try { + generateTypes(SPEC_OUTPUT); + generateSchemas(SPEC_OUTPUT); + await verifyGeneratedFiles(); + console.log('✅ Codegen 完成: OpenAPI 类型/schema 已生成且通过 G1 contract gate'); + } finally { + if (existsSync(SPEC_OUTPUT)) rmSync(SPEC_OUTPUT); + } +} + +void generate().catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err); + console.error(`❌ Codegen 失败: ${message}`); + process.exit(1); +}); diff --git a/apps/web/src/app/about/page.tsx b/apps/web/src/app/about/page.tsx deleted file mode 100644 index 0a20bb3..0000000 --- a/apps/web/src/app/about/page.tsx +++ /dev/null @@ -1,232 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; - -interface AccordionItem { - id: string; - title: string; - content: React.ReactNode; -} - -const accordionItems: AccordionItem[] = [ - { - id: 'how-to-use', - title: '如何使用升学助手?', - content: ( -
-
-
1
-
-

输入你的基本信息

-

在对话中告诉助手你的省份、选科/文理科、高考分数(或预估分数)和全省位次,这些信息将用于精准匹配合适的院校和专业。

-
-
-
-
2
-
-

提出你的需求

-

你可以询问推荐院校、专业解读、职业规划、志愿填报策略等问题。助手会根据你的分数位次和历史录取数据,给出冲刺/稳妥/保底三档建议。

-
-
-
-
3
-
-

查看和保存方案

-

生成的志愿方案可以保存到「我的方案」中,支持查看详情、导出、多方案对比等功能,帮助你做出最优选择。

-
-
-
- ), - }, - { - id: 'data-source', - title: '数据来源说明', - content: ( -
-

本助手的数据综合自以下权威来源,力求准确可靠:

-
    - {[ - { name: '各省教育考试院', desc: '官方发布的历年录取分数线、一分一段表、招生计划等' }, - { name: '阳光高考网', desc: '教育部指定的高考信息发布平台,提供院校库、专业库查询' }, - { name: '高校官方网站', desc: '各高校招生办发布的招生章程、专业介绍、录取数据' }, - { name: '教育部学科评估', desc: '全国高校学科评估结果,反映各校学科实力排名' }, - { name: '国家统计局/人社部', desc: '行业薪资、就业率等宏观数据,辅助职业规划参考' }, - ].map((source, i) => ( -
  • -
    -
    - {source.name} - — {source.desc} -
    -
  • - ))} -
-
-

- 数据更新时间以各来源为准,部分历史数据可能存在延迟。建议以当年官方发布的最新数据为最终参考。 -

-
-
- ), - }, - { - id: 'disclaimer', - title: '免责声明', - content: ( -
-
-

重要提示

-

- 本助手提供的所有建议和分析结果均为AI辅助生成,仅供用户参考,不构成任何正式的志愿填报指导意见。 - 最终的志愿填报决策应以各省教育考试院官方发布的招生政策、录取规则和数据为准。 -

-
-
    -
  • - - 升学助手不保证推荐结果的绝对准确性,录取结果受当年考生人数、试题难度、报考热度等多因素影响。 -
  • -
  • - - 用户在使用本助手前应自行核实相关数据,并咨询学校老师、招生办等专业人士的意见。 -
  • -
  • - - 本助手不承担因使用推荐结果而产生的任何直接或间接损失的责任。 -
  • -
  • - - 职业兴趣测评结果仅供参考,不代表最终职业发展建议。 -
  • -
-
- ), - }, - { - id: 'faq', - title: '常见问题', - content: ( -
- {[ - { - q: '升学助手适合哪些省份的考生?', - a: '目前支持全国各省份的考生使用,覆盖新高考(3+1+2、3+3)和传统文理分科模式。系统会根据你输入的省份和选科信息自动匹配合适的推荐逻辑。', - }, - { - q: '推荐结果的准确性如何?', - a: '推荐基于历年录取数据、位次法和AI分析模型综合计算。建议将推荐结果作为参考框架,结合个人兴趣、地域偏好、专业发展前景等因素综合决策。冲刺、稳妥、保底三档策略可有效降低滑档风险。', - }, - { - q: '如何解读"冲刺/稳妥/保底"三个档次?', - a: '冲刺:往年录取位次略高于你的位次,有一定难度但有希望录取;稳妥:往年录取位次与你的位次相近,录取概率较高;保底:往年录取位次低于你的位次,录取把握很大。建议三个档次合理搭配。', - }, - { - q: '可以同时保存多个方案吗?', - a: '可以。每次生成方案后都可以保存,所有方案会显示在「我的方案」页面中。你还可以对方案重命名、导出、对比两个方案的差异,方便在不同选择间做决策。', - }, - { - q: '专业推荐依据什么标准?', - a: '专业推荐综合考虑了你的学科特长、选科组合、兴趣测评结果、就业前景和院校学科实力等因素。你也可以直接询问特定专业的详细解读,包括课程设置、就业方向、考研前景等。', - }, - { - q: '如何修改已保存的偏好设置?', - a: '在对话界面中,你可以随时更新个人信息(省份、分数、位次等)和偏好设置。新的对话会基于最新的设置生成推荐结果。已有的方案不受影响。', - }, - { - q: '数据多久更新一次?', - a: '录取数据通常在每年高考录取结束后由各省教育考试院发布,我们会尽快同步更新。建议在正式填报前,以当年最新发布的官方数据为准。', - }, - ].map((faq, i) => ( -
-

Q: {faq.q}

-

{faq.a}

-
- ))} -
- ), - }, -]; - -export default function AboutPage() { - const [openIds, setOpenIds] = useState(['how-to-use']); - - const toggle = (id: string) => { - setOpenIds((prev) => - prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] - ); - }; - - return ( -
- {/* Header */} -
- - - - - 返回对话 - -

使用帮助

-
- -
-
- {accordionItems.map((item) => { - const isOpen = openIds.includes(item.id); - return ( -
- - {isOpen && ( -
{item.content}
- )} -
- ); - })} - - {/* Contact section */} -
-

联系我们

-
-

如有任何问题或建议,欢迎通过以下方式联系我们:

-
- - - - support@zhiyuan-assistant.cn -
-
- - - - 400-888-6789(工作日 9:00-18:00) -
-
-
- -

- 升学助手 v1.0.0 · AI志愿规划师 -

-
-
-
- ); -} diff --git a/apps/web/src/app/assessment/page.tsx b/apps/web/src/app/assessment/page.tsx deleted file mode 100644 index 24647d1..0000000 --- a/apps/web/src/app/assessment/page.tsx +++ /dev/null @@ -1,350 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import Link from 'next/link'; - -interface Question { - id: number; - title: string; - options: { - label: string; - text: string; - type: 'R' | 'I' | 'S' | 'A'; - }[]; -} - -const questions: Question[] = [ - { - id: 1, - title: '你更喜欢哪种工作方式?', - options: [ - { label: 'A', text: '动手操作,解决实际问题', type: 'R' }, - { label: 'B', text: '思考分析,研究抽象概念', type: 'I' }, - { label: 'C', text: '与人沟通,协调组织事务', type: 'S' }, - { label: 'D', text: '创意表达,产出独特作品', type: 'A' }, - ], - }, - { - id: 2, - title: '哪种活动最让你感到充实?', - options: [ - { label: 'A', text: '拆解和修理物品', type: 'R' }, - { label: 'B', text: '阅读科学文章或解数学题', type: 'I' }, - { label: 'C', text: '组织朋友聚会或团队活动', type: 'S' }, - { label: 'D', text: '绘画、写作或音乐创作', type: 'A' }, - ], - }, - { - id: 3, - title: '面对问题时,你通常?', - options: [ - { label: 'A', text: '立即动手尝试解决', type: 'R' }, - { label: 'B', text: '先分析问题的根源', type: 'I' }, - { label: 'C', text: '找朋友或同事讨论', type: 'S' }, - { label: 'D', text: '用创新的方式重新定义问题', type: 'A' }, - ], - }, - { - id: 4, - title: '你更擅长?', - options: [ - { label: 'A', text: '使用工具和设备', type: 'R' }, - { label: 'B', text: '逻辑推理和数据分析', type: 'I' }, - { label: 'C', text: '说服和激励他人', type: 'S' }, - { label: 'D', text: '想象和创造新事物', type: 'A' }, - ], - }, - { - id: 5, - title: '在团队中,你更倾向于?', - options: [ - { label: 'A', text: '负责具体的执行任务', type: 'R' }, - { label: 'B', text: '提供分析和策略建议', type: 'I' }, - { label: 'C', text: '协调成员关系和沟通', type: 'S' }, - { label: 'D', text: '提出新颖的想法和方向', type: 'A' }, - ], - }, - { - id: 6, - title: '哪种学习方式最适合你?', - options: [ - { label: 'A', text: '动手实践、做实验', type: 'R' }, - { label: 'B', text: '阅读教材、听讲座', type: 'I' }, - { label: 'C', text: '小组讨论、互相教学', type: 'S' }, - { label: 'D', text: '独立探索、自由创作', type: 'A' }, - ], - }, - { - id: 7, - title: '你对哪种信息更敏感?', - options: [ - { label: 'A', text: '物理空间和机械原理', type: 'R' }, - { label: 'B', text: '数字、图表和公式', type: 'I' }, - { label: 'C', text: '人的情绪和社交信号', type: 'S' }, - { label: 'D', text: '色彩、形状和美学', type: 'A' }, - ], - }, - { - id: 8, - title: '哪种工作环境你最舒适?', - options: [ - { label: 'A', text: '实验室或工作坊', type: 'R' }, - { label: 'B', text: '安静的办公室或图书馆', type: 'I' }, - { label: 'C', text: '开放式的协作空间', type: 'S' }, - { label: 'D', text: '灵活自由的创意空间', type: 'A' }, - ], - }, - { - id: 9, - title: '你更看重工作的?', - options: [ - { label: 'A', text: '稳定性和实用性', type: 'R' }, - { label: 'B', text: '专业性和深度', type: 'I' }, - { label: 'C', text: '人际关系和影响力', type: 'S' }, - { label: 'D', text: '自由度和创造性', type: 'A' }, - ], - }, - { - id: 10, - title: '未来5年,你希望成为?', - options: [ - { label: 'A', text: '技术专家/工程师', type: 'R' }, - { label: 'B', text: '科学家/研究员', type: 'I' }, - { label: 'C', text: '管理者/领导者', type: 'S' }, - { label: 'D', text: '艺术家/创业者', type: 'A' }, - ], - }, -]; - -const typeInfo: Record = { - R: { name: '动手型 (R)', desc: '喜欢实际操作,擅长使用工具和设备解决具体问题', majors: '工学类(计算机、机械、电子等)', color: 'bg-orange-500' }, - I: { name: '思考型 (I)', desc: '喜欢研究和分析,擅长逻辑推理和抽象思维', majors: '理学类(数学、物理、化学等)', color: 'bg-blue-500' }, - S: { name: '社交型 (S)', desc: '喜欢与人交往,擅长沟通协调和组织管理', majors: '经管/教育/法学类', color: 'bg-green-500' }, - A: { name: '创造型 (A)', desc: '喜欢创意表达,擅长想象和创造独特作品', majors: '文学/艺术/设计类', color: 'bg-purple-500' }, -}; - -export default function AssessmentPage() { - const [current, setCurrent] = useState(0); - const [answers, setAnswers] = useState<(string | null)[]>(Array(10).fill(null)); - const [showResult, setShowResult] = useState(false); - - const handleSelect = (type: string) => { - const newAnswers = [...answers]; - newAnswers[current] = type; - setAnswers(newAnswers); - }; - - const handleNext = () => { - if (current < 9) { - setCurrent(current + 1); - } else { - setShowResult(true); - } - }; - - const handlePrev = () => { - if (current > 0) { - setCurrent(current - 1); - } - }; - - const calculateResult = () => { - const scores: Record = { R: 0, I: 0, S: 0, A: 0 }; - answers.forEach((answer) => { - if (answer && scores[answer] !== undefined) { - scores[answer]++; - } - }); - return scores; - }; - - const question = questions[current]; - - // Result view - if (showResult) { - const scores = calculateResult(); - const maxType = Object.entries(scores).reduce((a, b) => (b[1] > a[1] ? b : a))[0]; - const info = typeInfo[maxType]; - const total = Object.values(scores).reduce((a, b) => a + b, 0); - - return ( -
-
- - - - - 返回对话 - -

测评结果

-
- -
-
- {/* Result card */} -
-
-
- {maxType} -
-
-

{info.name}

-

{info.desc}

-
-
- -
-

推荐专业方向

-

{info.majors}

-
- - {/* Dimension scores */} -
-

各维度得分

- {(['R', 'I', 'S', 'A'] as const).map((type) => { - const t = typeInfo[type]; - const pct = total > 0 ? (scores[type] / total) * 100 : 0; - return ( -
- {t.name} -
-
-
- {scores[type]} -
- ); - })} -
-
- - {/* Action buttons */} -
- - 返回对话 - - -
-
-
-
- ); - } - - return ( -
- {/* Header */} -
- - - - - 返回对话 - -

兴趣倾向测评

-
- -
-
- {/* Progress bar */} -
-
-
- {questions.map((q, i) => ( -
- ))} -
- - {current + 1}/{questions.length} - -
-
-
-
-
- - {/* Question card */} -
-

- 第 {question.id} 题 -

-

{question.title}

- -
- {question.options.map((option) => { - const isSelected = answers[current] === option.type; - return ( - - ); - })} -
-
- - {/* Navigation buttons */} -
- - -
-
-
-
- ); -} diff --git a/apps/web/src/app/consultations/page.tsx b/apps/web/src/app/consultations/page.tsx deleted file mode 100644 index db9fd05..0000000 --- a/apps/web/src/app/consultations/page.tsx +++ /dev/null @@ -1,253 +0,0 @@ -'use client'; - -import React, { useEffect, useMemo, useState } from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import type { ConsultationRecord } from '@/lib/useChat'; - -const RECORDS_KEY = 'consultationRecords'; -const ACTIVE_KEY = 'activeConsultationId'; - -function loadRecords(): ConsultationRecord[] { - if (typeof window === 'undefined') return []; - try { - const raw = localStorage.getItem(RECORDS_KEY); - return raw ? JSON.parse(raw) : []; - } catch { - return []; - } -} - -function saveRecords(records: ConsultationRecord[]) { - try { - localStorage.setItem(RECORDS_KEY, JSON.stringify(records)); - } catch {} -} - -function formatDate(iso: string) { - const d = new Date(iso); - if (Number.isNaN(d.getTime())) return '未知时间'; - const pad = (n: number) => String(n).padStart(2, '0'); - return `${d.getMonth() + 1}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; -} - -function getRecordSummary(record: ConsultationRecord) { - const userMessages = record.messages?.filter(msg => msg.role === 'user') || []; - const assistantMessages = record.messages?.filter(msg => msg.role === 'assistant') || []; - const lastUser = [...userMessages].reverse()[0]?.content; - const profile = record.userProfile || {}; - const profileText = [ - profile.province, - profile.subjects?.join('+'), - profile.score ? `${profile.score}分` : undefined, - profile.rank ? `位次${profile.rank}` : undefined, - ].filter(Boolean).join(' · '); - - return { - messageCount: (record.messages || []).length, - userCount: userMessages.length, - assistantCount: assistantMessages.length, - lastUser, - profileText, - hasPlan: Boolean(record.currentPlan), - hasAudit: Boolean(record.currentAuditReport), - }; -} - -export default function ConsultationsPage() { - const router = useRouter(); - const [records, setRecords] = useState([]); - const [keyword, setKeyword] = useState(''); - const [deleteTarget, setDeleteTarget] = useState(null); - - useEffect(() => { - setRecords(loadRecords()); - }, []); - - const filteredRecords = useMemo(() => { - const q = keyword.trim().toLowerCase(); - if (!q) return records; - return records.filter(record => { - const text = [ - record.title, - record.userProfile?.province, - record.userProfile?.score, - record.userProfile?.rank, - record.userProfile?.subjects?.join(' '), - ...(record.messages || []).map(msg => msg.content), - ].filter(Boolean).join(' ').toLowerCase(); - return text.includes(q); - }); - }, [records, keyword]); - - const openRecord = (id: string) => { - try { - localStorage.setItem(ACTIVE_KEY, id); - } catch {} - router.push('/'); - }; - - const startNewConsultation = () => { - try { - localStorage.removeItem(ACTIVE_KEY); - } catch {} - router.push('/?new=1'); - }; - - const confirmDelete = () => { - if (!deleteTarget) return; - const updated = records.filter(record => record.id !== deleteTarget.id); - saveRecords(updated); - setRecords(updated); - try { - if (localStorage.getItem(ACTIVE_KEY) === deleteTarget.id) { - localStorage.removeItem(ACTIVE_KEY); - } - } catch {} - setDeleteTarget(null); - }; - - return ( -
-
- - - - - 返回对话 - -
-

咨询记录

-

恢复历史对话与规划上下文

-
- -
- -
-
- setKeyword(e.target.value)} - placeholder="搜索记录:分数、省份、职业、方案关键词..." - className="w-full rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - {keyword && ( - - )} -
-
- - {records.length === 0 ? ( -
-
💬
-

暂无咨询记录

-

- 你和升学助手的每次咨询会自动保存。生成方案、职业解读、审核结果都能在这里回溯。 -

- -
- ) : filteredRecords.length === 0 ? ( -
-
🔎
-

没有找到匹配的咨询记录

-
- ) : ( -
- {filteredRecords.map(record => { - const summary = getRecordSummary(record); - return ( -
- -
-
- 用户 {summary.userCount} 轮 · AI {summary.assistantCount} 轮 -
-
- - -
-
-
- ); - })} -
- )} - - {deleteTarget && ( -
-
-

删除咨询记录?

-

- 将删除“{deleteTarget.title || '未命名咨询'}”及其中的对话上下文。已单独保存的志愿方案不会删除。 -

-
- - -
-
-
- )} -
- ); -} diff --git a/apps/web/src/app/favicon.ico b/apps/web/src/app/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/apps/web/src/app/favicon.ico and /dev/null differ diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css deleted file mode 100644 index 4949359..0000000 --- a/apps/web/src/app/globals.css +++ /dev/null @@ -1,89 +0,0 @@ -@import "tailwindcss"; - -/* ═════════════════════════════════════════════ - 设计系统 — CSS 变量 & 主题 Token - ═════════════════════════════════════════════ */ -@import "../styles/design-system.css"; - -/* ═════════════════════════════════════════════ - 全局基础样式 - ═════════════════════════════════════════════ */ -body { - background-color: var(--bg-secondary); - color: var(--text-primary); - font-family: var(--font-sans); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transition: background-color var(--transition-normal), - color var(--transition-normal); -} - -/* 自定义滚动条 */ -::-webkit-scrollbar { - width: 4px; -} -::-webkit-scrollbar-track { - background: transparent; -} -::-webkit-scrollbar-thumb { - background: #d1d5db; - border-radius: 2px; -} -::-webkit-scrollbar-thumb:hover { - background: #9ca3af; -} - -/* 打字动画 */ -@keyframes typing-dot { - 0%, - 60%, - 100% { - opacity: 0.3; - transform: translateY(0); - } - 30% { - opacity: 1; - transform: translateY(-4px); - } -} -.typing-dot { - animation: typing-dot 1.4s infinite; -} -.typing-dot:nth-child(2) { - animation-delay: 0.2s; -} -.typing-dot:nth-child(3) { - animation-delay: 0.4s; -} - -/* 消息进入动画 */ -@keyframes message-enter { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.message-enter { - animation: message-enter var(--transition-normal); -} - -/* 无障碍:全局焦点指示器 */ -*:focus-visible { - outline: 2px solid var(--brand-500); - outline-offset: 2px; - border-radius: 2px; -} - -/* 减少动画偏好 */ -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx deleted file mode 100644 index cbb375f..0000000 --- a/apps/web/src/app/layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { Metadata, Viewport } from 'next'; -import './globals.css'; -import { initThemeScript } from '@/lib/theme'; - -export const metadata: Metadata = { - title: '升学助手 - AI志愿规划师', - description: '基于大模型的职业解读与志愿填报智能助手', -}; - -export const viewport: Viewport = { - width: 'device-width', - initialScale: 1, -}; - -export default function RootLayout({ children }: { children: React.ReactNode }) { - return ( - - - {/* 主题初始化 — 阻塞渲染,防止暗色模式闪白 */} - hello" />); + expect(container.querySelector('script')).toBeNull(); + expect(container.innerHTML).not.toMatch(/