Appearance
2.4 页面状态与浏览器验收
Page 接上 client、store 和 route 后,先写清每种状态对应的页面输出。
| 状态 | 页面输出 | 允许的动作 |
|---|---|---|
idle | 尚未发出请求 | 无 |
loading | “正在加载推荐…” | 等待 |
success + items | 数量、卡片、收藏态 | 切阶段、收藏、进详情 |
success + empty | “当前阶段还没有推荐内容。” | 切阶段 |
error | 安全文案与“重新加载” | retry |
| persistence notice | 当前内存修改与保存风险 | 关闭提示、继续操作 |
| detail not found | “没有找到这条推荐。” | 返回 |
Empty 是成功响应,不显示 retry。合法 ID 没有对应详情也不是 network error。
本节检查:
text
apps/web/src/components/AsyncStatePanel.vue
apps/web/src/components/RecommendationCard.vue
apps/web/src/pages/index/index.vue
apps/web/src/pages/detail/index.vue
apps/web/src/pages/saved/index.vue
apps/web/src/pages.json
apps/web/src/utils/keyboard-activation.ts三个 page 不共享查询细节
| Page | Client 调用 | Store action |
|---|---|---|
| 首页 | list(selectedStage) | selectStage、setFavorite |
| 详情 | get(id) | 找到 item 后 recordViewed、setFavorite |
| 我的记录 | listByIds(savedIds) | setFavorite |
三个 page 都从 app provider 取得 client,并使用共享请求 controller。它们不 import Mock 数据,不直接碰 storage。
详情页先检查 query:
ts
recommendationId.value = isRecommendationId(query?.id)
? query.id
: undefined;请求成功后,只有拿到 item 才记录 history。未知、空白或目录之外的 ID 显示 not-found,不产生副作用。
AsyncStatePanel 只接 loading | empty | error、一条安全文案和 retry event。Component 不发请求,不读 store,也不知道当前 stage。
Retry 复用最近一次请求
切换 stage 或重新进入页面会触发新的 loader。共享 controller 记录最近 loader,并用 token 丢弃 stale success 和 stale error。
Page 只负责给出本次查询:
ts
function loadRecommendations(stageId: StageId): Promise<void> {
return loadRequest(() => client.list(stageId));
}retryRequest() 重跑最近的 client.list(stageId),不复制 query 和错误映射。新请求开始时旧错误文案被清空。
一套键盘 helper 覆盖所有紧凑操作
卡片打开与收藏是 sibling 控件。点击收藏不应冒泡成打开详情。阶段按钮、卡片、收藏、“我的记录”、详情收藏、retry 和通知关闭共用同一个 helper:
ts
export function createKeyboardActivationHandler<Arguments extends unknown[]>(
action: (...args: Arguments) => void,
): (event: KeyboardEvent, ...args: Arguments) => void {
return (event, ...args) => {
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
event.preventDefault();
action(...args);
}
};
}Enter、空格和 Spacebar 执行 action 并阻止默认事件,其他键不执行。Helper 有 6 项行为测试。
紧凑操作的 CSS 同时声明 min-width: 44px 与 min-height: 44px。CSS guard 有 1 项测试,防止收藏、记录入口、retry、详情收藏与通知关闭被改回小触点。源码声明还不够,H5 要量 computed style 和 bounding box。
自动 gate 是 9 个文件、143 项
bash
npm run check
npm --workspace apps/web run dev:h5 -- --host 127.0.0.1 --port 4173Fresh npm run check 结果:9 个 test files、143 tests passed,随后 typecheck 与 H5 build 均为 exit 0。143 项包括 keyboard 6 和 CSS guard 1。
按真实 H5 路径操作
打开 http://127.0.0.1:4173/。同一首页只改 hash query 后执行 refresh。
| 路径 | 操作与结果 |
|---|---|
| 阶段 refresh | 选择 1–3 岁 后显示 1 条;refresh 后仍选中且仍为 1 条 |
| 阶段切换请求 | slow 完成后切到孕期,立即回到 loading;约 900 ms 后显示 1 条 |
| 收藏 refresh | 用 Space 切换“日常主题图画书”收藏,URL 留在首页;refresh 后收藏态保留 |
| 历史去重 | 连续两次打开 rec-picture-book,最近浏览仍为 1 条 |
scenario=slow | refresh 后先显示 loading,约 900 ms 后显示当前阶段 1 条 |
scenario=empty | 显示 0 条和“当前阶段还没有推荐内容。” |
scenario=retry | 首次显示错误与“重新加载”;Space 激活后恢复为 1 条 |
补查这些结果:
- 点击收藏前后 URL 都是首页,没有打开详情;
- 未知详情 ID 显示 not-found,“最近浏览”数量不变;
- Enter 激活阶段,Space 激活 retry;
- “我的记录”、详情收藏、retry、卡片收藏和通知关闭的实际 bounding box 均至少
44px × 44px; - 390 × 844 下 document 与 body 的
clientWidth/scrollWidth都是390/390; - 正常关键路径 console error 为 0。
失败时沿链路查第一处断点
| 症状 | 先查 |
|---|---|
| 收藏却打开详情 | 卡片与收藏是否 sibling,event 是否分开 |
| 切阶段仍显示旧列表 | page 是否把新的 stage 传给 client.list(stageId),controller 是否丢弃 stale result |
| 未知详情写入历史 | query 是否先经 isRecommendationId,是否拿到 item 后才 recordViewed |
| Refresh 丢状态 | hydrate()、store action、persistence key |
| Retry 没有恢复 | scenario 是否位于 hash query,是否 refresh,controller 是否保留最近 loader |
| 键盘无反应 | role、tabindex、共享 helper 与 preventDefault() |
下一步:2.5 PR Review 与交付。