# 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 测试 | |----|------|------|------------| | 首屏 loading(initial/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/w800(05 §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 5(fake_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/ refreshCount;loadType/failureReason)。 **(b)Linux 桌面真跑(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 Developer(Flutter)** **日期**:2026-09-09