- 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>
13 KiB
25 M3 第三波:帖子详情页替换 + 互动接线(T3-15/T3-16)
执行日期:2026-09-09
工单:T3-15 帖子详情页整页替换(demo 数据层退役)+ T3-16 互动接线(ToggleSync UI 层),同域合并交付
依赖:21 号(T3-12 数据层,ToggleSync/CommunityController 就位)、24 号(T3-14 组件与遗留交接)、05 号 UI 规范 §2.2/§3.4/§3.5/§4、22 号事件白名单 v3、17 号后端评论/互动语义
提交:patbond-flutter dev 92524da(T3-16 基建)+ f873acf(T3-15/16 页面与接线),基线 8aac8c5,已推送 origin/dev
0. 概要
post_detail_page.dart 整页重写为真实数据:四态首屏、媒体全量渲染(真九宫格 +
全屏大图)、作者卡关注双态、评论区(游标列表 / 输入条创建 / 仅本人可删)全部
落地;点赞/收藏经共享 ToggleSync 接入 Feed 卡片与详情页(同一 controller 实例,
互动状态跨页一致),按 05 号 §4 三层视觉抑制实现;互动域 8 事件挂接完成。
Feed 整卡点按导航详情接通,T3-14 的占位 SnackBar 与禁用态移除。未动
patbond-api;create 页 demo 留给 T3-17。
质量门禁:flutter test 458/458 全绿(基线 421,+37;另 2 个 env 门控
compose 冒烟默认跳过)、flutter analyze 0 问题、dart format --set-exit-if-changed 无 diff、compose 六容器真链路实测通过(§5,含断网
点赞回滚)。
1. 详情页四态与结构(T3-15)
| 态 | 渲染 | widget 测试 |
|---|---|---|
| loading(无内存副本) | 居中转圈;评论区独立骨架 2 个(32 圆 + 圆角 16 块高 72,05 §3.7) | ✓ |
| 内存副本先渲染 | 进入即展示 controller 缓存内容,getPost 后台拉新静默替换(Feed 卡片互动字段一并回写) |
✓ |
| error | InlineErrorBanner(pets 同款话术映射)+ 「重试」;有副本时后台刷新失败不打断阅读 |
✓ |
| 40403 不存在态 | SnackBar「帖子不存在或已被删除」→ 返回 Feed 并触发整体刷新(失效帖剔除);详情 / 评论 / 评论创建三条路径均可触发,单次守卫防重复 pop | ✓ |
| ready | 媒体区 → 作者卡 → 正文卡 → 操作行 → 评论区,底部固定输入条 | ✓ |
结构落点(05 §2.2 对照):
- 媒体区(本单裁定:真九宫格,D11 轮播方案弃用):单图原比例通栏、高度
钳制 [宽×0.75, 宽×1.33](widthPx/heightPx 缺失回落 4:3);多图走
PostMediaGrid全量形态(24 号预留的列数规则 2/4→2 列、3/5–9→3 列与 超 9 折叠「+N」直接生效)。点格进全屏大图:黑底 +InteractiveViewer(03 号拍板 E 选①内置方案,零依赖)+ 横滑翻页 + 双击定点 2.5x 缩放 + 右上「n/N」ink 胶囊(13.50:1)与关闭钮。偏差:05 §2.2 的「下滑关闭」 与 InteractiveViewer 平移手势冲突,本版未做(关闭钮 + 返回手势可退出), 留待 photo_view 复评(03 号 E 的升级条件「体验不达标」)。 - 作者卡:
PetAvatarmd44 + 名字/相对时间;关注双态钮见 §3。 - 正文卡:标题 titleMedium + 求助帖
TagPill(accent)+ 正文 14/1.6 全文 + 「发布于 …」12inkSoft。契约 Post 无话题字段,TopicChip 不涉本单。 - 操作行:与 Feed 卡片同一套组件卡外裸排(demo 的 FilledButton.tonalIcon 弃用);评论锚点钮点按聚焦底部输入框(唤起键盘直接开写)。
- 输入条:surface 底 + 顶部 border 1px 分隔线(demo 缺失,已补)+ isDense 输入框 + filled 发送钮(空文本禁用;发送中 18 转圈锁尺寸)。
- AppBar 分享:占位 SnackBar「分享功能即将上线」(无契约端点)。
2. 评论区(T3-15)
- 游标列表:
(created_at DESC, id DESC)服务端序原样渲染,触底(余量 400)携 nextCursor 补页,失败态只走显式重试(Feed 同款守卫);空态 「还没有评论,来抢沙发」(装饰图标 muted 合法、文案 inkSoft,DEBT-2 零新增)。 - 创建:仓库层 Idempotency-Key 每次提交换新键(21 号已测线上语义);成功
插入列表头 +
adjustCommentCount(+1)同源写入(详情副本与 Feed 卡片 commentCount 一并更新)+ 清空输入收起键盘;失败保留输入 + 按类型话术 SnackBar(40000 →「评论内容不合规」等),撞 40403 走不存在态流程。 - 仅本人可删(UI 呈现):
currentUserId(app.dart 注入 sessionManager.userId) 与评论 author.userId 相等才渲染「删除」入口——权限判定只做 UI 自见性, 服务端 40301/40404 仍是裁决者(17 号 §2.3)。删除经确认弹窗 → 软删成功 剔除 + 计数 -1;40404(已在别处删)本地同步剔除;40301 提示无权限。 - CommentTile 升共享组件(
lib/core/widgets/comment_tile.dart,05 §3.4): PetAvatar sm32 + 气泡(surface/border 1px/圆角 16/padding 12);@ 回复以 「回复 @昵称:」前缀呈现(响应 replyToUser,含降级「宠友」占位);删除 in-flight 转圈锁定。取舍:评论点赞(§3.4 底行右端)无契约端点不渲染; @ 回复的发起 UI 与长按操作 sheet(回复/复制/举报)留待后续工单 (数据层 replyToUserId 已支持,isReply 埋点属性预留)。
3. 互动视觉实现(T3-16,05 §3.5/§4 三层抑制对照)
| 层 | 规范 | 实现落点 |
|---|---|---|
| 即时反馈 | 点按即刻翻转 + 激活动画 | ToggleSync 乐观写入同帧 notify;LikeButton 升 Stateful——点按驱动的激活播 240ms 弹性缩放(1→1.25→1)+ 120ms 图标淡入,取消仅 120ms 颜色渐出无缩放 |
| 连点合并 | 只发最终态 | 由数据层单飞合并承担(在途链只并入 pendingTarget、完成后按最终意图至多补发一次,连点至多两在途);UI 不再叠加 600ms 计时防抖——ToggleSync 已保证「合并后只发最终态」的语义,双状态机会打架(D8 的跨角色确认以 21 号定稿为准) |
| 回滚静默化 | 零动画 + 成对恢复 + SnackBar | 非点按驱动的状态变化(回滚/对账)直接跳变;计数与展示态成对更新(不出现「心已灭计数未减」中间帧);激活动画未播完等播完再跳(§4.3a);toggleError 一次性消费出 SnackBar「操作失败,请重试」(Feed 页与详情页共用消费口,先消费者清空,同帧恰一条) |
| 对账不打扰 | 静默替换计数 | 服务端权威计数与乐观值不同(他人并发)时数字直接替换、无动画(LikeButton 对「状态不变的计数变化」不播任何过渡) |
关注钮同策略(§4.5):乐观翻转、失败直接跳回 + SnackBar;取关先确认
「不再关注 TA?」;本人帖不渲染(自关注 42204 不给触发面);关注状态经
getFollowStats.followedByMe 拉取,拉取失败不渲染钮(不阻塞阅读)。
系统「减弱动态效果」开启时全部动画降级瞬变(LikeButton 与 FeedSkeleton 同口径)。
跨页一致:Feed 卡片与详情页共享同一 CommunityController/ToggleSync 实例,
互动写入经 _writeInteraction 同帧更新详情副本与 Feed 卡片(widget 测试从
UI 侧断言「详情点赞、卡片同帧 +1」)。
4. 埋点挂接清单(8 事件 + 口径)
新增 community_interaction_analytics.dart(22 号白名单键集逐一对齐,
编译期锁死):
| # | 事件 | 触发点 | props | 挂接位置 |
|---|---|---|---|---|
| 1 | post_liked |
点赞成功响应后 | source(feed / post_detail) | CommunityController send 闭包(触点在 toggle 调用处归因) |
| 2 | post_unliked |
取消点赞成功响应后 | source | 同上 |
| 3 | post_favorited |
收藏成功响应后 | source | 同上 |
| 4 | post_unfavorited |
取消收藏成功响应后 | source | 同上 |
| 5 | comment_create_succeeded |
评论创建成功响应后 | durationMs、isReply、textLengthBucket | 详情页提交回调 |
| 6 | comment_create_failed |
评论创建失败 | failureReason、errorCode、httpStatus、attemptSeq | 详情页提交回调 |
| 7 | user_followed |
关注成功响应后 | source=post_detail | 详情页关注钮 |
| 8 | user_unfollowed |
取关成功响应后 | source=post_detail | 详情页关注钮 |
口径说明(widget/单元测试逐字段断言):
- 成功才报:乐观翻转与失败回滚不报(06 §1.4「点赞/收藏/关注不埋失败」); 单飞合并链每个实际抵达服务端并成功的状态变更各报一条(快速连点合并后 至多两条、方向相反,与「成功响应后」字典口径一致)。
- comment_create_started 不发(22 号锁死 unknown);
durationMs取 「输入会话首字符 → 成功响应」(评论无 started 事件,时长随成功事件带出);textLengthBucket分桶 empty/short(≤50)/medium(51–500)/long(>500),精确 字数不出端(红线 1);attemptSeq输入会话内从 1 递增,成功或清空输入重置;httpStatus = code ~/ 100(pet 域同款);会话失效不上报;postId/commentId 等内容 ID 一律不进 props(红线 2)。 - follow UI 判定:详情页作者卡有关注钮(demo 形态保留升级双态),故 user_followed/unfollowed 本单接通;user_profile / follow_list 触点随 后续页面启用。
5. compose 真链路实测
后端 patbond-api dev@8089c06 六容器 docker compose up -d --build 全部
Up、postgres/minio healthy;测毕 docker compose down 干净退出,patbond-api
零改动。
(a)互动一轮(test/smoke/detail_interactions_smoke_test.dart,env 门控
PATBOND_DETAIL_SMOKE=1,生产 ApiClient/Repository/Controller 全真实现):
注册一次性账号 → 发帖(published)→ 点赞({liked:true, likeCount:1})→
重复 PUT 幂等不重复计数 → 收藏/取消(计数 1→0)→ 评论创建
(Idempotency-Key,commentCount 0→1)→ 仅作者删除评论(commentCount
回 0、列表剔除)→ 权威计数逐步对账。一次通过。
(b)断网点赞回滚(同测试内,生产 CommunityController + ToggleSync):
Feed 刷新拿到该帖(likedByMe=true/count=1)→ community 端点整体切至不可达
端口模拟断网(连接拒绝走生产 ApiClient 的真实 ApiNetworkException 链路)→
toggleLike:乐观翻转同帧可见(false/0)→ 请求失败后快照成对回滚
(true/1)、toggleError 为 ApiNetworkException(SnackBar 消费口就位)→
恢复网络再 toggle → 服务端权威终态收敛(likedByMe=false/likeCount=0)。
一次通过(首轮实测暴露测试自身竞态:以乐观值判收敛会早退,已改为轮询
服务端权威终态)。
(c)互动 8 事件白名单验真(curl,客户端真实 payload 形状):
POST /api/v1/events(user :8082)一批 8 条(platform=android)→
202 accepted 8 / rejected 0,platform.product_events 落库 props 完整
(source / durationMs+isReply+textLengthBucket / failureReason+attemptSeq+
errorCode+httpStatus 逐键核对无剥离)。
真机专属项(乐观更新手感:连点合并请求数、回滚动画帧率、跨页一致、减弱 动态降级)已补全 device-verification.md「M3 预登记」第 2 项的步骤与通过标准。
6. 测试数变化
| 项 | 基线 | 本单后 |
|---|---|---|
| flutter test | 421(+1 门控冒烟跳过) | 458(+37,门控冒烟跳过 2) |
| flutter analyze | 0 | 0 |
| dart format | 无 diff | 无 diff |
新增分布:post_detail_page widget 测试 17(四态 5 含 40403 弹回刷新与内存 副本先渲染、媒体九宫格与全屏大图 1、评论区 5——游标补页/失败重试/创建成败 与 attemptSeq/删除权限与 40301、互动 4——乐观翻转/失败回滚/收藏事件/跨页 一致、关注 5)、LikeButton 动画 6(点按激活缩放/取消无缩放/外部零动画跳变/ 动画中回滚等播完/对账静默/禁用态)、CommentTile 3、互动埋点单测 6(键集/ 分桶边界/失败原因映射)、home_page 更新 3(导航接通替换 T3-14 占位断言、 点赞接线成功事件、失败回滚 SnackBar)、helpers 扩展(评论/关注假仓钩子)。 另 compose 冒烟 1 条(env 门控,默认跳过不计入套件)。
7. 遗留与交接
- T3-17 发布页:create 页 demo 数据层(AppState.publishPost/updatePost 与
posts字段本体)随发布页真实化退役;demo 发布流现只回 Feed 不再导航 (demo 详情页已消亡);PostMediaGrid 编辑态 + UploadProgressOverlay 已在。 - @ 回复发起 UI / 长按操作 sheet(举报):数据层与埋点属性(isReply) 已支持,交互留待范围拍板。
- 大图浏览下滑关闭:与 InteractiveViewer 平移手势冲突未做,photo_view 复评条件不变(03 号 E)。
- 真机项:device-verification.md M3 预登记第 2 项待真机执行(连点合并 请求数 ≤2 的抓包核对只能在真机/真网完成)。
Frontend Developer(Flutter) 日期:2026-09-09