Files
patbond-doc/docs/development/iterations/iteration-3/24-feed-page-report.md
T
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

10 KiB
Raw Blame History

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-apicreate/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 EmptyStateIllustrationforum_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)。次级文字全部显式 inkSoftDEBT-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 呼吸 1200msdisableAnimations 静止
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 + feedAnalyticsMainShellPageHomePageisActive = 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;驻留计时在 FeedViewSegmentfeed_exposure.dart),跌破 阈值/滚出视口即取消。扫描统一调度到 post-frame(滚动通知发生在本帧布局 前,同步读 RenderBox 是旧位置——实测踩到,已修)且一帧至多一次。
  • 计数口径refreshCount = 用户下拉/错误重试(首屏自动预取不计); loadMoreCount = 触底翻页请求(含尾部显式重试);durationMs 前台 时长(退后台即结算,段天然前台连续),30 分钟截断。
  • 生命周期WidgetsBindingObserver 只在离开 resumed 的第一次变更 结算(inactive→hidden→paused 级联不重复,SessionTracker 同款处理); 回前台若仍在 Feed 面开新段。settle() 幂等,一段恰一条。
  • feed_load_failedrefresh / load_more 双路,failureReason 网络归并 口径同 pet 域(断网/超时/5xx → network_error),errorCode 仅业务码、 httpStatus 由五位码推导;会话失效不上报(应用即将回登录页)。
  • 页名核对Feed 属首页 Tabpage_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 acceptedplatform.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 onTapopenPost 导航(页名 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