- 01 任务拆解:用户实测 6 项反馈的分类与处置;**数据模型审计推翻迁移预估** (nickname/avatar_asset_id 列 V1/V3 早已存在、purpose 白名单是配置项)→ 本批零迁移 - 02 第一批已交付:中文本地化 + 日期录入收口 + 花费卡月份(flutter 502→526) - 6 项待拍板(首页 demo 裁剪范围、获赞端点形态、头像写权限档等) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# 01 M3.5 任务拆解:体验补齐
|
||||
|
||||
**迭代定位**:M3 收官(v0.3.0 发布)后,用户桌面实测反馈 6 项问题的补齐迭代。规模远小于 M1~M3,不做 8 角色开工分析。
|
||||
**执行人**:主会话(PM agent 两次卡死未落盘,实情由主会话独立审计取得)
|
||||
**日期**:2026-09-10
|
||||
|
||||
---
|
||||
|
||||
## 0. 缘起与范围
|
||||
|
||||
用户 2026-09-10 在 Linux 桌面实测 M3 成果,反馈 6 项问题。分类后:
|
||||
|
||||
| 用户反馈 | 定性 | 归属 |
|
||||
| --- | --- | --- |
|
||||
| ① 日历英文 | 缺陷(从未配本地化) | **第一批已修**(M3.5-01) |
|
||||
| ② 月份只能 `< >` 切 | 可用性缺陷,已实际致误录 | **第一批已修**(M3.5-02) |
|
||||
| ⑤ 本月花费 ¥0 | **不是 bug**:记录在 2026-04-09、当天 2026-09-10,9 月确实为 0;根因是 ② | **第一批已修** UI 可自查性(M3.5-03) |
|
||||
| ③ 宠物无头像上传 | ADR-010 剪出项,M3 媒体链路已就位 | 本批 |
|
||||
| ④ 资料页无法改昵称/头像、显示 demo | 代码层缺口 | 本批 |
|
||||
| ⑥ 资料页关注/获赞/作品是假数据 | 同 ④(同一 demo 页) | 本批 |
|
||||
| (未提)首页顶部 demo | 混合性质,须裁剪 | 本批部分 |
|
||||
|
||||
**本批范围一句话**:把「用户资料(昵称+头像)与宠物头像」从 demo 打通为真实读写,资料页数据真实化,并裁剪首页 demo 中属本迭代的部分。
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据模型实情审计(**推翻了原先的迁移预估**)
|
||||
|
||||
开工前一度以为需要 Flyway V6 加列。**逐一核实原始 SQL 与代码后确认:所需列全部早已存在,本批零迁移。**
|
||||
|
||||
| 需求 | 原预估 | **实情(已核实)** |
|
||||
| --- | --- | --- |
|
||||
| 用户昵称 | `identity.users` 无 nickname 列,需加 | **V1 第 63 行就有** `nickname varchar(32)`,含 `ck_users_nickname`(btrim + 1~32 长度);`UserRepository.insertUser(id, username, nickname, phone)` 注册时已在写 |
|
||||
| 用户头像 | 需加列 | **V1 第 67 行就有** `avatar_asset_id uuid`,含 `fk_users_avatar_asset`(→`media.assets` ON DELETE SET NULL)与索引 `ix_users_avatar_asset` |
|
||||
| 宠物头像 | 需加列 | **V3 第 64 行就有** `avatar_asset_id uuid REFERENCES media.assets(id) ON DELETE SET NULL`,含索引 `ix_pets_avatar`。但 **pet 模块代码零处读写它**(grep 无命中) |
|
||||
| 获赞总数 | 需加冗余列 | `community.posts` 已有 `like_count/comment_count/bookmark_count` 冗余列(M3 写侧同事务维护),`SUM(like_count)` 即可 |
|
||||
| 头像 media 用途 | 需加枚举/约束迁移 | `media.assets.purpose` 为 `varchar(32) NOT NULL` **无 CHECK 约束**;白名单在 **配置项** `MediaProperties.allowedPurposes = List.of("post_image")` —— 加新用途只改配置 + 契约枚举 |
|
||||
|
||||
**已就位可直接复用的能力**:
|
||||
- `/internal/users/profiles` 已返回 `PublicProfileResponse(userId, nickname, avatarAssetId)`,**nickname→username 回退在 SQL 层完成**(M3 T3-05 交付)。即 Feed 作者名一旦用户设了昵称即自动生效,无需改社区侧。
|
||||
- media 两步上传(user :8082)+ 客户端 `MediaUploader` 六态编排(M3 T3-03/T3-13)。
|
||||
- `MediaAssetGateway` 的 asset 校验先例(ready + 属本人,M3 T3-04)。
|
||||
- `GET /api/v1/me/posts`(含草稿)、`GET /api/v1/me/bookmarks`、`GET /api/v1/users/{id}/follow-stats` 均已实现。
|
||||
|
||||
**真实缺口只在代码层**:`MeResponse` 只有 `(userId, username, phone, createdAt)` 缺昵称/头像;**无任何写接口**(无 `PATCH /me`);pets 不读写头像列;获赞总数无端点。
|
||||
|
||||
---
|
||||
|
||||
## 2. 工单拆解
|
||||
|
||||
编号自 **T3.5-04** 起(01~03 已由第一批客户端修复占用)。
|
||||
|
||||
### 第一波:后端与契约
|
||||
|
||||
#### T3.5-04 用户资料读写(user 模块)
|
||||
- **仓库**:patbond-api(patbond-user)
|
||||
- **描述**:`GET /api/v1/me` 响应补 `nickname`、`avatarUrl`(预签名 GET,沿用 M3 私有桶签名读口径,URL 会过期不得持久化);新增 `PATCH /api/v1/me` 支持改 `nickname` 与 `avatarAssetId`(置 null 即清除头像)。校验:nickname 与 `ck_users_nickname` 对齐(btrim、1~32);avatarAssetId 必须 `status='ready'`、属当前用户、`purpose='user_avatar'`(复用 MediaAssetGateway 同类校验语义,错误码沿用 40405/42203)。`MediaProperties.allowedPurposes` 增 `user_avatar`。
|
||||
- **验收**:设昵称后 `/me` 与 `/internal/users/profiles` 双端一致;清空昵称回退 username(回退逻辑已在 SQL 层,勿重复实现);非 ready/非本人/错 purpose 的 asset 被拒;六类路径覆盖。
|
||||
- **依赖**:无。**规模**:M
|
||||
|
||||
#### T3.5-05 宠物头像读写(pet 模块)
|
||||
- **仓库**:patbond-api(patbond-pet)
|
||||
- **描述**:`PATCH /api/v1/pets/{petId}` 支持 `avatarAssetId`(含置 null);宠物详情/列表/FeedCard 无关响应补 `avatarUrl`(预签名 GET,community 侧本地现签先例见 M3 T3-04)。asset 校验复用 `MediaAssetGateway`(ready + 属本人)+ `purpose='pet_avatar'`。权限沿用 `PetAccessService`:改头像属 **WRITE 档**(owner+caregiver)还是 **MANAGE 档**(仅 owner)——见待拍板 D3.5-3。`allowedPurposes` 增 `pet_avatar`。
|
||||
- **验收**:owner 设头像后详情返回可访问 URL;viewer 改被拒;version 乐观锁沿用 40902;非法 asset 被拒。
|
||||
- **依赖**:无(与 T3.5-04 可并行,但同仓需串行提交)。**规模**:S~M
|
||||
|
||||
#### T3.5-06 获赞总数聚合(community 模块)
|
||||
- **仓库**:patbond-api(patbond-community)
|
||||
- **描述**:提供当前用户的社区统计:获赞总数(`SUM(like_count)` over 本人未删帖)、作品数(已发布帖数)。端点形态见待拍板 D3.5-2(新增 `GET /api/v1/me/community-stats` vs 扩展既有 follow-stats)。**不引入新冗余列**——写侧维护成本高于读侧聚合收益,且量级远未到瓶颈。
|
||||
- **验收**:草稿/软删帖不计入;空数据返回 0 而非 null;口径写入契约描述。
|
||||
- **依赖**:无。**规模**:S
|
||||
|
||||
#### T3.5-07 契约冻结 v1.4.0
|
||||
- **仓库**:patbond-doc(openapi.yaml)+ patbond-api(快照同步)
|
||||
- **描述**:沿用 M2/M3 迭代式冻结:草案(TODO-FREEZE 标注待定型点)→ 随 04/05/06 实现定型回填 → 拍板 → 合入 **v1.4.0** → **四模块字节级快照同步**(pet/auth/community/user 的 `src/test/resources/contract/`)→ 契约矩阵扩展新操作全响应格。
|
||||
- **验收**:`mkdocs build --strict` 通过;契约矩阵零漂移;升版后 CI 绿(**漏快照同步必红**,见 iteration-3/19)。
|
||||
- **依赖**:T3.5-04/05/06 定型。**本波闸门,不冻结不放行第二波前端。规模**:M
|
||||
|
||||
### 第二波:客户端
|
||||
|
||||
#### T3.5-08 资料页真实化 + 编辑页
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:`lib/features/profile/profile_page.dart`(现 176 行全硬编码 demo:「萌宠新手(豆豆家长)」/24/1.8k/2)替换为真实数据——昵称(空则 username)、头像、获赞、作品数、关注数;新增编辑页(昵称输入 + 头像上传复用 `MediaUploader`,复用 M3 的 gating/失败语义);四态齐备。
|
||||
- **验收**:登录 `llx` 显示 `llx` 而非 demo 文案;改昵称后 Feed 作者名同步(同一后端回退链);头像上传全链路;四态有 widget 测试。
|
||||
- **依赖**:T3.5-07 冻结。**规模**:L
|
||||
|
||||
#### T3.5-09 宠物头像上传接线
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:宠物详情页头像的铅笔角标(**当前已渲染但无功能**)接 `MediaUploader`;列表/详情展示真实头像(`SignedNetworkImage` 复用,缓存 key 剥签名参数已就位);无头像回退现有爪印占位。
|
||||
- **验收**:上传后详情与列表同步显示;viewer 不显示编辑入口;失败态可重试。
|
||||
- **依赖**:T3.5-07 冻结。**规模**:M
|
||||
|
||||
#### T3.5-10 首页 demo 裁剪
|
||||
- **仓库**:patbond-flutter
|
||||
- **描述**:首页顶部 demo 分项处置——问候语「下午好,豆豆」改真实昵称(**本批做**);天气/位置(接外部服务,**建议出本批**);圈子「柴犬圈/猫咪圈/救助站」(实为话题,ADR-018 已剪出,**建议出本批**);促销卡「新用户首单立减 ¥20」(属 M5 服务域,**建议出本批**)。见待拍板 D3.5-1。
|
||||
- **验收**:按拍板结果,保留项标注为「刻意保留的占位」并在报告登记,避免下次实测重复反馈。
|
||||
- **依赖**:T3.5-04 定型(昵称字段)。**规模**:S~M
|
||||
|
||||
---
|
||||
|
||||
## 3. 波次与关键路径
|
||||
|
||||
```text
|
||||
第一波: T3.5-04 / T3.5-05 / T3.5-06(同仓串行提交)→ [T3.5-07 契约冻结 v1.4.0 闸门]
|
||||
第二波: T3.5-08(L) / T3.5-09 / T3.5-10
|
||||
```
|
||||
|
||||
预计一到两波收,无 L 工单堆叠在关键路径(仅 T3.5-08 一个 L)。发布按 releases.md 的**新流程走 PR**(main 已受保护,首发的直推写法已作废)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 待拍板清单
|
||||
|
||||
| # | 决策 | 选项与影响 | 建议 |
|
||||
| --- | --- | --- | --- |
|
||||
| D3.5-1 | **首页 demo 裁剪范围** | 天气/位置需接外部服务(和风天气类,含 key 管理与配额);圈子=话题(ADR-018 剪出);促销卡属 M5 服务域 | **仅做问候语真实化**,其余三项留待对应里程碑,并在代码注释与报告显式标注「刻意保留的 demo 占位」 |
|
||||
| D3.5-2 | **获赞总数端点形态** | A. 新增 `GET /api/v1/me/community-stats`(语义清晰、可扩展);B. 扩展既有 `follow-stats`(少一个端点,但语义混杂——它现在是「某用户的关注数」,加"我的获赞"会变成两种主体) | **A** |
|
||||
| D3.5-3 | **宠物头像的写权限档** | WRITE(owner+caregiver 都能改)vs MANAGE(仅 owner) | **WRITE**——头像属日常照护信息,与体重/疫苗同档;照护人本就能改这些 |
|
||||
| D3.5-4 | **nickname 唯一性** | 现 DB 无唯一约束(仅长度/btrim CHECK)。允许重名(社区常见,靠 userId 区分)vs 加唯一约束(需迁移,破本批零迁移前提) | **允许重名**,不加约束 |
|
||||
| D3.5-5 | **注册时是否让用户填昵称** | 现注册只收 username/phone/password(nickname 走 `insertUser` 但值来源需确认)。加一个可选昵称输入 vs 保持不填、注册后到资料页设 | **保持不填**(少一步注册摩擦),资料页可设 |
|
||||
| D3.5-6 | **版本号** | v0.3.1(补丁语义)vs v0.4.0(含新端点,属功能增量) | **v0.4.0**——加了端点与字段,不是纯修补 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险清单
|
||||
|
||||
| # | 风险 | 缓解 |
|
||||
| --- | --- | --- |
|
||||
| R1 | **契约升版漏同步快照** → 四模块守卫测试全红 | 冻结与快照同步放**同一工单**(T3.5-07)连贯执行,iteration-3/19 已有可照抄的操作序列 |
|
||||
| R2 | 头像 URL 是**会过期的预签名 GET**,客户端若持久化会出现"图突然裂" | 沿用 M3 纪律:URL 不入本地存储;`SignedNetworkImage` 缓存 key 已剥签名参数 |
|
||||
| R3 | 同仓(patbond-api)三个后端工单并行会抢工作树 | 同仓串行派工(M2/M3 已验证的模式) |
|
||||
| R4 | 首页 demo 裁剪范围失控,滑向"顺手把首页重做一遍" | D3.5-1 钉死范围;保留项显式登记,避免反复 |
|
||||
| R5 | 头像功能上线后,真机验证清单需新增项(弱网上传头像、头像缓存) | 收口时按维护约定登记进 `development/device-verification.md` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 与既有纪律的衔接
|
||||
|
||||
- **零迁移**:本批不新增 Flyway 版本(下一个版本号仍为 V6,留给后续真正需要建表的迭代)
|
||||
- **契约先行**:T3.5-07 冻结前,前端不得依赖未冻结字段
|
||||
- **分支保护**:日常推 dev;发布走 PR(releases.md「发布后生效的纪律」)
|
||||
- **路径参数化**:文档内命令一律 `cd <你的工作区>/<仓名>`
|
||||
@@ -0,0 +1,289 @@
|
||||
# M3.5 第一批 · 客户端体验修复(Flutter)
|
||||
|
||||
- **单号**:M3.5-01 中文本地化 / M3.5-02 日期选择器可用性 / M3.5-03 「本月花费」卡可自查
|
||||
- **仓库**:`patbond-flutter`,分支 `dev`(`main` 已受保护,本单只推 dev)
|
||||
- **基线**:`dev` HEAD `0e87413`,502 测试全绿
|
||||
- **范围红线**:纯客户端。不动 `patbond-api`、不动 `openapi.yaml`、不碰契约。
|
||||
用户反馈的另外 3 项(宠物头像、用户资料编辑、资料页真实数据)需契约变更,
|
||||
本单不涉及,留给第二批走正式迭代流程。
|
||||
|
||||
---
|
||||
|
||||
## 1. 三项修复:根因与修法
|
||||
|
||||
### 1.1 M3.5-01 中文本地化(根因:完全没配)
|
||||
|
||||
**根因**:`lib/app/app.dart` 的 `MaterialApp` 从一迭代建起就没有
|
||||
`localizationsDelegates` / `supportedLocales` / `locale`,`pubspec.yaml` 也没有
|
||||
`flutter_localizations`。Flutter 在缺 delegate 时**静默**回退内置的
|
||||
`DefaultMaterialLocalizations`(只有英文),于是业务自绘文案全中文、Material
|
||||
内置组件全英文,同一个弹窗里中英混排。实测确认的英文兜底文案:
|
||||
|
||||
| 位置 | 英文兜底 | 挂上 zh-CN 后 |
|
||||
| --- | --- | --- |
|
||||
| 日期选择器标题 | `Select date` | 选择日期 |
|
||||
| 确认 / 取消 | `OK` / `Cancel` | 确定 / 取消 |
|
||||
| 手输模式标签 | `Enter Date` | 输入日期 |
|
||||
| 模式切换按钮 tooltip | `Switch to input` / `Switch to calendar` | 切换到输入模式 / 切换到日历模式 |
|
||||
| 手输格式报错 | `Invalid format.` | 格式无效。 |
|
||||
| 越界报错 | `Out of range.` | 超出范围。 |
|
||||
| 手输提示格式 | `mm/dd/yyyy` | `yyyy/mm/dd`(zh 顺序年在前) |
|
||||
| 月份年份表头 | `September 2026` | 2026年9月 |
|
||||
|
||||
**修法**:
|
||||
|
||||
1. `pubspec.yaml` 加 `flutter_localizations`(sdk 依赖)与 `intl: ^0.20.2`
|
||||
(`flutter_localizations` 的日期符号/数字格式底座,显式直接依赖以锁版本)。
|
||||
2. 新增 `lib/app/app_localization.dart`:把 `Global{Material,Cupertino,Widgets}Localizations.delegate`
|
||||
三件套、`appSupportedLocales`、`appLocale = Locale('zh','CN')` 收在一处常量。
|
||||
**收在一处的理由**:widget 测试若只 `pumpWidget(MaterialApp(home: ...))` 而
|
||||
不挂 delegate,测到的「中文」是假的(仍是英文兜底);测试直接引用同一份常量,
|
||||
生产与测试不会各配一套而漂移。
|
||||
3. `lib/app/app.dart` 的 `MaterialApp` 挂上三者。
|
||||
4. **单语言 zh-CN**(不列 `Locale('en')`):避免设备语言为英文时回退英文,
|
||||
再次造出「业务中文 + 组件英文」的混排。
|
||||
|
||||
**顺带修的配色**(用户截图里日期选择器是暗红棕,脱离品牌色):
|
||||
|
||||
根因是 `buildAppTheme()` 里 `ColorScheme.fromSeed(seedColor: #FF6F4C)` 派生出的
|
||||
M3 调和色被日期选择器直接吃掉,而项目从未定制 `datePickerTheme`。新增的
|
||||
`_datePickerTheme` **只复用 05 号规范(iteration-2/05、iteration-3/05)已审计过
|
||||
的色对,不新造任何色值**:
|
||||
|
||||
| 位置 | 色对 | 对比度 | 来源 |
|
||||
| --- | --- | --- | --- |
|
||||
| 头部(帮助文字 + 标题 + 模式切换图标) | `surfaceTint #FFE8D6` 底 + `primaryDark #7A2E12` | 7.98:1 | 选中 chip 同款(iteration-2/05 §3 D7) |
|
||||
| 年份下拉 / 上下月箭头 | 白底 + `primaryDark` | 8.74:1 | 同上族 |
|
||||
| 选中日 / 选中年 | `primaryStrong #D6431A` 实底 + 白字 | 4.49:1 | FAB / 头像徽标同款(iteration-2/05 §2) |
|
||||
| 今日(未选中)文字与 1.5px 描边 | 白底 + `primaryStrong` | 4.49:1(非文字门槛 3:1) | 同上 |
|
||||
| 未选中日 / 年 | 白底 + `ink #3E2A1F` | ≥12:1 | 正文主色 |
|
||||
| 星期表头 | 白底 + `inkSoft #6B5A4A` | 6.59:1 | 承载信息的次级文字(DEBT-2) |
|
||||
| 越界不可选日 | 白底 + `muted #9C8977` | 3.36:1 | 禁用态,DEBT-2 允许的 `muted` 用途 |
|
||||
| 确定 | 白底 + `primaryStrong` 文字 | 4.49:1 | 可点击文字链接 |
|
||||
| 取消 | 白底 + `inkSoft` 文字 | 6.59:1 | 次级动作 |
|
||||
|
||||
另外 `headerHeadlineStyle` 取 22px(默认 32):中文 `formatMediumDate` 是
|
||||
「9月10日周四」5~6 字,横屏侧栏头部宽度下 26px 起就折行,实测截图确认 22 一行放得下。
|
||||
弹窗形状对齐 `cardTheme`(radius `xl` 24 + `border` 描边),`elevation: 0` +
|
||||
`surfaceTintColor: transparent` 去掉 M3 的紫调 tint 叠色。
|
||||
|
||||
### 1.2 M3.5-02 日期选择器可用性(根因:月份只能逐月切)
|
||||
|
||||
**根因**:Flutter 原生 `showDatePicker` 的日历模式只给了**年份网格**,月份必须靠
|
||||
`<` `>` 逐月点。用户从 9 月要回到 4 月得点 5 次,**已实际造成误录**——他把当月
|
||||
(2026-09)的就医记录记成了 2026-04-09,进而误判「本月花费 ¥0」是聚合坏了。
|
||||
次要根因是手输快路虽然原生就有(头部铅笔按钮),但在英文兜底下提示是
|
||||
`mm/dd/yyyy`、报错是 `Invalid format.`,中文用户看不懂也不敢用。
|
||||
|
||||
**修法**:新增 `lib/core/widgets/app_date_picker.dart` 共享层,7 处调用点全部收口。
|
||||
|
||||
- `pickAppDate({context, initialDate, firstDate, lastDate})`:
|
||||
- `initialEntryMode: DatePickerEntryMode.calendar`(日历首屏,**保留**头部铅笔
|
||||
切手输)。
|
||||
- `initialDate` 自动夹进 `[firstDate, lastDate]`,防原生越界断言(调用方常传
|
||||
「当前值 ?? 今天」,而「到期日期」的 `firstDate` 就是今天,历史值可能已越界)。
|
||||
- 返回值统一 `dateOnly()` 抹掉时分秒。
|
||||
- **中文文案一律交给本地化,不在此硬编码**(不传 `helpText`/`confirmText`/
|
||||
`fieldHintText` 等),避免两处文案漂移。
|
||||
- `AppDateFieldTrailing({firstDate, lastDate, onToday, enabled})`:日期行尾部统一
|
||||
形态 = 「今天」按钮 + 日历图标。今天越界时按钮自动隐藏,只留图标;
|
||||
`enabled: false`(提交中)时按钮禁用;触控目标 44×44(项目最小口径),
|
||||
文字 `primaryStrong` 白底 4.49:1,带 `设为今天` tooltip。
|
||||
- 各调用点原有的 `firstDate` / `lastDate` 业务约束**原样传入,一字未改**
|
||||
(健康事件 `lastDate: now` 不许未来、到期日 `firstDate: now` 不许补记过去、
|
||||
疫苗 `allowFuture` 双态、生日 `lastDate: now`)。已由 widget 测试直接断言
|
||||
`DatePickerDialog.firstDate/lastDate`,防后续改动悄悄放宽。
|
||||
|
||||
### 1.3 M3.5-03 「本月花费」卡可自查(根因:无法自证记到哪个月)
|
||||
|
||||
**根因**:卡片标签硬编码「本月花费」,而服务端 `summary.monthlyExpense` 本就返回
|
||||
`month`(ISO year-month,如 `2026-09`,按 `tz` 归月)。用户看不到实际月份,
|
||||
所以无法自证「我这条记录到底落在哪个月」,把正确的 ¥0 当成统计故障。
|
||||
|
||||
**核实结论:不改后端聚合逻辑**。用户记录落在 2026-04-09、当天是 2026-09-10,
|
||||
「本月花费 ¥0」是**正确行为**。本单只让客户端把口径亮出来。
|
||||
|
||||
**修法**:
|
||||
|
||||
- `lib/features/pets/health_record_display.dart` 新增纯函数
|
||||
`monthlyExpenseCardLabel(String month, {DateTime? now})`:
|
||||
- 同年 → 「9 月花费」。
|
||||
- 跨年(服务端归月年份 ≠ 设备当前年份,如设备已跨到 1 月而窗口仍是去年 12 月)
|
||||
→ 「2026/12 花费」补年份消歧。
|
||||
- 串非法 → 退回「本月花费」(不崩、不显示脏值)。
|
||||
- `pet_detail_page.dart` 花费卡标签改用该函数。
|
||||
- 可点提示:`_SummaryCard` 在 `onTap != null` 时右上角补 `chevron_right`
|
||||
(16px,`muted`)。**沿用项目既有可点行/卡的表达方式**(宠物列表卡
|
||||
`pets_page.dart:221`、健康提醒卡 `pet_detail_page.dart:692`、资料页设置行
|
||||
`profile_page.dart:125` 全是 `chevron_right`),不自创。
|
||||
- 同时把整卡包一层 `MergeSemantics`:读屏一次读全「¥128.50,9 月花费,按钮」,
|
||||
而不是两段孤立文字。**没有用 `excludeSemantics`**——那会连带丢掉 InkWell 的
|
||||
可激活性,读屏用户就点不动了。
|
||||
|
||||
---
|
||||
|
||||
## 2. 日期选择器方案的取舍理由
|
||||
|
||||
### 2.1 入口模式:为什么是 `calendar` 而不是 `calendarOnly` / `input`
|
||||
|
||||
| 候选 | 结论 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| `DatePickerEntryMode.calendar`(选用) | ✅ | 日历首屏对「今天/最近几天」(健康记录的绝对多数)一眼可点;头部铅笔按钮保留,日期已知时一行敲完。两条路都在,代价是多一次点击。 |
|
||||
| `calendarOnly` | ❌ | 恰好**砍掉**手输按钮。任务里提到「评估是否该放开」——评估结论是相反方向:它会把「快速录入一个已知日期」这条唯一的快路堵死。 |
|
||||
| `input`(手输首屏) | ❌ | 「记今天」这类高频场景反而更慢(要敲 8 个数字 + 认格式),且首屏不给日历会让不确定日期的用户懵。 |
|
||||
| `inputOnly` | ❌ | 无日历可翻,比现状更糟。 |
|
||||
|
||||
补充:手输这条路**只有在 M3.5-01 之后才真正可用**(此前提示 `mm/dd/yyyy`、
|
||||
报错 `Invalid format.`),所以「本地化」与「日期可用性」实际是同一个修复的两半。
|
||||
|
||||
### 2.2 「今天」快捷键:为什么放在表单行而不是弹窗内
|
||||
|
||||
先说被否掉的方案:**原生 `showDatePicker` 无法注入自定义动作**。
|
||||
`builder` 参数只能包裹整个 `Dialog`,拿不到它的内部选中态,也就没法「把日历跳到
|
||||
今天并选中」;把按钮塞进 `Column` 里还会因为 `Dialog` 在无界高度下贪心布局而
|
||||
溢出,并且按钮会浮在遮罩上与弹窗视觉脱节。自绘一个带月份网格的选择器成本远超
|
||||
本单范围。
|
||||
|
||||
选定方案:**「今天」放在调用方的日期行尾部**(`AppDateFieldTrailing`),
|
||||
一次实现、7 处统一:
|
||||
|
||||
- **更快**:一键落值,连弹窗都不用开(原方案是「开弹窗 → 找今天 → 确定」3 步)。
|
||||
- **绕开根因**:「记今天的事」是健康记录的主场景,这条路整段避开了容易走错的
|
||||
月份导航。
|
||||
- **零风险**:不与 Flutter 弹窗内部结构较劲,不影响 a11y 与布局。
|
||||
- **一致**:一个共享 widget,7 处形态完全相同(5 处原来是裸的日历图标,
|
||||
2 处对话框里原来什么都没有,现在统一成「今天 + 日历图标」)。
|
||||
|
||||
一处判断说明:`pet_form_page` 的「生日(可选)」也挂了「今天」。语义上是
|
||||
「今天出生的新生宠物」,合法但少见;为了 7 处形态一致仍然保留,代价可忽略。
|
||||
|
||||
### 2.3 什么**没有**做
|
||||
|
||||
- 没有实现月份网格选择器(需自绘或引三方包,超出本单「纯客户端小修」范围)。
|
||||
年份网格 + 手输 + 「今天」三条路已经覆盖了实测暴露的全部痛点。
|
||||
- 没有改任何 `firstDate` / `lastDate` 业务约束。
|
||||
|
||||
---
|
||||
|
||||
## 3. 7 处调用点收敛情况
|
||||
|
||||
改造前 `grep -rn showDatePicker lib/` 命中 7 处裸调用;改造后 `lib/` 下
|
||||
`showDatePicker` **只出现在 `app_date_picker.dart` 内部一次**。
|
||||
|
||||
| # | 调用点 | 字段 | 业务约束(未改) | `pickAppDate` | 「今天」 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | `pets/pet_form_page.dart` | 生日(可选) | `[1990, 今天]` | ✅ | ✅ |
|
||||
| 2 | `pets/weight_form_page.dart` | 称重日期 | `[1990, 今天]` | ✅ | ✅ |
|
||||
| 3 | `pets/health_event_form_page.dart` | 发生日期 | `[1990, 今天]`(不许未来) | ✅ | ✅ |
|
||||
| 4 | `pets/care_reminder_form_page.dart` | 到期日期 | `[今天, 今年+5]`(不许补记过去) | ✅ | ✅ |
|
||||
| 5 | `pets/vaccination_form_page.dart` | 接种/下次日期 | `[1990, allowFuture ? 今年+5 : 今天]` | ✅ | ✅ |
|
||||
| 6 | `pets/vaccination_records_page.dart` | 标记完成对话框 · 接种/下次 | 同上 | ✅ | ✅(原无 trailing) |
|
||||
| 7 | `pets/care_reminders_page.dart` | 标记完成对话框 · 完成日期 | `[1990, 今天]` | ✅ | ✅(原无 trailing) |
|
||||
|
||||
删掉的重复代码:7 份手写的 `showDatePicker(...)` 参数块 + 5 份手写的
|
||||
`trailing: const Icon(Icons.calendar_month_outlined, color: AppColors.muted)`。
|
||||
|
||||
测试侧同步:`vaccination_records_page_test` / `care_reminders_page_test` /
|
||||
`vaccination_form_page_test` / `care_reminder_form_page_test` /
|
||||
`health_event_form_page_test` 的 `MaterialApp` 都挂上了 zh-CN delegate,
|
||||
`tester.tap(find.text('OK'))` 相应改为 `'确定'`——让 pets 域的 widget 测试与
|
||||
生产环境一致,而不是在英文兜底下测。
|
||||
|
||||
---
|
||||
|
||||
## 4. compose 桌面实测记录
|
||||
|
||||
### 4.1 环境
|
||||
|
||||
```bash
|
||||
cd <你的工作区>/patbond-api
|
||||
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw -DskipTests package
|
||||
docker compose up -d --build
|
||||
docker compose ps # 六容器:postgres / minio / auth / user / pet / community 全 running
|
||||
```
|
||||
|
||||
```bash
|
||||
cd <你的工作区>/patbond-flutter
|
||||
PATBOND_UX_LIVE=1 flutter test integration_test/client_ux_live_test.dart -d linux
|
||||
```
|
||||
|
||||
新增的 `integration_test/client_ux_live_test.dart` 沿用 M3 既有 live 测试的形态
|
||||
(环境变量门控、默认 skip、不计入常规测试套件),驱动**真实 App**(Linux 桌面
|
||||
GTK 渲染管线 + 真实 HTTP,仅注入内存 token 存储因桌面无 keyring)。
|
||||
|
||||
截图方案说明:本机是 Wayland 会话,X11 的 `import -window root` 取不到根窗口
|
||||
(实测 `exit=1`),改为把整棵 `App` 包一层 `RepaintBoundary` 后 `toImage()`
|
||||
直出真实渲染像素(含 Overlay 里的弹窗),落到 `build/ux-live/*.png`。
|
||||
|
||||
### 4.2 逐步所见
|
||||
|
||||
| # | 截图 | 所见 |
|
||||
| --- | --- | --- |
|
||||
| 01 | `01-pet-form.png` | 建档表单;「生日(可选)」行右侧是新的「今天 + 日历图标」 |
|
||||
| 02 | `02-date-picker-zh.png` | 日期选择器:**标题「选择日期」**、头部 `2026年9月`(原 `September 2026`)、星期表头「一二三四五六日」、底部**「取消」/「确定」**;配色为 peach 头部 + 深棕字 + **珊瑚红实底选中日**(不再是暗红棕);左下角铅笔按钮在位 |
|
||||
| 03 | `03-date-input-zh.png` | 点铅笔切手输:标签**「输入日期」**、输入框预填 `2025/9/1`(zh 年在前)、焦点边框珊瑚色、「取消」/「确定」中文 |
|
||||
| 04 | `04-date-typed.png` | 敲入 `2024/03/15` → 确定 → 表单行显示 `2024-03-15`(**未点任何月份箭头**) |
|
||||
| 05 | `05-today-shortcut.png` | 点「今天」→ 表单行直接变 `2026-09-10`,**弹窗未打开**(`DatePickerDialog` findsNothing 断言通过) |
|
||||
| 06 | `06-pets-list.png` | 档案列表含种子宠物「实测豆豆」 |
|
||||
| 07 | `07-pet-detail-expense-card.png` | 详情页四张数据卡**每张右上角都有 `>` 可点提示**;花费卡显示 **`¥128.50` / 「9 月花费」**,「本月花费」已不存在 |
|
||||
|
||||
### 4.3 花费卡口径的真链路验证
|
||||
|
||||
种子数据经真实 HTTP 下到 pet 服务(`POST /api/v1/pets` +
|
||||
`POST /api/v1/pets/{id}/health-events`,`occurredAt = now`、`amountCents = 12850`),
|
||||
详情页 `GET /pets/{id}/summary?tz=+08:00` 返回 `month: 2026-09` →
|
||||
卡片渲染「9 月花费 ¥128.50」。这正是用户当初无法自证的那一格:记录落在当月才计入,
|
||||
标签现在直接把「当月是几月」写在卡上。
|
||||
|
||||
实测断言全部通过(`00:08 +1: All tests passed!`),收尾 `docker compose down` 已执行。
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试数变化
|
||||
|
||||
| 项 | 改造前 | 改造后 |
|
||||
| --- | --- | --- |
|
||||
| `flutter test` | **502** passed / 2 skipped | **526** passed / 2 skipped(+24) |
|
||||
| `flutter analyze` | No issues | **No issues** |
|
||||
| `dart format` | 无 diff | **无 diff**(151 文件 0 changed) |
|
||||
| live 实测 | — | `client_ux_live_test.dart` 1 passed(门控,不计入 526) |
|
||||
|
||||
新增 24 个测试的分布:
|
||||
|
||||
- `test/core/widgets/app_date_picker_test.dart`(14):`dateOnly`/`today`/
|
||||
`isDateSelectable` 纯函数;日历模式中文文案**实际渲染**(并反向断言
|
||||
`Select date`/`OK`/`Cancel` findsNothing);手输切换按钮在位 + 切换后
|
||||
「输入日期」;手输敲入日期即返回;取消返 null / 确认抹时分秒;
|
||||
`initialDate` 越界夹取;「今天」回调今天且不开弹窗 / 越界隐藏 / 禁用态 /
|
||||
44×44 触控;`datePickerTheme` 色对(含 disabled → `muted`)与**渲染层**
|
||||
选中日 `Ink` 圆底取 `primaryStrong`。
|
||||
- `test/app/app_localization_test.dart`(1):根 `MaterialApp` 实际挂上三件套
|
||||
delegate + `locale zh-CN`,并从运行期 `MaterialLocalizations` 取回中文文案
|
||||
(守住「delegate 一行」不被回删)。
|
||||
- `test/features/pets/health_record_display_test.dart`(3):
|
||||
`monthlyExpenseCardLabel` 同年 / 跨年 / 非法串三组。
|
||||
- `test/features/pets/health_event_form_page_test.dart`(3):先手输改到
|
||||
2026-04-09(复现用户那格)再点「今天」一键回今天且触发 started 埋点;
|
||||
选择器中文 + `firstDate/lastDate` 业务约束不变;提交中禁用「今天」。
|
||||
- `test/features/pets/care_reminder_form_page_test.dart`(2):`firstDate` 就是
|
||||
今天的那一格——未选日期时「今天」也在位且一键清掉必填校验错;
|
||||
约束仍是 `[今天, 今年+5]`。
|
||||
- `test/features/pets/pet_detail_page_test.dart`(1):四张数据卡都有
|
||||
`chevron_right` 可点提示。
|
||||
|
||||
既有测试的口径调整(非新增):`pet_detail_page_test` 两处「本月花费」断言改为
|
||||
实际月份,且样本 `monthlyExpense.month` 改用**当月**串,使断言不随年份漂移
|
||||
(跨年格式由纯函数单测覆盖)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 遗留与移交
|
||||
|
||||
- **未做**:月份网格选择器(需自绘/引包)。若第二批有余量可评估,但当前
|
||||
年份网格 + 手输 + 「今天」已覆盖实测暴露的全部痛点。
|
||||
- **未做**:`SegmentedButton` 选中态仍是 `fromSeed` 派生的粉底(实测截图 06 可见),
|
||||
与品牌 `surfaceTint + primaryDark` 的既有语言不一致。这是 M2 遗留的既有债,
|
||||
不在本单范围,建议并入后续主题收敛单。
|
||||
- **不在本单**:宠物头像、用户资料编辑、资料页真实数据——需契约变更,
|
||||
走第二批正式迭代流程。
|
||||
- **契约**:零改动。`openapi.yaml` 未触碰,`patbond-api` 未触碰。
|
||||
Reference in New Issue
Block a user