Files
lixi f5457c2f4c
CI / docs-build (push) Successful in 2m2s
docs: M3 第三波收口——报告 21~27 入档挂导航
- 21~26 Flutter 社区接入五单 + 字典 v3 白名单(flutter 286→502、api 325→334)
- 27 收口总表:社区 demo 三页消亡、M3 四条验收标准逐条取证、
  乐观更新与媒体链路端到端、实现期修正记录
- device-verification.md 的 M3 四项真机步骤已由各单收口补全

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

150 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 24 M3 第三波:首页 Feed 接入真实数据(T3-14)
**执行日期**2026-09-09
**工单**T3-14 首页 Feed segment 替换真实数据——社区 demo 消亡的第一页
**依赖**21 号(T3-12 数据层,CommunityController 就位)、05 号 UI 规范、06 号埋点规划(feed 域白名单已随 api dev@8089c06 就绪)
**提交**patbond-flutter dev `8aac8c5`(基线 `1441f01`
---
## 0. 概要
`home_page.dart` 的 Feed segment 由「AppState demo 帖子 + 500ms 假延时刷新」
整体切换为 `CommunityController` 真实数据:四态首屏、尾部三态、下拉刷新与
游标翻页、聚合曝光埋点全部落地;PostCard 三形态等 4 个共享组件入
`lib/core/widgets/`;预签名 URL 的图片缓存 key 剥签名改造全仓生效。
**未动 patbond-api**create/post_detail 的 demo 按工单边界留给 T3-15/17。
**质量门禁**`flutter test` 421/421 全绿(基线 379+42)、`flutter analyze`
0 问题、`dart format --set-exit-if-changed` 无 diff、compose 六容器真链路
实测通过(§5)。
## 1. 四态与尾部三态覆盖表
| 态 | 渲染 | 交互 | widget 测试 |
|----|------|------|------------|
| 首屏 loadinginitial/loading | `FeedSkeleton` 连排 3 张(呼吸动效,尊重系统减弱动态设置静止 1.0) | — | ✓ |
| 首屏 error | `InlineErrorBanner`(pets 同款话术映射)+ 「重试」FilledButton | 重试 = 用户刷新(计入浏览段 refreshCount| ✓(含恢复 ready|
| 首屏 empty | `EmptyStateIllustration`forum_outlined「还没有动态」)+ CTA「发布第一条」 | CTA → 创作 Tab | ✓ |
| ready | `PostCard` 列表(卡间距 16) | 见 §2 | ✓(含降级作者)|
| 尾部 loading | 24 转圈(primary)居中,上下留白 16 | 滚动近底(余量 400)自动触发,携上页 nextCursor | ✓ |
| 尾部 error | 错误话术 + 「加载失败,点此重试」 | **只走显式点按重试**——失败态不随滚动通知自动重打(实测发现滚动风暴会把失败态冲掉并重复请求,已加守卫) | ✓(含事件上报与重试补页)|
| 尾部到底 | 「没有更多了」12 `inkSoft` 居中 | — | ✓ |
刷新语义照 controller 契约:下拉刷新失败且旧列表在手 → 保留列表不闪空态,
SnackBar 轻提示 + `feed_load_failed` 上报(widget 测试覆盖「失败保留旧列表 →
再刷成功整体替换不残留」全序列)。翻页不丢不重由 controller 代次守卫保证
(21 号已测),本单 widget 测试再从 UI 侧验证:两页游标取齐后三帖各恰一张。
搜索框保留 demo 交互(客户端过滤已加载多页缓存;契约 v1.3.0 无检索端点),
过滤中不渲染尾部三态(翻页语义混淆);无命中沿用既有 `EmptyState`
## 2. 组件落位
| 组件 | 落位 | 说明 |
|------|------|------|
| `PostCard` | `lib/core/widgets/post_card.dart` | 三形态:单图(mediaCount≤1 有封面)通栏出血 4:3;多图(mediaCount>1)走 PostMediaGrid 折叠封面;纯文字正文放宽 6 行、15/1.6。头部 `PetAvatar` sm32 + 名字 14/w700 + 相对时间 12 `inkSoft`;求助帖追加 `TagPill(accent)`。次级文字全部显式 `inkSoft`DEBT-2 零新增) |
| `PostMediaGrid` | `lib/core/widgets/post_media_grid.dart` | 展示态:列数规则 2/4→2 列、3/5–9→3 列,格间距 4、圆角 sm12;超 9 图末格 `ink` 80% scrim + 白字 +N 20/w80005 §5.1 精算,60% 档弃用)。**形态偏差**FeedCard 契约只带 coverImage+mediaCount(裁剪形态),Feed 卡多图实渲染为 4:3 封面 + 右下 +N 胶囊角标(同 80% scrim 精算);真九宫格留给详情/发布页全量媒体场景。编辑态(+格/删除角标)随 T3-17 扩展 |
| `LikeButton` | `lib/core/widgets/like_button.dart` | 点赞/收藏参数化一件:未激活 `inkSoft`;点赞激活 `error` 图标 + `errorDark` 计数(demo `Colors.red` 3.13:1 修订清零,D6);收藏激活 `accentDark`。触控 44×44 |
| `FeedSkeleton` | `lib/core/widgets/feed_skeleton.dart` | 05 §3.7 单元结构;0.6↔1.0 呼吸 1200ms`disableAnimations` 静止 |
| `SignedNetworkImage` | `lib/core/network/signed_network_image.dart` | 预签名 URL 缓存 key 剥离 `X-Amz-*` 签名参数(大小写不敏感、保留其余 query),`RemoteImage` 全仓换用——同对象两次响应 URL 必然不同,剥签名后命中同一 ImageCache 条目,未命中仍以完整签名 URL 请求 |
| `community_display.dart` | `lib/features/community/` | 相对时间、加载失败话术、降级作者「宠友」统一占位(`isDegraded` 一个判定口 + `PetAvatar` 无图占位形态) |
**T3-14 互动取舍**(工单预留的选项里选了禁用态):demo 详情页按
`appState.posts` 查 demo id,无法渲染服务端 postId 的真实帖,导航过去即崩;
故整卡点按先弹 SnackBar「帖子详情正在接入真实数据」,点赞/收藏/评论/分享
按钮为**纯展示禁用态**(真实计数与激活态照常渲染,`onPressed` 传 null)。
T3-15 详情页重写后接导航,T3-15/16 接 ToggleSync 与激活动画。
主壳装配:`app.dart` 注入 `communityController` + `feedAnalytics`
`MainShellPage``HomePage``isActive = currentIndex == 0` 驱动浏览段);
`openPost(PostModel)` 保留给 create demo 流(T3-17 收编)。home 对
`AppState.posts` 的消费清零,`posts` 字段本体随 T3-15/17 退役。
## 3. 曝光结算设计(feed_viewed / feed_load_failed
一句话:**浏览段聚合**——进入 Feed 面开段,离开(切 Tab / 切服务分段 /
退后台 / 页面销毁)时结算发**一条** `feed_viewed`postId 只作段内内存
去重键、绝不上报(06 §1.2 裁定 + 隐私红线 2)。
- **判定**:卡片可见面积 ≥50%(列表视口与卡片 RenderBox 纵向交叠比例)且
驻留 ≥500ms;驻留计时在 `FeedViewSegment``feed_exposure.dart`),跌破
阈值/滚出视口即取消。扫描统一调度到 post-frame(滚动通知发生在本帧布局
前,同步读 RenderBox 是旧位置——实测踩到,已修)且一帧至多一次。
- **计数口径**`refreshCount` = 用户下拉/错误重试(首屏自动预取不计);
`loadMoreCount` = 触底翻页请求(含尾部显式重试);`durationMs` 前台
时长(退后台即结算,段天然前台连续),30 分钟截断。
- **生命周期**`WidgetsBindingObserver` 只在离开 resumed 的**第一次**变更
结算(inactive→hidden→paused 级联不重复,SessionTracker 同款处理);
回前台若仍在 Feed 面开新段。`settle()` 幂等,一段恰一条。
- **feed_load_failed**refresh / load_more 双路,`failureReason` 网络归并
口径同 pet 域(断网/超时/5xx → network_error),`errorCode` 仅业务码、
`httpStatus` 由五位码推导;**会话失效不上报**(应用即将回登录页)。
- **页名核对**Feed 属首页 Tab`page_viewed(home)` 由既有 Tab 补点覆盖,
无新增页名;`post_detail` 枚举已在(T3-15 接线导航后自动生效)。
两事件线上验证见 §5(白名单 202 accepted + `platform.product_events` 落库)。
## 4. 测试数变化
| 项 | 基线 | 本单后 |
|----|------|--------|
| flutter test | 379 | **421(+42,另 1 个既有默认跳过冒烟)** |
| flutter analyze | 0 | 0 |
| dart format | 无 diff | 无 diff |
新增分布:home_page widget 测试 13(四态 4、尾部三态与翻页 3、刷新失败
序列 1、曝光结算 4——切 Tab/快速滑过/退后台/切分段、取舍与搜索 2)、
PostCard 6(三形态/求助标/降级作者/操作行展示态)、PostMediaGrid +
FeedSkeleton 7、FeedViewSegment 5fake_async 控驻留时序)、FeedAnalytics 4
(属性逐字段 + 异常映射)、缓存 key 与 provider 判等 7。另
`integration_test/feed_live_test.dart` 桌面真链路 1 条(环境变量门控,
默认跳过不计入套件)。
实现期修正两处(widget 测试暴露):尾部失败态被滚动通知自动重试冲掉
(加 idle 守卫);回前台 `_lastLifecycle` 读旧值导致不开新段(resumed
分支先置状态)。
## 5. compose 真链路实测
后端 patbond-api dev@8089c06(含 feed 域白名单),六容器 `docker compose
up -d --build` 全部 Up、postgres/minio healthy。
**(a)数据种子 + 接口链路(curl)**:注册一次性账号 → 发 26 帖
(24 纯文字 + 1 单图 + 1 双图,图走 media 两步上传:预签名 PUT 直传
MinIO → confirm → ready assetId 引用发帖,全部 published)。
`GET /api/v1/feed?limit=20` 首页 20 条 hasMore=true → 携 nextCursor 取第二页
6 条 hasMore=false**两页零重叠、26 条取齐**;封面预签名 GET 回读字节与
上传原件 `cmp` 一致。`feed_viewed` / `feed_load_failed` 按客户端真实 payload
形状 `POST /api/v1/events` → 双双 202 accepted`platform.product_events`
落库 props 完整(feedTab/durationMs/impressionCount/loadMoreCount/
refreshCountloadType/failureReason)。
**bLinux 桌面真跑(integration_test**
`PATBOND_FEED_LIVE=1 flutter test integration_test/feed_live_test.dart -d linux`
——真实 App 桌面渲染管线 + 真实 HTTP + MinIO 预签名图片(仅注入内存
token 存储,桌面无 keyring):注册 → UI 登录 → Feed 首屏真数据卡片
(含单图/多图 +1 角标卡)→ fling 触底游标翻页至「没有更多了」且最早
一帖(#1)在列(两页取齐直接证据)→ 回顶下拉刷新列表仍健。**一次通过**
(约 25s)。测后 `docker compose down` 干净退出,patbond-api 零改动。
**遗留观察**:桌面端 analytics 真实上报因 platform 枚举不含桌面值被服务端
整批拒绝(既有已知约束,device-verification 通用前置已记载),不影响
本单验证((a) 已按契约 platform 验真);Feed 图片加载的真机表现(蜂窝
网络、缓存命中、局域网/公网 MinIO 可达性)预登记补全见 device-verification
M3 第 3 项。
## 6. 遗留与交接
- T3-15:详情页重写后把 PostCard `onTap``openPost` 导航(页名
`post_detail` 既有)、评论钮锚点;LikeButton 接 ToggleSync + §3.5 激活
动画与回滚零动画;`AppState.posts` 消费面只剩 create/post_detail。
- T3-16/17:收藏交互、发布页(PostMediaGrid 编辑态 + UploadProgressOverlay
已在)。
- 多图九宫格全量形态:详情页拿到 `Post.media` 全量后启用(PostMediaGrid
列数规则与 +N 已就绪并有测试)。
---
**Frontend DeveloperFlutter**
**日期**2026-09-09