Files
lixi b81c050e03
CI / docs-build (push) Successful in 58s
docs: M2 第三波收口——报告 22~27 入档挂导航
- 22~26 Flutter 接入五单报告(T2-11~14 + 白名单扩充,flutter 测试 64→272)
- 27 第三波收口总表:demo 数据消亡、四态纪律、埋点端到端贯通、DEBT-1 偿还
- 三次 compose 实测无契约偏差;波内 agent 中断续跑事故记录在案

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 14:10:36 +08:00

15 KiB
Raw Permalink Blame History

T2-12 宠物列表、详情与编辑页接入真实数据(交付报告)

执行日期2026-09-08 角色Frontend DeveloperFlutter 工单T2-12L,关键路径)+ 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 全绿(+51flutter analyze 0 问题,dart format 无 diff
  • compose 真实后端实测:注册 → 空态 → 品种目录 → 建档 → 列表 → 详情 → 差量编辑 → 40902 → 40903 → 自定义品种建档,全部符合契约预期(§6)。

1. 页面与四态覆盖表

页面 / 网络面 loading empty error retry 测试文件
P1 宠物列表(档案 Tabpets_page.dart 居中转圈 EmptyStateIllustration「还没有宠物档案」+ 建档 CTA InlineErrorBanner(按错误类型分文案) 重试按钮 + 下拉刷新 pets_page_test.dart6
P2 宠物详情(pet_detail_page.dart 无内存副本时转圈 (有副本即时渲染、后台刷新失败降级 SnackBar,有测试) 「不存在」态:40401 → 提示 + 返回列表并刷新 (详情页的 empty 语义即目标缺席) 横幅 重试按钮 pet_detail_page_test.dart(8)
表单页品种目录(pet_form_page.dart 内) 内联转圈 目录空 → 仅「自定义品种…」可选(结构兜底) 「目录加载失败」提示 + 回落自定义输入 内联重试按钮 pet_form_page_test.dart11

页面结构与导航:

档案 TabIndexedStack,页名 pet_list
  └─ P1 宠物列表:宠物卡(PetAvatar lg + 名字 + 品种·性别·年龄 + 状态 TagPill)
       ├─ 「添加」/ 空态 CTA / 虚线卡 → PetFormPage.createfadePageRoute,路由名 pet_form
       └─ 点卡 → PetDetailPage(路由名 pet_detail
             └─ owner 编辑徽标 / 编辑按钮 → PetFormPage.edit(无路由名,见 §4 决策 3)

2. 表单与冲突处理(对齐冻结契约)

  • 字段:昵称*、物种*SegmentedButton 犬/猫/其他,编辑锁定静态显示——species 不可改)、性别*male/female/unknown,契约必填,未选提交拦截)、品种(目录下拉 + 「自定义品种…」互斥,二选一必填;编辑时目录缺席的既有品种保底成项防下拉失配)、生日(DatePicker + 「估算」勾选)、芯片号(可选)、性格(可选)。头像按 ADR-010 本地占位形态(PetAvatar url 缺省),不做上传。
  • 校验:失焦 + 提交双校验,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 落位 AppColorserrorDark #B02C25inkSoft #6B5A4A(05 D8;本单页面族次级信息文字一律 inkSoftmuted 只作占位/禁用/装饰——DEBT-2 局部规避执行)。
  • 回归:既有零参数调用(post_detail 话题标签、services「认证服务」、services 商家标签)零参数变更,全量 177 测试回归通过;映射行为由 test/widgets/tag_pill_test.dart 5 个用例锁定(含显式 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 建宠表单首次输入(任一字段/选择器,每次进入一次) entryPointprofile_empty_state / pet_listpost_register_guide 预留) 首次输入仅一次
2 pet_create_succeeded 建宠接口 code=0 durationMs(表单打开→成功)、speciespetIndex 三属性齐备、petIndex=1
3 pet_create_failed 失败响应 / 超时 / 本地校验拦截 failureReasonerrorCode(可空)、httpStatus(由业务码 ~/100 推导,可空)、attemptSeq 校验拦截 / 40903(409) / 网络三路径
4 page_viewed(pet_form) 建宠表单页 pushRouteSettings(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_listTab 曝光补点机制不变) 同上 既有 Tab 补点测试覆盖机制

映射决策(报数据侧知悉):

  1. failureReason 枚举照 06 §4 四值(pet_limit_reached 因产品未设上限未纳入);客户端网络层不区分 5xx 与断网/超时(同为 ApiNetworkException),两者并入 network_errorserver_error 留作兜底;40903 等业务拒绝归 validation_error 并以 errorCode 细分。
  2. durationMs 口径 = 表单打开(页面 initState)→ 成功响应(06 未定义精确口径,此口径对「动笔→成功」段更有解释力)。
  3. 编辑表单不带路由名pet_form 是建宠漏斗到达段专属页名(06 §1.6),编辑曝光计入会使「到达→动笔」分母系统性虚高;编辑本身不设事件(06 §1.4 既定取舍)。
  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 founddart format --set-exit-if-changed 无 diff(三个提交逐个通过)。

6. compose 真实后端实测记录(验收链路)

环境:patbond-api dev@64c9b72./mvnw -DskipTests package + docker compose up -d --buildauth :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,返回完整 Petversion=0myRole=owner)—— 建档
5 GET /api/v1/pets 列表含新宠物 —— 列表
6 GET /api/v1/pets/{id} 详情字段逐一回读 —— 详情
7 PATCH /api/v1/pets/{id}{"version":0,"name":"豆豆二世"} 差量) code=0name 更新 —— 编辑
8 PATCH 携带旧 version=0 {"code":40902,"message":"数据已被修改,请刷新后重试"} —— 冲突路径与页面处理对齐
9 POST 同芯片号再建档 {"code":40903,"message":"芯片号已被其他宠物登记"} —— 字段级报错路径对齐
10 POST 自定义品种(customBreedName,无 breedId code=0breedId=null, customBreedName="狸花" —— 互斥另一半

结论:空态 → 建档 → 列表/详情全链路 + 两类冲突码在真实后端全部符合冻结契约与页面实现预期;未发现契约偏差。UI 侧同构行为由 §5 的 widget 测试(注入假仓库)锁定。实测后 docker compose downpatbond-api 仓库零改动。

7. AppState demo 清理

  • 删除:AppState.vaccines / updateVaccines / updatePet 及其持久化键、initialVaccines、models 中 VaccineRecord / VaccineItem / VaccineStatus(消费方仅原 pets_page,随页面替换全部失效);原 EditPetSheet / VaccineSheet demo 随页面重写移除。
  • 保留(未越界):AppState.pet demo 仍被首页问候卡、创作页上传占位、主壳头部头像消费——属其他 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_archivepet_list 字典 v2 初始集合本含 pet_list;数据侧看板注意 2026-09-08 起的页名断点
4 sterilizedOn 详情展示、表单暂不可编辑 工单字段清单(品种/性别/生日/芯片号)之外,避免表单过长;记小遗留
5 归档入口(D2-7「首版仅归档」)未做 依赖 listPets 对 archived 的过滤语义确认(契约未明示列表是否含 archived),建议随 T2-13 或收口单补一个详情页归档动作
6 HealthTimelineTile05 §3.3)未随本单交付 其唯一消费方是 T2-14 时间线,留给 T2-14 与真实数据一并落地

9. 交接 T2-13/14

  • 页面骨架:PetDetailPage._content 的「基本资料」卡之上/之下即 stat 行与时间线的落位点;RecordTypeDotEmptyStateIllustration、TagPill 深变体、recordTypeStyles 映射可直接取用。
  • 数据获取范式:页内四态 + petLoadErrorMessage 文案分档 + 内存副本先渲染的模式可复制;分页用 CursorPage22 号报告 §2)。
  • 埋点:health_record_* 事件按 pet_analytics.dart 同款强类型封装新建 health_record_analytics.dartrecord_form / record_detail 页名枚举已就位待接线。

Frontend Developer · 2026-09-08 · patbond-flutter dev@97a1f46