- 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>
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user