# 任务 04 检查记录:前端 API 自动生成与基础视觉壳层 日期:2026-08-31 ## 交付内容 ### OpenAPI 代码生成管线 - `client/scripts/generate-api.mjs`:从后端运行时端点 `http://localhost:3000/api/openapi.json`(可用 `OPENAPI_URL` 覆盖)拉取并生成类型;获取失败/生成失败时 exit 1 并提示先启动后端。 - `client/package.json`:新增 `api:generate`,并接入 `dev`、`build` 前置(`npm run api:generate && vite`),OpenAPI 无法获取或生成失败时命令直接失败。 - 根 `package.json`:`pnpm api:generate` = client `api:generate`(需后端服务运行中)。 - `client/src/api/generated/schema.d.ts`:由 openapi-typescript 生成,已在 `.gitignore` 中忽略、禁止手工修改。 ### 请求客户端与领域 API - `client/src/api/client.ts`:`openapi-fetch` 的 `createClient`,统一 baseUrl(默认相对路径 `/api`,开发环境经 Vite 代理)、JSON headers、15s 超时(AbortController)与错误解包;`ApiError` 统一异常。 - `client/src/api/index.ts`:按域暴露类型化方法(dashboard/practice/review/plans/mock/news/questions/ai/profile/settings),请求/响应类型由 `BodyOf`/`QueryOf`/`SuccessBody` 从生成的 `paths` 推导,业务代码不写 URL 与 `fetch`。 ### 设计 Token 与基础组件 - `client/src/styles/tokens.css`(承接 `deliverables/design/design-tokens.css`)、`reset.css`、`main.css`。 - `components/base/`:AppIcon(内联线性 SVG 图标集,替代依赖)、AppButton、AppCard、AppBadge。 - `components/feedback/`:AppLoading、AppEmpty、AppError、AppToast、ComingSoon。 ### 路由、Store 与双端布局 - `router/nav.ts`(PC 8 项 / Mobile 5 项导航配置)、`router/index.ts`(8 条路由 + 兜底重定向 + 文档标题)。 - `stores/app.ts`(busy + toast)、`profile.ts`(档案加载)、`practice.ts`(会话状态)。 - `layouts/DesktopLayout.vue`(220px 侧栏)、`layouts/MobileLayout.vue`(顶部栏 + 底部 TabBar)。 - `App.vue` 按 `<768px` 断点切换双端布局;`main.ts` 接入 Pinia + Router。 - 首页 `DashboardView.vue` 通过 `dashboardApi.overview()` 加载真实数据,覆盖加载/空/错误/成功四态;其余 7 个导航页为 ComingSoon 占位。 ## 过程中发现并修复的问题 1. **后端 OpenAPI 参数污染**:`routes.ts` 用 `emptyObjectSchema = { type: 'object' }` 占位导致 `@fastify/swagger` 把 "type" 关键字误当参数名,每个路由生成虚假 `type`(query/path)参数,会污染前端生成类型。改为「无 schema 时省略 params/querystring 键」,重新导出后 31 个路径参数干净(0 个虚假参数)。 2. **openapi-typescript v7 API 变化**:返回 `ts.Node[]` 而非字符串,需 `astToString` 转换。 3. **Vite 依赖优化缓存**:`client/node_modules/.vite` 过期导致 dev 启动触发 bulk-delete 守卫报错,清除缓存后恢复。 4. **空响应体错误处理**:后端宕机时 Vite 代理返回 502 空 body,openapi-fetch 返回 `error: undefined`,导致 `unwrap` 误判成功并返回 undefined,触发 `isEmpty` 读取 undefined 报错。改为在 `unwrap` 中按 `response.ok` 判定并映射 502/503/504 为「无法连接服务」。 ## 命令检查 - `pnpm typecheck`:通过(client + server)。 - `pnpm build`:通过,client 构建前自动从 `http://localhost:3000/api/openapi.json` 执行 `api:generate`(后端运行中)。 - `pnpm --filter @gwy/client api:generate`:后端运行中生成成功;后端未启动时 exit 1 并提示「请确认后端服务已启动(pnpm dev:server)」。 ## 浏览器检查(agent-browser,实际渲染 DOM 验证) | 场景 | 结果 | |---|---| | 桌面 1440 视口 | 侧栏 8 项导航齐全;首页标题「数据中枢」+ 问候语(profile 真实加载) | | 首页数据 | hero 卡「学习天数 92 / 累计答题 0 / 正确率 0%」+ 5 张统计卡(题库题量 3、待复习 0、今日任务 0/0、今日学习 0、连续打卡 0)均来自 `/api/dashboard/overview` | | 移动 390 视口 | 顶部栏标题 + 底部 TabBar 5 项(首页/刷题/要闻/分析/我的) | | SPA 导航 | 点击「刷题中心」→ 路由切到 `/practice`,TabBar/标题同步变化,占位页「建设中」正常 | | 错误态 | 停后端 → 显示「数据加载失败 / 无法连接服务,请确认后端已启动」+「重试」按钮 | | 重试恢复 | 重启后端点击「重试」→ 数据恢复渲染 | | 空态 | 清空题库 → 显示「还没有学习数据」空状态,恢复数据后正常 | 截图:`deliverables/checks/task-04-desktop.png`、`task-04-mobile.png`。 ## 完成标准核对 - ✅ 前端业务代码不直接写 URL 和 `fetch`(统一走 `api/index.ts` 类型化方法 + `openapi-fetch`)。 - ✅ 修改后端 Schema 能重新生成类型(`pnpm api:generate` 从运行时 API 拉取生成,需后端运行)。 - ✅ 首页路由能通过真实接口加载(overview + profile 经 Vite 代理到后端)。 - ✅ 页面可在桌面和移动断点切换(768px 断点,双端布局实测)。 - ✅ 统一加载、空数据、错误提示组件(AppLoading/AppEmpty/AppError,均实测)。 ## 当前结论 任务 04 完成:前端 API 自动生成管线、类型化请求客户端、路由与 Pinia 基础 store、设计 Token、PC 侧栏与 Mobile TabBar、以及四态反馈组件全部落地,首页已通过真实接口加载。图标采用内联线性 SVG 替代 lucide 依赖(`lucide-vue-next` 已改名弃用为 `@lucide/vue` 且安装受阻,为避免大体积依赖与不稳定安装而自建图标集)。剩余 7 个页面为占位,待任务 05–11 逐个实现。 > 微调(2026-08-31):前端类型生成改为从后端运行时端点 `http://localhost:3000/api/openapi.json` 拉取,不再依赖本地 `server/openapi.json` 文件;`dev`/`build` 前置 `api:generate` 需后端已启动。