- 22~26 Flutter 接入五单报告(T2-11~14 + 白名单扩充,flutter 测试 64→272) - 27 第三波收口总表:demo 数据消亡、四态纪律、埋点端到端贯通、DEBT-1 偿还 - 三次 compose 实测无契约偏差;波内 agent 中断续跑事故记录在案 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15 KiB
T2-12 宠物列表、详情与编辑页接入真实数据(交付报告)
执行日期:2026-09-08
角色:Frontend Developer(Flutter)
工单:T2-12(L,关键路径)+ DEBT-1 偿还 + T2-17 前端半边(pet 域三事件)
依据:01 号拆解 T2-12 节、22 号数据层交付(T2-11)、05 号 UI 设计规范、06 号埋点规划
提交:patbond-flutter dev@97a1f46(基线 7fb9031,已 push origin dev),拆 3 个提交:
| 提交 | 内容 |
|---|---|
3179528 |
共享组件三件(PetAvatar / RecordTypeDot / EmptyStateIllustration)+ TagPill 深变体映射(DEBT-1) |
c0a8a56 |
pet 域埋点强类型封装(pet_analytics.dart 三事件) |
97a1f46 |
列表/详情/表单页接入真实数据 + app 装配 + demo 清理 + 全部页面测试 |
0. 结论摘要
- 档案 Tab 替换为真实宠物列表;列表 / 详情 / 建档 / 编辑全链路走 T2-11 数据层(PetsController → PetsRepository → ApiClient),页面零直连 ApiClient、零 AppState demo 依赖;
- 四态硬要求达成:列表、详情、表单内品种目录三处网络面均有 loading / empty / error / retry 且有 widget 测试锁定;
- 40902 版本冲突有「明确提示 + 自动取新 version 重提」路径(测试锁定 version 3→4 重提序列);40903 芯片号冲突字段级报错(测试锁定);
- DEBT-1 随本单偿还:TagPill 深变体映射落地,全部组合 ≥5.78:1(AA),既有调用零参数回归;
- 埋点:pet 域三事件 + page_viewed 的 pet_form / pet_detail / pet_list 接线完成(观察者路由名采集有测试证据);
- 测试 126 → 177 全绿(+51),
flutter analyze0 问题,dart format无 diff; - compose 真实后端实测:注册 → 空态 → 品种目录 → 建档 → 列表 → 详情 → 差量编辑 → 40902 → 40903 → 自定义品种建档,全部符合契约预期(§6)。
1. 页面与四态覆盖表
| 页面 / 网络面 | loading | empty | error | retry | 测试文件 |
|---|---|---|---|---|---|
P1 宠物列表(档案 Tab,pets_page.dart) |
居中转圈 ✅ | EmptyStateIllustration「还没有宠物档案」+ 建档 CTA ✅ |
InlineErrorBanner(按错误类型分文案)✅ |
重试按钮 + 下拉刷新 ✅ | pets_page_test.dart(6) |
P2 宠物详情(pet_detail_page.dart) |
无内存副本时转圈 ✅(有副本即时渲染、后台刷新失败降级 SnackBar,有测试) | 「不存在」态:40401 → 提示 + 返回列表并刷新 ✅(详情页的 empty 语义即目标缺席) | 横幅 ✅ | 重试按钮 ✅ | pet_detail_page_test.dart(8) |
表单页品种目录(pet_form_page.dart 内) |
内联转圈 ✅ | 目录空 → 仅「自定义品种…」可选(结构兜底) | 「目录加载失败」提示 + 回落自定义输入 ✅ | 内联重试按钮 ✅ | pet_form_page_test.dart(11) |
页面结构与导航:
档案 Tab(IndexedStack,页名 pet_list)
└─ P1 宠物列表:宠物卡(PetAvatar lg + 名字 + 品种·性别·年龄 + 状态 TagPill)
├─ 「添加」/ 空态 CTA / 虚线卡 → PetFormPage.create(fadePageRoute,路由名 pet_form)
└─ 点卡 → PetDetailPage(路由名 pet_detail)
└─ owner 编辑徽标 / 编辑按钮 → PetFormPage.edit(无路由名,见 §4 决策 3)
2. 表单与冲突处理(对齐冻结契约)
- 字段:昵称*、物种*(SegmentedButton 犬/猫/其他,编辑锁定静态显示——species 不可改)、性别*(male/female/unknown,契约必填,未选提交拦截)、品种(目录下拉 + 「自定义品种…」互斥,二选一必填;编辑时目录缺席的既有品种保底成项防下拉失配)、生日(DatePicker + 「估算」勾选)、芯片号(可选)、性格(可选)。头像按 ADR-010 本地占位形态(
PetAvatarurl 缺省),不做上传。 - 校验:失焦 + 提交双校验,
errorText受控、onChanged即清(登录纵切模式,昵称 Focus 失焦有测试)。 - 部分更新:编辑只发送改动字段 + version(测试锁定
{version:3, name:…}精确形状);品种对整体替换;无变更不发 PATCH 直接返回(有测试)。 - 错误分层(对齐 22 号报告 §3 处理语义,各有测试或复用既有锁定):
| 错误 | 呈现 |
|---|---|
| 40903 芯片号冲突 | 芯片号字段级 errorText「该芯片号已被登记,请核对后重试」 |
| 40902 版本冲突 | 横幅「资料已在其他设备被修改,已获取最新版本,请核对后重新保存」+ 自动 getPet 更新基线 version(保留用户输入),重提即用新 version——测试锁定提交序列 [3, 4] |
| 40401 不存在 | SnackBar + 返回列表并刷新 |
| 40300 无权限 | 横幅;且详情页对非 owner 隐藏全部编辑入口(viewer 用例有测试) |
| 40000 / 其他业务码 | 横幅通用文案(原始 message 不上屏) |
| 429 | 横幅「操作过于频繁」 |
| 网络/超时/5xx | SnackBar + 重试动作 |
| 会话失效 | 静默(认证状态机自动回登录页;登出同时 PetsController.reset() 防跨账号泄漏,有测试) |
3. DEBT-1 偿还证据(TagPill 深变体)
方案照 05 号规范 §5.3 落地:TagPill 增可选 inkColor,缺省按 color 查内置映射;底色维持 withAlpha(20) 不变;字号 11/w700 不变。
| 组合(文字色 / 8% 淡底) | 修复前对比度 | 修复后对比度 | 判定 |
|---|---|---|---|
| primary → primaryDark | 2.55 | 8.74:1 | AA ✅ |
| success → successInk | 2.50 | 7.39:1 | AA ✅ |
| accent → accentDark | 1.67 | 7.07:1 | AA ✅ |
error → errorDark(新 token #B02C25) |
— | 5.78:1 | AA ✅ |
| 未命中映射 → ink 兜底 | — | ≥12:1 | AA ✅ |
- 新 token 落位
AppColors:errorDark #B02C25、inkSoft #6B5A4A(05 D8;本单页面族次级信息文字一律inkSoft,muted只作占位/禁用/装饰——DEBT-2 局部规避执行)。 - 回归:既有零参数调用(post_detail 话题标签、services「认证服务」、services 商家标签)零参数变更,全量 177 测试回归通过;映射行为由
test/widgets/tag_pill_test.dart5 个用例锁定(含显式inkColor覆盖与兜底)。 - 同工单落位(05 §5.3 第 4 点建议):
RecordTypeDot五类型三色映射唯一出口(lib/core/widgets/record_type_dot.dart,含 §2 表全量映射常量与测试),供 T2-13/14 时间线直接取用;PetAvatar四尺寸档收敛重复头像实现,编辑徽标底修订为primaryStrong(白图标 4.49:1 达非文字 3:1,修复原primary底 2.75:1 不达标)。
4. 埋点挂接清单(T2-17 前端半边)
强类型封装 lib/features/pets/pet_analytics.dart(13 号规范 §3.1 惯例,枚举编译期锁死),注入链 app.dart → MainShellPage → PetsPage → 表单页:
| # | 事件 / 页名 | 触发点 | 属性 | 测试 |
|---|---|---|---|---|
| 1 | pet_create_started |
建宠表单首次输入(任一字段/选择器,每次进入一次) | entryPoint(profile_empty_state / pet_list;post_register_guide 预留) |
首次输入仅一次 ✅ |
| 2 | pet_create_succeeded |
建宠接口 code=0 | durationMs(表单打开→成功)、species、petIndex |
三属性齐备、petIndex=1 ✅ |
| 3 | pet_create_failed |
失败响应 / 超时 / 本地校验拦截 | failureReason、errorCode(可空)、httpStatus(由业务码 ~/100 推导,可空)、attemptSeq |
校验拦截 / 40903(409) / 网络三路径 ✅ |
| 4 | page_viewed(pet_form) |
建宠表单页 push(RouteSettings(name: 'pet_form'),fadePageRoute 为 PageRoute,被既有 AnalyticsRouteObserver 采集) |
既有 pageName/referrer | push 路由名断言 ✅(06 §1.6 三段漏斗到达段接通) |
| 5 | page_viewed(pet_detail) |
详情页 push 路由名 pet_detail |
同上 | push 路由名断言 ✅ |
| 6 | page_viewed(pet_list) |
档案 Tab 页名由 pet_archive 改报 pet_list(Tab 曝光补点机制不变) |
同上 | 既有 Tab 补点测试覆盖机制 |
映射决策(报数据侧知悉):
failureReason枚举照 06 §4 四值(pet_limit_reached因产品未设上限未纳入);客户端网络层不区分 5xx 与断网/超时(同为ApiNetworkException),两者并入network_error,server_error留作兜底;40903 等业务拒绝归validation_error并以errorCode细分。durationMs口径 = 表单打开(页面 initState)→ 成功响应(06 未定义精确口径,此口径对「动笔→成功」段更有解释力)。- 编辑表单不带路由名:
pet_form是建宠漏斗到达段专属页名(06 §1.6),编辑曝光计入会使「到达→动笔」分母系统性虚高;编辑本身不设事件(06 §1.4 既定取舍)。 - 后端白名单:patbond-api dev@64c9b72 已含 pet 域 10 事件(T2-17 后端半边先行完成),三事件可直接落库。
5. 测试数变化
| 时点 | 测试数 | 说明 |
|---|---|---|
| 基线(dev@7fb9031) | 126 | T2-11 数据层交付 |
| 本单(dev@97a1f46) | 177(+51,全绿) | 见下分布 |
| 文件 | 数量 | 覆盖 |
|---|---|---|
test/widgets/tag_pill_test.dart |
5 | DEBT-1 映射四组 + 兜底 + inkColor 覆盖 + 底色不变 |
test/core/widgets/pet_avatar_test.dart |
5 | 四尺寸档、占位形态、徽标底色修订、sm/md 无徽标、点击/禁用 |
test/core/widgets/record_type_dot_test.dart |
3 | 五类映射齐备、渲染规格(50% 图标/8% 底)、三尺寸档 |
test/core/widgets/empty_state_illustration_test.dart |
2 | 全要素渲染 + CTA 回调、无 CTA/说明不渲染 |
test/features/pets/pet_analytics_test.dart |
4 | 三事件属性形状、可空属性缺席语义、httpStatus 推导 |
test/features/pets/pets_page_test.dart |
6 | 列表四态、pet_form/pet_detail 路由名、状态标签 |
test/features/pets/pet_detail_page_test.dart |
8 | 详情四态(含 40401 返回刷新)、副本即时渲染 + 降级 SnackBar、viewer 隐藏入口、编辑跳转预填、估算标记/未填写兜底 |
test/features/pets/pet_form_page_test.dart |
11 | 校验拦截、started 去重、目录/自定义互斥请求形状、40903 字段级、网络 SnackBar、目录失败回落+重试、失焦校验、编辑差量、40902 冲突重提序列、无变更不发 PATCH |
test/features/pets/pet_display_test.dart |
4 | 年龄边界(岁/月/未满月/未知)、元信息行、错误文案分档、标签 |
pets_controller_test.dart 增量 |
3 | loadBreeds 物种缓存、失败重试、reset 登出清空 |
质量门禁:flutter test 177/177 全绿;flutter analyze No issues found;dart format --set-exit-if-changed 无 diff(三个提交逐个通过)。
6. compose 真实后端实测记录(验收链路)
环境:patbond-api dev@64c9b72,./mvnw -DskipTests package + docker compose up -d --build(auth :8081 / pet :8083,均本机默认端口,客户端无需 --dart-define)。curl 按页面实际发出的请求逐步复演(token 已脱敏,测试账号随机生成、用后随 compose down 丢弃):
| 步骤 | 请求 | 结果 |
|---|---|---|
| 1 | POST /api/v1/auth/register(新用户) | code=0,取得 accessToken |
| 2 | GET /api/v1/pets | {"code":0,"data":[]} —— 新用户空态 ✅ |
| 3 | GET /api/v1/breeds?species=dog | 目录返回(中华田园犬/金毛/拉布拉多…),表单下拉数据源 ✅ |
| 4 | POST /api/v1/pets(表单同构体:name/species/sex/breedId/birthDate/birthDateEstimated/microchipNo/personality) | 201 语义 code=0,返回完整 Pet(version=0,myRole=owner)—— 建档 ✅ |
| 5 | GET /api/v1/pets | 列表含新宠物 —— 列表 ✅ |
| 6 | GET /api/v1/pets/{id} | 详情字段逐一回读 —— 详情 ✅ |
| 7 | PATCH /api/v1/pets/{id}({"version":0,"name":"豆豆二世"} 差量) |
code=0,name 更新 —— 编辑 ✅ |
| 8 | PATCH 携带旧 version=0 | {"code":40902,"message":"数据已被修改,请刷新后重试"} —— 冲突路径与页面处理对齐 ✅ |
| 9 | POST 同芯片号再建档 | {"code":40903,"message":"芯片号已被其他宠物登记"} —— 字段级报错路径对齐 ✅ |
| 10 | POST 自定义品种(customBreedName,无 breedId) | code=0,breedId=null, customBreedName="狸花" —— 互斥另一半 ✅ |
结论:空态 → 建档 → 列表/详情全链路 + 两类冲突码在真实后端全部符合冻结契约与页面实现预期;未发现契约偏差。UI 侧同构行为由 §5 的 widget 测试(注入假仓库)锁定。实测后 docker compose down,patbond-api 仓库零改动。
7. AppState demo 清理
- 删除:
AppState.vaccines/updateVaccines/updatePet及其持久化键、initialVaccines、models 中VaccineRecord/VaccineItem/VaccineStatus(消费方仅原 pets_page,随页面替换全部失效);原EditPetSheet/VaccineSheetdemo 随页面重写移除。 - 保留(未越界):
AppState.petdemo 仍被首页问候卡、创作页上传占位、主壳头部头像消费——属其他 Tab 的 demo 家具,留待相应工单收敛(AppState 内已注释标记)。
8. 决策与遗留
| # | 事项 | 说明 |
|---|---|---|
| 1 | 05 D1「单宠物跳过列表直进 P2」未采纳 | 该项待拍板;本单始终显示列表(P2 头部宠物切换器同属 D1,未做)。拍板后为小改动 |
| 2 | P2 的 stat 卡行 / AI 提醒 / 健康时间线未渲染 | T2-13/14 接摘要与记录接口时加回;不渲染 demo 占位(ADR-004),RecordTypeDot / HealthTimelineTile 所需映射已备好(前者已交付) |
| 3 | 档案 Tab 页名 pet_archive → pet_list |
字典 v2 初始集合本含 pet_list;数据侧看板注意 2026-09-08 起的页名断点 |
| 4 | sterilizedOn 详情展示、表单暂不可编辑 |
工单字段清单(品种/性别/生日/芯片号)之外,避免表单过长;记小遗留 |
| 5 | 归档入口(D2-7「首版仅归档」)未做 | 依赖 listPets 对 archived 的过滤语义确认(契约未明示列表是否含 archived),建议随 T2-13 或收口单补一个详情页归档动作 |
| 6 | HealthTimelineTile(05 §3.3)未随本单交付 | 其唯一消费方是 T2-14 时间线,留给 T2-14 与真实数据一并落地 |
9. 交接 T2-13/14
- 页面骨架:
PetDetailPage._content的「基本资料」卡之上/之下即 stat 行与时间线的落位点;RecordTypeDot、EmptyStateIllustration、TagPill 深变体、recordTypeStyles映射可直接取用。 - 数据获取范式:页内四态 +
petLoadErrorMessage文案分档 + 内存副本先渲染的模式可复制;分页用CursorPage(22 号报告 §2)。 - 埋点:
health_record_*事件按pet_analytics.dart同款强类型封装新建health_record_analytics.dart;record_form/record_detail页名枚举已就位待接线。
Frontend Developer · 2026-09-08 · patbond-flutter dev@97a1f46