Commit Graph
77 Commits
Author SHA1 Message Date
laoyang c11548cb51 首页改造:概述跳锚点、主题色、详情卡、点击响应 (#59 #60 #61 #62 #63 #64 #65)
#59 概述四格数字可点击跳锚点
- 加 Scroller + yEvents/yTasks 坐标字段
- 两个区块挂 onAreaChange 记录 y 偏移
- FbStatCell 加 onTap(整格热区,不是只有数字两个小字)
- 今天日程 -> 今天日程区块;在做/任务/逾期 -> 近期任务区块
  (三者都是任务状态计数,点哪个都是想去看任务,让"逾期"单独跳
    一个不存在的"逾期列表"是骗人的)
- 概述数字本来就来自 d.stat.*(接口真实数据),无需改

#60 各区块标题用模块主题色 + 新建改胶囊按钮
- FbSectionTitle 加 accent 参数(默认空 = 保持原灰,老调用点不动)
- 标题有主题色时加粗一档(光换色辨识度不够)
- 新建从文字链接改成胶囊按钮:底色主题色、白字、全圆角
- 6 处调用点分别传 calAccent/taskAccent/daysAccent/moviesAccent/peopleAccent

#61 全天日程与下方白块无间距
- FbDayTimeline 全天块 bottom padding 6 -> 12
- 6vp 在全天色块实心背景下几乎看不出缝;12 才有明显呼吸
- 与 Column(space:4) 的行间距形成「行内紧、块间松」层次

#62/#63/#64/#65 四个区块加"先看后改"详情卡
根因:5 个 @Builder 全都没有 onClick(memoryRow/movieRow/recentDayRow/
personTile 各自 0 个),所以用户"点了没反应"。

- 新增状态:mvDetail/mvDetailLoading/mvDetailError/mvSeed/pfDetail/
  dayDetail/memDetail,全部计入 sheetCount(否则按返回键直接退出页面)
- openMovieDetail 照 FbMoviesPage.openDetail 的成熟写法用
  FbMovieRepo.inst().detail(),不自己拼接口(parseMovieDetail 不导出)
- 复用三张现成卡:FbMovieDetailSheet / FbPersonSheet / FbDayEditSheet
- 往年今天按 kind 分发:movie 走电影卡;trip_node 拿不到 tripId
  ⇒ 退回通用摘要卡(弹错类型的卡比不弹更糟)
- memorySheet 用 FbSheetRing 就地画,内容可滚动(用户"滑下去看信息")

联系人头像本来就有(personTile 里 if (p.avatar !== "") 分支),无需改。
2026-11-05 13:43:03 +08:00
laoyang aafd83b130 悬浮栏选中底圆角:999 -> 12,不再那么圆鼓 (#58)
用户 2026-11-05:「颜色我感觉透明度什么的还是可以的,但是这个圆有点太圆了,
应该弄成更椭圆一点,效果可能是不是更好啊?」

borderRadius(999) 会把 40 高的块钳成两端完整半圆,看着很圆鼓。
改成 12:圆角半径只占高度的 30%,是"柔和的圆角矩形",
视觉更平更舒展,也与 dock 外框的大圆角(28)形成内外层次。

ArkUI 的 borderRadius 是等比圆角(四角半径相同、正圆圆弧),
没有"椭圆角"(x/y 半径不同)的写法 —— 所以"更椭圆"只能靠减小半径近似,
半径越小圆弧越平。

实机偏圆就降到 10,偏方就加到 14,改动只有这一个数字。
2026-11-05 13:25:06 +08:00
laoyang 5d9902bb82 底部悬浮栏:选中项加灰色椭圆底 (#58)
用户 2026-11-05:「当选中其中一个按钮之后,它除了图标和文字颜色发生变化,
还给它加了一个那种椭圆的那种框那种背景,灰色的。这样更能清晰表示
现在正在被选中是哪一个功能。」

原来选中态只有变色 + 加粗,没有背景,观感偏弱。

结构上关键:不能直接给 Column 加 backgroundColor + borderRadius ——
那个 Column 是 layoutWeight(1),椭圆会撑满整格,看起来像"选中了一整块"。
套两层:外层等分格(透明,负责布局与热区)+ 内层椭圆(自适应内容宽高)。

底色用半透明黑 #14000000 而不是实色灰:
dock 底色是 EBFFFFFF(92% 白叠模糊层),实色灰在深色系统主题下会突兀;
半透明黑与背景混合,浅色下浅灰、深色下深灰,两种主题都协调。

borderRadius 用 999 而非固定值:固定值在标签变长时圆角比例会失调。
2026-11-05 13:21:34 +08:00
laoyang 0c2cf9b5e7 表单规范全量铺开 + 编辑时视频附件不显示 (#56)
① 编辑行程的视频附件不显示

根因:照片墙对所有附件都用 Image 渲染,视频是 .mp4,
Image 加载不出来 ⇒ 空白块 ⇒ 用户以为"视频没带进来"。
按 mime 分流:video/ → 深色底 + ▶(不取首帧,端上解视频首帧很卡)。

② 新增共享组件 FbFormField.ets(GLOBAL-FORM-SPEC)

5 个表单原来各写一份 field()/labelRow(),改一处要改五处,
而且外层结构不一致(有的 Row 有的 Column),批量脚本改坏过两次。
收敛到一个组件,表单只调用不自己实现。

③ 全量铺开

- FbPersonEditSheet  姓名标必填(红色无星号)
- FbEventEditSheet   日程 field 两排
- FbMovieNewSheet    片名/影院/同看的人/海报/观影照片/简介/备注 全部两排
- FbDateTimeField    FbDateTimeRow 改两排(电影+日程同时生效)

规范:标签主题色 + Bold + fontSmall,必填红 #C0533F 无星号,
标签独占第一排、控件第二排整宽。
2026-11-05 12:52:51 +08:00
laoyang ffd6ac5289 编辑行程加附件:用序号强制重建浮层,修回填不全 (#55)
根因:@Param editing 在 V2 里是单向同步 —— 父组件改 editNode 后子组件
的 editing 会更新,但 aboutToAppear 只在首次创建时跑一次。
⇒ 换一条行程再点编辑时,editing 变了但回填不重跑 ⇒ photos 保留上次的值。

修法:加 nodeSheetSeq,点编辑时 +1,组件用 .id(nodeSheet_N) 绑定序号。
序号变化时 ArkUI 销毁旧组件、创建新组件 ⇒ aboutToAppear 必然重跑。

顺带:返回键关闭时也清 editNode,避免下次走到复用路径拿到上一条的 editing。
2026-11-05 12:45:43 +08:00
laoyang 86de59616c 端上:#47~#55 打包 —— 行程双轨、动态类型、表单全局规范 (#53)
#48 计划/实际双轨:nodeAction(start/done/cancel),完成即转实际行程,
    实际行程不再显示状态;新增 actualStartedAt/actualEndedAt
#49 类型/出行方式/车辆 chip 改内部读状态(@Builder 值传参不刷新)
#50 类型点击立即变色、出行方式平铺、参与人显示已选头像
#51 节点类型动态加载(原硬编码5种)、地址 onPick 拿详细地址与坐标
#52 标签改主题色 Bold、必填红无星号、编辑行程标题同步
#53 表单两排版式(标签独占一行解决左右对不齐);电影弹层 layoutWeight 修滚动
#54 地址联想框加时间窗抑制(600ms 内不搜)、候选限高 180

含附件回填诊断日志(DIAG-ATTACH/FILL),待真机确认后移除
2026-11-05 12:41:04 +08:00
laoyang 88fa38ecf9 导航修复:加系统地理编码拿坐标,改用 openLink 拉起高德
## 用户反馈

「智橙的导航那个地方功能不好用,跳转到高德,说是高德提示说什么不支持。
 我记得同步日历它那个是支持的,所以你看一下同步日历是怎么实现的。」

## 对照 SyncCalendar(实测能用)找出两个致命差异

| 环节 | SyncCalendar | 我们原来的实现 |
|---|---|---|
| 拿坐标 | `geoLocationManager.getAddressesFromLocationName()` | 无(只能从地点文字正则抠) |
| 坐标系 | WGS84 → GCJ02 本地转换 | 无 |
| 高德 URI | `amapuri://route/plan/`(**带尾斜杠**) | `amapuri://route/plan`(无斜杠) |
| 无坐标时 | 仍给 dname 尽力而为 | 退到 `amapuri://poi?keyword=` |
| 拉起方式 | `ctx.openLink()` | `startAbility({uri})` 隐式 Want |

**为什么高德报「不支持」:**

1. **没有坐标**。`route/plan` 的 `dlat/dlon` 是**必填**,
   我们只在"用户恰好把坐标粘进地点文字"时才给 ⇒ 绝大多数情况给不出。

2. **退路 `amapuri://poi?keyword=` 在新版高德已废弃** ——
   那正是「不支持」的直接来源。

3. URI 少了尾斜杠(`route/plan` vs `route/plan/`)。

## 另外:原文件有一条未经验证的错误结论

原文写着:

    「拉起方式用 startAbility 的隐式 Want,而不是 openLink:
      openLink 走的是 App Linking,会报 16000019 No matching ability is found」

但 SyncCalendar **就是用 openLink 拉起高德的**,而且它的 `module.json5`
**根本没声明 `querySchemes`** —— 说明 openLink 不需要那套声明。
原结论是推断、未经实测,本次改用 openLink。

## 改动

### FbNav.ets

- 新增 `geocodeBySystem()` —— 系统正向地理编码拿 WGS84 坐标
- 新增 `wgs84ToGcj02()` —— 火星坐标转换(**必须转**,否则偏 100~700 米,
  而且不报错,只是位置不对,比"打不开"更难排查)
- 新增 `openLinkWithFallback()` —— openLink 优先,失败退 startAbility
- `fbOpenNavigation()` 改为 async:先求坐标,再依次试各家地图
- 高德 URI 修正:`amapuri://route/plan/?sourceApplication=FamilyBook&dlat=..&dlon=..&dname=..`
- 关键字退路改成 `route/plan/?dname=` 形式(旧 `poi?keyword=` 已废弃)

### 两个调用方补 catch

`fbOpenNavigation` 变 async 了,不接住 rejection 会报未处理异常。

## 不需要新权限

`getAddressesFromLocationName()` 是「地址→坐标」换算,不是读设备位置,
不需要 `ohos.permission.LOCATION`。SyncCalendar 也没声明,实测可用。

## 验证

- 编译:`build_hap debug` → BUILD SUCCESSFUL
- **真机验证未做**(设备未连接)。需要确认:
  · 点地点能否直接进高德**路线规划页**(不是搜索页)
  · 目的地是否**位置准确**(验证 GCJ02 转换生效)
  · 没装高德时是否正常降级到下一家 / 复制地点

## 保留

多地图探测(高德/百度/花瓣/腾讯)、复制地点兜底、
「坐标失败 → 关键字重试」的链都保留。
2026-11-03 10:28:28 +08:00
laoyang 0397270e8b 日程改为本地库按需查询,修掉每次进页面解析 9.3MB 的架构错误
## 用户指出的问题

「这个数据量很大,不是应该每次同步完了之后解包放在一个数据库里吗?
 然后点开日程,不就是把今天的日程从数据库里查出来吗?
 这个应该速度非常快才对呀!怎么能每次打开都要去读所有的数据呢?」

用户说得完全对。这是**架构错误**,上一轮我只做了算法层的局部优化
(把 O(n²) 改成 O(n)),没动"数据怎么存、怎么取"这个根本问题。

## 改前 vs 改后

改前:
    load() → module_cache 表读 **9.3MB 字符串**
           → JSON.parse **28628 个对象**
           → 全塞进 this.allEvents 内存数组
    渲染   → 在这 28628 条上遍历

`module_cache` 的设计注释本来写着「解析一遍毫秒级,可接受」——
那是按小数据量假设写的,日程涨到 2.8 万条后这个假设就破了。

改后:
    load() → ① `WHERE date BETWEEN ? AND ?`(走 idx_cal_date)→ 立刻渲染
             ② 后台拉全量 → 批量入库 → 重查当前范围
    渲染   → 只在**当前范围**那几十到几百条上遍历

## 改动

### FbSchema / FbDb:新增 cal_events 实体表

字段对齐 FbCalEvent(12 个)+ uid + fetched_at,三个索引:
- `idx_cal_date` —— 按日期查(最高频)
- `idx_cal_date_kind` —— 农历/节日按 kind 过滤
- `idx_cal_uid` —— 同步去重

⚠️ 主键不用服务端 `ref_id`:衍生条目(农历/节气/纪念日)的 ref_id **恒为 0**,
几万条全撞主键。用自增 rowid + (cal_id, uid) 唯一索引。
uid 由 kind|calId|evId|date|time|title哈希 组成。

⚠️ **建表语句与 migrate 两处都改了**,列名逐一比对一致
(这个坑本项目已踩 5 次,加了静态守卫钉住)。

### FbCalStore(新建):本地库读写

- `replaceAll(rows, stamp)` —— 事务内先 DELETE 再分批写(20 行/批,照
  FbMovieRepo 的实测结论,200 行会卡死)。整批替换而非逐条 upsert,
  否则服务端已删的条目会永远留在本地。
- `between(from,to)` / `ofDay(iso)` —— 页面真正用的查询
- `lunarBetween` —— 月视图 42 个格子只要农历文字,单独查比取全量再过滤快
- `count()` / `fetchedAt()` —— 判空与"数据新不新"

⚠️ 命名用 `FbCalStore` 而不是 `FbCalRepo`:后者已存在,是**接口读写**层
(create/update/remove/fetchBooks)。我第一版误覆盖了它,编译报 8 处错误后
从 git 恢复,改名为 Store 区分「本地库」和「接口」。

### FbCalendarPage

- `allEvents` → **`rangeEvents`**(改名强制提醒:这里不再是全量)
- `rangeOfView()` —— 算当前视图需要的日期区间
- `loadFromLocal()` —— 先查本地,有数据立刻渲染
- `persistAndRefresh()` —— 入库完成后重查,界面跟上最新数据
- `reloadRange()` + `@Monitor('showYear','showMonth','mode')` —— 翻月/切视图按需重查
- `rangeSeq` —— 防抖,避免旧结果覆盖新结果

## 验证

### 编译
`build_hap debug` → BUILD SUCCESSFUL

### 静态校验(34 项全过)
`_tooling/check_cal_db.js`,含最关键的两项:
- 建表与迁移**列名逐一相同**(15 列全对上)
- `load()` 里本地查询确实在远程同步**之前**

### 未验证
**真机实测未做**(设备未连接)。需要确认:
- 老库升级(v7→v8)是否顺利建表
- 首次进入(库空)要等全量拉取,之后进入应该秒开
- 翻月是否立刻有数据

## 过程中的一个失误

第一版把新仓储命名为 `FbCalRepo`,**覆盖了已存在的同名文件**。
编译立刻报 8 处 "has no exported member / does not exist on type"。
从 git 恢复原文件后改名 `FbCalStore`,避免了两层职责混在一个类里。

教训:新建文件前先查同名是否存在(`git status` 没报是因为该文件本就在版控里,
我 `cp` 上去把它改成"已修改"状态,看着像正常改动)。
2026-11-03 09:59:40 +08:00
laoyang 57a0ddd5ad 日程页加载慢:O(n²) 改 O(n) + 全量范围收窄(A+B)
## 问题

用户:「首页点悬浮按钮进日程,加载得好几秒」。
实测数据:`calendar_events` 2026 条 + 328 条 rrule,按范围展开后 **28628 条**。

## 根因:渲染路径上有 O(n²)

| 函数 | 每次成本 | 调用次数 |
|---|---|---|
| `lunarOf(iso)` | 遍历 28628 条 **×2 遍**(先找节日、再找农历) | 月视图每格 + 周视图每天 + 列表每天 |
| `visibleEvents()` | **新建数组** + 遍历 28628 条 | `eventsOf()` 每次调用,列表按天调 |

月视图 42 格 × 2 遍 × 28628 ≈ **240 万次比较**,全部同步跑在 build 路径上。

## A:索引化(客户端)

- 新增 `ensureIndex()`:**一次遍历**同时建 `lunarIdx`(iso→农历文字)
  和 `dayIdx`(iso→当天条目,已排序)
- `lunarOf()` 改为 O(1) 查 Map
- `eventsOf()` 改为 O(1) 查桶,返回副本(防止调用方 sort/push 改到索引)
- 缓存失效:`evtVer` 计数,`allEvents` / `books` 变化时 `++`;
  `idxVer` 记录索引对应版本,不一致才重建

⚠️ **语义必须一致**:旧 `lunarOf` 是「先找 traditional/solarterm/holiday,
找不到才用 kind=lunar」。建索引时用两个 Map 分级收集、最后按"节日优先"合并,
否则会出现「初一」盖掉「中秋」。

保留了这些特例:
- `calId === 0` 的衍生条目不受"本可见性"控制
- `books` 为空时全部通过
- 桶内按 `byTime` 排序

## B:范围收窄 + 服务端上限

### 客户端
`CAL_FULL_FROM/TO` 写死的 `1970-01-01 ~ 2100-12-31`(47846 天)
改为 `calFullRange()` 动态算「今年 ±5 年」(`CAL_SPAN_YEARS = 5`)。

理由:
- 服务端要为**每一天**生成农历条目 ⇒ 130 年范围要传上万条(约 6MB)
- 实际使用只看眼前几年
- 动态计算而非写死年份,避免随时间推移失效
- 缓存键仍带 from/to ⇒ 跨年后范围平移、旧缓存自然失效

### 服务端 `app/Holiday.php`(三处都修)

| 函数 | 原问题 | 修法 |
|---|---|---|
| `lunarRange()` | `$i < 400` ⇒ 只算前 400 天,**今天的农历空** | 去上限,改年数上限 30 年 + 夹 Lunar 支持范围 |
| `traditionalRange()` | 同样 400 天 ⇒ **今年春节/中秋全丢** | 同上 |
| `solarTermRange()` | 只取首末两年 ⇒ 130 年仅 2 年节气 | 改逐年遍历 + 年数上限 |

`MAX_LUNAR_YEARS = 30` 是防御性的:客户端只要 ±5 年,
留余量足够;同时防止有人手拼 1900~2100 把服务器拖死(算 7 万天农历需数十秒)。

## 验证

### 服务端实测(部署后)
```
今天 2026-11-02 → 「廿四」        ← 修复前为空
本月农历覆盖 30/30 天
今年 11 个传统节日、24 个节气     ← 齐全
2021~2031 农历 4017 条(应 4017) ← 修复前只有 400 条
2021~2031 节气 264 条(应 264)   ← 修复前只有 48 条
1970~2100 农历 10958 条,0.47 秒(按 30 年上限截断,符合设计)
```
> ⚠️ 中途发现改动**没上传到服务器**就跑了验证,一度得出"已修好"的错误结论。
> 比对 md5 才定位到。现在 `_tooling/check_cal_perf.js` 里有这项检查(7.1)。

### 静态校验
`_tooling/check_cal_perf.js`:**36 项全过**,含
- 渲染路径无全表遍历
- 索引语义与原实现一致(8 项)
- 缓存失效正确
- 范围收窄且动态
- 服务端三处上限已修
- 服务器文件 md5 与本地一致

### 编译
`build_hap debug` → `BUILD SUCCESSFUL`

### 未验证
**真机加载耗时未测**(设备当时未连接)。理论提速约 84 倍
(240 万次比较 → 28628 次遍历 + 42 次查表),但实际感知需真机确认。

## 顺带记录

服务端 `Holiday.php` 需要**单独上传**(它不在客户端仓库里)。
以后改服务端文件,验证前必须先比对服务器 md5,否则会拿旧代码验出假结果。
2026-11-02 08:51:28 +08:00
laoyang b32501535e 日程改为全量同步 + 修复大响应超时(laoyang/FamilyBook#31)
## 背景

用户 2026-10-30:「全部拉取,全量同步」。
起因:切到下个月再点某一天,端上没数据(即使服务端有)。
根因是请求范围写死成「今天-7 ~ +60」且切月不重拉 ⇒ 窗口外的日子永远空。

## 改动

### FbModuleLoad.loadModule
- `name` 参数从 `FbModuleName` 放宽到 `string`
  日程改成全量后,缓存键要带请求窗口(`calendar:1970-01-01:2100-12-31`),
  否则"旧窗口的数据"会被当成新窗口的数据显示。
- 新增可选 `timeoutMs`
  全量约 9.3MB / 28628 条,默认 15 秒在移动网络下会超时;
  超时后 `FbModuleRepo` 退回旧缓存,表现成"翻月没数据"且极难查。

### FbModuleRepo.load
- 透传 `timeoutMs` 给 `FbApi`。

### FbCalendarPage
- 请求改为全量范围(`CAL_FULL_FROM` ~ `CAL_FULL_TO`),超时放宽到 60 秒。
- 缓存键带窗口(`calendar:<from>:<to>`):某次请求失败后 `FbModuleRepo`
  会把**旧范围**的缓存 sink 回来,端上就把"只含 10~12 月的数据"当全量显示
  —— 这是该 bug 的帮凶,加窗口标识后从根上消失。
- `agendaDays()` 的 `-7 ~ +60` 现在**只决定列表显示多少天**,
  不再兼作请求范围(原来两个数必须同步的约束随全量同步作废)。
- 删除 `agendaFrom()` / `agendaTo()`(不再需要按当前日期算窗口)。

### 恢复 canEdit 的写权限判断
本轮改动曾把
    `isEventWritable(this.detailBase, this.books)`
回退成
    `isEditableEvent(this.detailBase)`
这是**工单 #31 已修过的 bug 回退**:只判 kind/id/calId 不看本的可写性,
导致只读共享日历里的日程也显示「编辑」按钮,用户改完点保存才报
「这本日历你没有写权限」。

已恢复,并与首页 `FbHomePage.ets:1517` 保持同一口径
(否则同一事件"从首页进能编辑、从日历页进不能")。

## 验证

`build_hap debug` → `BUILD SUCCESSFUL`(33 tasks)。

⚠️ **未做真机验证**。全量同步的实际耗时与内存占用需在真机上确认
(28628 条约 9.3MB,解析 150~300ms,主要成本是流量)。

## 备注

这 3 个文件的改动来自 2026-10-30 那一轮,因故未提交;
本次一并整理提交,与品牌改名(家书→岁月家)无关。
2026-11-01 22:54:22 +08:00
laoyang d2129c9aab 配置 AGC Client ID(Map Kit 地点联想与 Account Kit 登录都依赖它)
原值是可读的占位符 REPLACE_WITH_AGC_CLIENT_ID,占位期间:
- Map Kit 联想不返回任何候选(错误 1002600003 App authentication failed)
- 华为账号手机号登录也会失败

⚠️ Client Secret 不入库:它是服务端凭据,纯鸿蒙客户端不需要,
   且一旦进 git 历史就无法真正清除。
2026-11-01 20:44:50 +08:00
laoyang 825008657b 品牌改名:家书 → 岁月家 (laoyang/FamilyBook#38)
## 起因

用户 2026-11-01:「家书已经有人做了,八月份刚上线的」。

核实:「家书 HomeBook」(广州家书科技有限责任公司,粤ICP备2026096420号,
2026-06 成立、08 上线)。功能**部分重合**(家庭记录 + 相册 + 家谱)。
华为审核指南 1.3「应用名称不得和其他应用名称相同,避免混淆」,
虽然个人与企业主体查重不互通、技术上能过,但被人工驳回概率高。

## 定名过程(用户逐个否掉)

| 候选 | 否决原因 |
|---|---|
| 寸草 | — |
| 家卷 | ❌ 与「家眷」同音,口头传播会打错字 |
| 家时光 | ❌ App Store 已有「家·时光」定位几乎一致;且「家+时光」是常见组合,辨识度低 |
| 家纪 | ❌ 已有重名 |
| **岁月家** | ✅ 搜不到同名应用;是口号的直接展开(岁月 + 家) |

## 最终定稿(用户明确指定)

```
应用名      岁月家
主标语      一家人用岁月写成的一本书     ← 不变
一句话简介  一家人用岁月写成的一本书     ← 改为与标语相同
官网标题    岁月家 · 一家人用岁月写成的一本书
英文名      FamilyBook                ← 不变
包名        familybook.yangyq.net     ← 不变
```

## 改动范围(17 + 18 处,12 个文件)

### 应用名(17 处)
`AppScope/resources/base/element/string.json` 的 app_name,
`entry/.../string.json` 的 16 条(应用名、卡片名、无障碍描述)。

### 用户可见文案(18 处)
`FbBackgroundSync` / `EntryAbility`「正在同步家书数据」、
`FbAttachOpen`「家书附件」相册名、`FbHttp` 地址校验失败提示、
`FbCardData` / `FbCardMediaData` 卡片空态「先在「家书」里填写服务器地址」、
`TopBar` / `Drawer` / `FbLoginPage` 的应用名文本、`FbSettingsPage` 的说明。

## 刻意不改

- **包名 / bundleName**(用户明确要求不变,且改了等于换一个应用)
- **英文名 FamilyBook**、目录名、仓库名(改了会让 git 历史、部署脚本、证书路径全跟改)
- **代码注释**里 200+ 处「家书 FamilyBook · 某模块」—— 不影响运行时,
  改了只会制造噪音 diff,让将来 blame 变难
- **主标语**里一个字都没动

## 验证

### 编译
`BUILD SUCCESSFUL`,33 tasks。

### 产物实测
解包 `.hap` 的 `resources.index`:
- 「岁月家」出现 **17** 次
- 「家书」出现 **0** 次

### 全局一致性校验(31 项全过)
`_tooling/check_rename.js`:
- 客户端 app_name / EntryAbility_label / 产物内容
- .ets 用户可见文案无残留(排除注释)
- 服务端非注释代码无残留
- `Auth::homeTitle()` 用「某某某的家庭岁月」
- `DEFAULT_SLOGAN` 未被改动
- 官网线上 5 个页面无「家书」、标题含岁月家、渲染后 h1 是品牌名
- 材料 app_name / 一句话简介 / 包名 / versionCode / 域名

⚠️ 校验过程中我自己的判据出了 2 次假红:
① 注释识别不全(漏了 `/**` 单行块注释和 `<!--` HTML 注释)→ 补齐 isCommentLine
② 「为什么叫岁月家」整节重写后,旧断言还在查「书信」「家与书」
   (那是**旧名字**的逻辑)→ 改成查「岁月」+「家」的拆解

## 一处语义决策(问过用户)

网页端 `Auth::homeTitle()` 生成「某某某的家书」—— 这是**产品核心语义**
(每个家有自己的那一本),直接换成品牌名会读成「某某某的岁月家」,不通。

用户定:改成 **「某某某的家庭岁月」**。
兜底(无姓名时)仍用品牌名「岁月家」。

## 连带影响(不阻塞)

- 上架材料 7 个文件已同步(含软著全称改为「岁月家家庭生活档案管理软件」)
- 官网已部署,线上标题已是新名字
- 软著申请尚未提交 —— 正好一次写对新名字,不需要变更
2026-11-01 19:55:36 +08:00
laoyang 96168811a3 Map Kit 地址联想:module.json5 补 client_id metadata 占位 (laoyang/FamilyBook#33)
官方「开发准备 - Map Kit」要求 entry 模块的 module.json5 里有
metadata: { name: "client_id", value: <AGC 应用 Client ID> }。
只在 AGC 打开地图服务开关、不配这里,仍会报
1002600004 The Map permission is not enabled(错误码表"可能原因"第 2 条就是它)。

当前 value 是占位 REPLACE_WITH_AGC_CLIENT_ID —— 拿到 AGC 的 Client ID 后替换。
占位期间不会崩(认证失败 ⇒ fbGeoTips 返回空 ⇒ 输入框退化成普通文本框)。

验证:BUILD SUCCESSFUL(33 tasks,27 executed);
产物 entry-default-signed.hap 的 module.json 里 metadata 实测
[{"name":"client_id","value":"REPLACE_WITH_AGC_CLIENT_ID"}]。
2026-11-01 09:39:47 +08:00
laoyang 5661312679 日程编辑:确认「开始」后结束时间自动 = 开始 + 1 小时 (laoyang/FamilyBook#34)
用户 2026-11-01:
  「当我选择了开始时间并确定之后,结束时间要跟着开始时间变成开始时间加1个小时。
    也就是说,所有时间我们都默认它是一个小时。如果不是一个小时,再让他自己去改。」

## 改前

`applyPicker()` 只在**会变成负时长**时才动结束时间:

    if (this.fEndDate < iso || (this.fEndDate === iso && this.fEndTime < hm)) { ... }

⇒ 用户改了开始、结束还挂在老位置,实际时长变成随机的。

## 改后

点「开始」确定 ⇒ 结束**无条件**重置为「开始 + 1 小时」。
改「结束」那条路一个字不动(改结束不影响开始)—— 这是「默认一小时、
不像一小时再自己改」的另一半。

全天日程跳过这段:时刻部分本来就不生效,改了不会落到服务端,
但会让"切回非全天"时看到一个莫名的时间。

## 新增 fbPlusHours()

`FbDateTimeField.ets` 里加 `fbPlusHours(date, time, hours)`,返回 `{date, time}`。
**不复用现成的 `fbAddHour()`**,因为那个有两个问题:

    return fbPad2(Math.min(hh + 1, 23)) + ':' + mm;

① `Math.min(hh+1, 23)` **封顶 23 点** ⇒ 23:30 加一小时还是 23:30;
② 只返回时间、不带日期 ⇒ 表达不了「次日 00:30」。

跨天/跨月/跨年全部交给 `Date` 运算,不自己算天数
(自己 `if (hh>=24) dd+=1` 在"11 月 30 日 + 1 天"上就会错成 11 月 31 日)。

## 验证

**纯函数真值表 13 项**(从真实 `.ets` 抽函数、只擦类型标注后 node 直接跑,
不手抄替身;擦除后断言行数不变、无类型残留、没引入新字符):

普通 / 跨天整点 / 跨天半点 / 跨月 / 跨年 / 非闰年 2 月末 / **闰年 2 月末 ⇒ 2/29** / 午夜 / 带分钟
—— 全过。其中 23:30+1h ⇒ **次日 00:30**、12/31 ⇒ 次年 01/01、2028-02-28 ⇒ 2028-02-29。

**反向验证 3 项**(证明判据不是恒真):
坏A(封顶 23 点 / 不带日期)被抓到、坏B(丢分钟)被抓到、坏C(固定 28 天)被抓到。

**接线断言 12 项 + 反向验证 5 项**(静态,因为"函数对 ≠ 接进去了",
上一轮 #34 主体 bug 就是参数名对不上、编译运行都不报错):
- 开始分支调用 `fbPlusHours(iso, hm, 1)`、结果**同时**写 `fEndDate` 与 `fEndTime`
- 结束分支**没有任何** `fStart*` 赋值
- 全天日程有 `!this.fAllDay` 判断
- `pickIsEnd = !isStart`(#34 主体防回归)
- 不再残留 `this.fEndDate < iso` 那种条件顺延
- 反向:改回老写法 / 只写日期不写时间 / 改结束顺手改开始 / `pickIsEnd` 再反过来
  → 逐条转红;**对照组**(只改一句注释)仍绿 —— 证明断言不会"见到改动就红"

> ⚠️ 接线判据自己第一版有 bug,是反向验证抓出来的:坏1 用正则
> `if \(!this\.fAllDay\) \{[\s\S]*?\n      \}` 找替换目标,但渲染层还有一个
> `if (!this.fAllDay)`(控制「结束」那一行显示),正则先吃到那个 ⇒ 注入落在无关位置
> ⇒ 断言仍绿(假阴性)。改成"先把 applyPicker 的函数体切出来再替换"。

**编译**:BUILD SUCCESSFUL,33 tasks(17 executed)。验签通过。

**⚠️ 已知存量失败(与本次无关)**:`_fb_arkts_guard.js` 跑出 1321 通过 / 3 失败,
三条都在工作区里**别的会话未提交的改动**上(`AGENDA_BACK_DAYS` 被换成 `CAL_FULL_FROM/TO`
导致"列表视图显式传日期范围"判据失配、`canEdit` 相关两条属 #31)。
本次只改 `FbDateTimeField.ets` 与 `FbEventEditSheet.ets`,不碰那些文件;
用 `git show HEAD:` 核实过 HEAD 版本里 `AGENDA_BACK_DAYS` 还在(判据本来通过)。
2026-11-01 09:38:14 +08:00
laoyang d2d0a435a3 日程编辑:点「开始」却改了结束时间 laoyang/FamilyBook#34
用户 2026-10-30:「鸿蒙端,日程添加时,选择时间,点上面,
修改的反而是结束时间,这个是 bug。」

根因:openPicker 的参数语义与调用方相反。
  · 调用方 dateTimeRow(label, isStart) 传的是 **isStart**
  · 被调用方签名却是 openPicker(isEnd),内部直接 pickIsEnd = isEnd
  ⇒ isStart=true(点开始)→ isEnd=true → 整条链路按"结束"处理。

错得很"自洽"所以一直没被发现:
  · 弹层初值读的是结束时间
  · 确认后写回的也是结束时间
  · 连标题都显示「选择结束时间」
三处一起错,"弹出来的值"和"改完的值"是一致的,看着不像坏。

修法:参数统一成 isStart(与调用方同名同义),内部自己取反。
  没在调用处写 openPicker(!isStart) —— 双重否定更容易再错一次。

对照:观影表单 FbMovieNewSheet 用同一个 FbDateTimePicker 组件,
但 pickIsEnd 是显式赋 false/true,没有正反颠倒的空间,不受影响。

验证:
  · 编译 BUILD SUCCESSFUL(33 tasks / 17 executed)
  · 从真实源码提取三条映射做断言:pickIsEnd=!isStart、
    读日期/读时间均在 isStart 时取 fStart* —— 全部通过
  · 真值表:点开始→读fStart*→写fStart*→标题「选择开始时间」,
    点结束→读fEnd*→写fEnd*→标题「选择结束时间」
  · 反向验证:拿修复前的代码跑同一判据,正确转红
    (旧版点开始时读写都是 fEnd*),证明判据不是恒真
  · 全仓库排查 isStart/isEnd 用法,无同类问题
2026-10-30 15:41:38 +08:00
laoyang 7bc26dd7e1 地点联想:改用鸿蒙自带 Map Kit,不再走高德 laoyang/FamilyBook#33
用户指示「端侧不用高德,用鸿蒙自带的接口」。

改用 @kit.MapKit 的 site.queryAutoComplete(),顺带解决原方案两个问题:
  ① 高德 Web 服务 key 是服务端密钥,本不该出现在客户端;
  ② 线上那个 key 实际是 JS API 类型、调 Web 服务接口一直失败(见 #33 排查记录)。
现在端上不需要任何外部 key,也不需要我们自己的服务端接口与配额。

改动:
- FbGeoRepo:site.queryAutoComplete({query}) -> sites[]
  取 name + formatAddress + location。
  formatAddress 的 SDK 注释原文是 "Formatted detailed address of a place",
  正是用户要的「作为参考的详细地址」。
  formatAddress 缺失时用 locality + streetName + streetNumber 兜底。
- FbAddrInput:去掉 token 参数(MapKit 鉴权走 AGC 应用认证,与 API 令牌无关)。
- FbEventEditSheet / FbTripNodeSheet:两处调用同步去掉 token。

错误码全部降级为空结果、不打扰用户(声明文件列明的):
  - 1002600003 App authentication failed
  - 1002600004 The Map permission is not enabled
  - 1002603001 Zero result(查不到,属正常业务结果,不刷错误日志)
  - 1002600002 Failed to connect to the Map Kit server
联想挂掉 => 退化成普通文本框,手敲照常保存。

字段名一律以声明文件 @hms.core.map.site.d.ts 为准:
StreetNumber 的字段是 streetName / streetNumber,
第一版凭印象写成 street / number,编译直接报
"Property 'street' does not exist on type 'StreetNumber'",已修正。

编译通过(33 tasks,17 executed)。
2026-10-30 08:12:15 +08:00
laoyang f4f27cea55 地点输入:补地址联想,下拉带详细地址好区分同名地点 laoyang/FamilyBook#36
用户 2026-10-30:「在所有调用高德显示详细地址的地方…不能只把地点告诉我,
一定要把详细地址也放在后面。因为同一个地址可能有好几个,
我根本不知道应该选哪一个。」

网页端(addr_suggest.js)已经是这个行为,端上原本**连联想都没有**(纯文本框)。
本次把能力补到端上:

- 新增 FbGeoRepo.ets:调服务端 /api/v1/geo/tips,最少 2 字才查
- 新增 FbAddrInput.ets:候选每行两行字(地名 14px 深色 + 详细地址 12px 灰),
  同名地点靠第二行区分;防抖 260ms(与网页端同节奏);用 seq 丢弃过期响应
- 日程编辑、行程节点两处地点接入

🔴 走服务端代理而不是端上直连高德:高德 Web 服务 key 是服务端密钥,
打进安装包等于公开(反编译/抓包即得,配额会被跑光、key 可能被封)。
网页端用的 JS API key + 安全密钥是域名白名单那套,原生端没有等价机制。

⚠️ 必须是 @ComponentV2 组件而不是 @Builder:@Builder 的值类型参数
不参与状态观察(SDK r/quick-rules/05-state.md),候选列表/展开态写在
@Builder 里会数据变了界面不重建。本项目在 FbEventChip/FbTagChip/
FbCalOptionRow 上各踩过一次。

⚠️ 联想失败一律返回空候选 ⇒ 退化成普通文本框,手敲照常保存
(接口挂/没网/没配 key 都不能拖垮主流程)。

编译通过(33 tasks,17 executed)。
2026-10-30 08:03:13 +08:00
laoyang f90cae0dcc 日程详情:只读日历不再给「编辑日程」入口 laoyang/FamilyBook#31
真机(DEL-AL20 / HarmonyOS 7.0.0.109)实测发现:只读共享本(如「工作学习」)
里的日程,首页详情卡仍显示「编辑日程」,用户改完点保存才被服务端拒:
「保存失败:这本日历你没有写权限。」

根因是 canEdit 写死 true,只判了 isEditableEvent(kind/id/calId),
没判这本日历能不能写。

- Types.ets 新增 isEventWritable(e, books):复用 FbCalBook.canWrite 口径
  (由 FbCalRepo.fetchBooks 依 /calendar/books 的 kind+perm 算出),
  不与写权限推导逻辑重复;books 缺失时判为不可编辑(宁可少给入口)
- FbHomePage:canEdit 由写死 true 改为按写权限判定;
  openEvent 里顺手预加载 calBooks(原本懒加载,进详情时还是空数组,
  会把本来能编辑的日程也误判成只读)
- FbCalendarPage:同步改用 isEventWritable,两处口径一致

真机复验:只读日程详情只剩「导航」;有权限的日程「编辑日程」正常,
改标题保存后杀进程重启仍在(确认真落库)。
2026-10-29 20:39:52 +08:00
laoyang fd3ea53086 资源:点列表进只读详情,附件可预览/下载,编辑走底部按钮分离 laoyang/FamilyBook#29
用户原话(2026-10-28):「资源模块,点击资源后是编辑,那怎么下载资源
里面的文件呢?应该和名册一样,点击后是详情,属于谁、共享给谁、附件,
可以打开预览然后下载。下面有按钮可以编辑,点编辑之后才是编辑功能。」

原来 FbResourceSheet 是「查看=编辑」合体:点列表直接进可编辑框,附件区
点不动、没有预览/下载。这次拆成两级:

1) 新增 FbResourceDetailSheet(只读详情):标题/正文纯文本、归属人、
   共享列表、附件列表(图片点开全屏预览、非图片点开交给系统「打开方式」、
   每条附件可「下载」)。底部「编辑」按钮(仅 owner/write)→ 切编辑浮层。
2) FbResourcesPage 编排:点列表 → 详情;详情「编辑」→ 原 FbResourceSheet;
   编辑保存/删除回列表。 + 新建仍直接进编辑浮层。
3) 新增 common/FbAttachOpen.ets:附件「下载明文 / 打开发 / 存相册 / 另存为」
   的端上工具。走服务端 media.php 的移动端出口(能力早已就绪,纯端上改动)。
   · 存相册:photoAccessHelper.showAssetsCreationDialog(系统弹窗,无需 WRITE 权限)
   · 另存为:picker.DocumentViewPicker.save
   · 系统打开:隐式 Want viewData + 读授权 flag

编译 BUILD SUCCESSFUL;守卫 _fb_arkts_guard.js 1324/0(浮层计数清单已同步
resources 1→3 模板、books 3→4 补 #28 存量、cardN 42→44)。
2026-10-28 23:53:53 +08:00
laoyang f5a979f5c8 读书上传+在线阅读+笔记导入:端上补齐三个入口 laoyang/FamilyBook#28
用户原话(2026-10-28):「鸿蒙端对齐网页端」。读书端上原来只有书架。

1) FbBookUploadSheet:上传电子书(选文件→服务端解析建书)+ 粘贴导入读书笔记
2) FbBookReadSheet:在线阅读(章节列表 + 单章正文,一次一章)
3) FbBookSheet 详情态加「在线阅读」入口(有章节时才显示);
   FbBooksPage 加「+上传/导入」入口 + 设置菜单「导入笔记」接上

编译 BUILD SUCCESSFUL;服务端冒烟 _i28_book_api.py 11/0 + 反证命中。
2026-10-28 22:47:38 +08:00
laoyang f1f279f973 电影影评+评论:详情浮层接「评价与评论」真功能 laoyang/FamilyBook#27
用户原话(2026-10-28):「鸿蒙端对齐网页端」。观影详情原来只能看平均分。
现在:

1) FbMovieRepo 加影评/评论类型 + reviews/saveReview/deleteReview/
   addReviewComment/deleteReviewComment 五个 API + 解析(含感受目录、评论树、mine 判断)
2) 新组件 FbMovieReviewSheet:我的影评(打分 1-10 星 + 观后感受多选 + 正文 +
   改/删)、全部影评列表(含评论树、回复、逐条删除)
3) FbMovieDetailSheet 加「评价与评论」入口 + 内嵌影评浮层

⚠️ 影评数/均分仍以列表项为准(详情接口不返回,与既有注释一致)。

编译 BUILD SUCCESSFUL;服务端冒烟 _i27_movie_review_api.py 22/0 + 反证命中。
2026-10-28 22:30:09 +08:00
laoyang 59a9d3df39 任务共享:面板补「共享给谁」区块 laoyang/FamilyBook#26
用户原话(2026-10-28):「鸿蒙端对齐网页端」。任务面板原来没有共享入口,现在:

1) FbTaskShare 类型 + parseTaskDetail 解析 Task::find 已带的 shares
2) FbTaskRepo 补 fetchTaskShares/shareTask/unshareTask(删除走 query)
3) FbTaskPanel 加「共享给谁」区块:列表(可改/只看)+ 逐条取消 +
   「+ 共享」复用选人组件(与资源/财务共享同一套)

编译 BUILD SUCCESSFUL;服务端冒烟 _i26_task_share_api.py 13/0 + 反证命中。
2026-10-28 22:10:15 +08:00
laoyang f93dd92cdb 资源附件+共享:浮层补「附件」「共享」两个真功能 laoyang/FamilyBook#25
用户原话(2026-10-28):「鸿蒙端对齐网页端」。资源浮层原来「附件与共享
还在网页端,App 暂未开放」,现在服务端补齐 API 后:

1) FbResourceRepo 补 attach/share 写操作(上传走 fbUploadOne 字段 file;
   删除走 query 传 attachment_id / person_id —— DELETE 带 JSON body 服务端读不到)
2) FbResourceSheet:附件区块(列表 + 缩略图/图标 + 「+传附件」+ 逐条删)+
   共享区块(列表 + 「共享给…」复用选人组件 + 逐条取消)
   · 只读共享来的不摆入口(与 canWrite 对齐)

编译 BUILD SUCCESSFUL;服务端冒烟 _i25_resource_api.py 21/0 + 反证命中。
2026-10-28 22:00:15 +08:00
laoyang 6f360fd6a1 财务折线:概览卡画净资产趋势(配服务端 /timeline)laoyang/FamilyBook#24
用户原话(2026-10-28):「鸿蒙端对齐网页端」。财务概览卡原来「折线图占位
(第二轮接图表能力后填)」,现在:

1) FbLedgerPoint 类型 + parseLedgerTimeline(读 /timeline 的 items)
2) loadEntries 里顺带拉 timeline(单独缓存键 ledger_timeline_<id>)
3) timelinePath():时序点 → 绝对坐标折线 path(Path 组件画,Y 轴按
   min..max 归一,不是从 0 起 —— 金额平稳时不贴底);<2 点时给占位文案。

编译 BUILD SUCCESSFUL;守卫 _fb_arkts_guard.js 1324/0;服务端冒烟
_i24_timeline_api.py 8/0 + 反证命中。
2026-10-28 21:47:28 +08:00
laoyang 97846029c7 笔记图片块:展示真图 + 插图 + blocks 保存(图文混排后半程)laoyang/FamilyBook#23
用户原话(2026-10-28):「鸿蒙端对齐网页端」,笔记端上只支持纯文本。
这半程改鸿蒙端,配合服务端 cd56a9d 补的 /notes/upload:

1) 展示:FbNoteItem 新增 blocks(FbNoteBlock[]),parseNotes 把原始块
   带进来;FbNoteSheet 里 image 块真画图(fbImageSrc),不再显示「[图片]」。
2) 插图:正文区下加「+ 插图」,选图 → fbUploadKey 上传拿 key → 加入
   待保存列表;每张图带「×」可删。
3) 保存(P0 修复):带图时改走 saveNoteWithBlocks —— 正文 text 块 + 图片
   image 块整组提交。原来只传 body,服务端折成单个 text 块整组替换,
   **编辑带图笔记会把图片块静默删光**。
4) fbUploadKey 是 fbUpload 抽出的第二出口:复用同一 multipart 核心拿 key,
   不复制第二份实现(本项目"两份实现漂移"栽过多次)。

编译 BUILD SUCCESSFUL;守卫 _fb_arkts_guard.js 1324/0;服务端冒烟
_i23_note_image_api.py 11/0(含 PATCH 后图片块不被删)+ 反证命中 2/2。
2026-10-28 21:35:11 +08:00
laoyang 3e910f1ca0 docs(card): 删掉「格子数与图数量永远相等」这句错误断言 #3
上一版(工单 #2)在这里写「这样"格子数"和"图数量"在结构上永远相等」。
这句话是错的,也正是工单 #3「21 格」的思想源头:

  keys 按 poster **去重**收集、还卡 20 张上限
  list 按 **行** 过滤、无上限,还会再裁偶数

两个集合基数不可能恒等,正确的说法只是「≤,不是 ==」。
把错误的断言留在注释里,下一个人会照着它推理,再踩一次同样的坑。

本次只改注释,无逻辑变更(守卫 1324/0、v97 反证 13/0 均通过)。
2026-10-28 10:49:20 +08:00
laoyang acb2eadcb7 fix(card): 观影海报墙格数改偶数并按海报去重 #3
用户原话:「怎么是单数的?我这里看,显示的是21张图片。观影卡片。」

上一版(#2)修掉了「只有 6 张图」,但留下了新的结构问题:
  · keys(配图)按 poster 去重,上限 20
  · list(格子)按行过滤,无上限
⇒ 一部电影看了两场(两天各一场 / 重映),两行 poster 相同、拿到的 img
  也相同,于是「有图的行数」会**多于**「图片张数」,可能是 21 这种单数。
两列栅格下奇数格必然让最后一行右侧留空 —— 就是用户看到的样子。

修法(`list` 的语义是「一部电影一格」,不是「一场观影一格」):
  · 按 poster 去重(键用 poster 而非 title:同名重映 / 译名相同的不同片不算同一部)
  · 顺带保证偶数:len - (len % 2),不写死 20/18(图少时减法会变负索引)
  · poster === '' 的行显式跳过(没海报键的行不该进墙)

验证:静态守卫 1324/0(新增 groupV97 六条)· 反向验证 13/0(6 条注入各自命中)
编译 BUILD SUCCESSFUL,产物 6.2M。真机待验(hdc 设备掉线,见工单)。

顺带修掉两处「跑不起来」的环境问题(与本次 bug 无关,但会挡住验证):
  · 编译脚本补 JAVA_HOME / PATH:PackageHap 阶段要 spawn java,
    缺了只报 `Error Code: 00308018 Unknown Error`,真因
    `spawn java ENOENT` 藏在 build-logs 深处。
  · PATH 必须写 POSIX 风格 /d/... 而不是 D:/...:
    MSYS2 的 PATH 是冒号分隔,`D:/xxx` 里的冒号会把路径劈成两段。
2026-10-28 10:10:33 +08:00
laoyang f0d0ab9a05 fix(card): 观影 4x4/6x4 海报墙「只有 6 张图」修复 #2
用户原话:「观影4x4和4x6,只有六张图,划到第七张,就没图片了」

病根:两个上限各写各的
  · CARD_IMG_MAX = 6      (配图数)
  · rows.length < 30      (列表长度)
⇒ 前 6 格有海报,第 7 格起 img === '',posterCell 走 else 画淡紫占位块。
渲染没错、数据也没缺(线上 movie_details 221 条全有 poster_key,去重 205 张),
纯粹是「配图数 < 格子数」的结构性问题。

修法(不是把 6 改成 20 就完事,而是让格子数由图数决定):
  · CARD_IMG_MAX 6 → 20,依据华为 Form Kit FAQ:API 20+ 上限 20 张 / 10MB,
    本机 const.ohos.apiversion = 26 走这一档;
    内存按解码后算 240×360×4×20 = 6.59MB < 10MB,留 3.4MB 余量。
  · 拆出 CARD_ROW_MAX = 30(命名常量),专给不画图的 2x2/2x4 纯文本行。
  · 新增 list:只保留 img !== '' 的行,长度天然 ≤ CARD_IMG_MAX
    ⇒ 格子数与图数在结构上永远相等,某张 404 也不会冒空格子。
  · fbAttachPosters 签名 (recent, list) → (rows),两个数组各写一遍循环太容易只改一处。
  · 新增 fbCardImagePaths 并发取图(MAX_PARALLEL = 4):20 张串行实测 10~17s,
    卡片刷新窗口撑不住。MAX_FILES 24 必须 ≥ CARD_IMG_MAX,否则后下的挤掉先下的。

验证:静态守卫 1317/0;反向验证 15/0(7 条注入各自恰好命中目标断言)。
其中注入 A 抓出原先一条假绿断言:写的是「CARD_IMG_MAX >= 4」,
而 bug 原值正好是 6,6 >= 4 恒真 ⇒ 等于什么都没守。已改成「> 6」。
2026-10-27 23:06:24 +08:00
laoyang b84ea06b73 v95 服务卡片四项:日程卡「今」按钮放大、观影/任务 2x4 列表可滑动、观影 4x4/6x4 补列间距且 6x4 改两列
#1

用户提四项(2026-10-26):
  「日程的右上角那个今天的今,特别不好点,一不小心就到日程里面去了……
    是不是可以把按钮弄的大一点。」
  「观影的卡片,2x4,右侧观影的列表,不能上下滑动,第三个被截断,很难看,
    如果显示多个,就要能滑动,如果不能滑动,就显示两个。」
  「任务的 2x4 卡片,也是一样的。」
  「观影的 4x4 卡片,和 4x6 卡片,两列图片挤在一起,中间没有空隙,应该缩小一点,
    这个大小,我感觉应该根据屏幕的大小来。4x6,宽度都是一样的,为啥要弄成三列?」

① 日程卡「今」按钮 22x22 → 32x32
   病根:22 的圆比最小建议点击区小一半,手指偏一点就落到卡片本体的
   `.onClick(openApp)` 上 ⇒ 「想回今天」变成「进了日程详情」。
   面积 484 → 1024(2.1 倍);同一份 sideBtn() 里的「+」一并放大,
   翻页键 ‹ › 本来 32,现在四个按钮手感一致。
   为什么是 32 不是 40:4x4 卡高度紧,顶栏与内容区竖向堆叠
   (内容区 layoutWeight(1)),顶栏长高多少内容区就少多少。
   必须改 width/height 本身 —— 卡片里这是 Button,可点区就是自己的宽高。

②③ 观影/任务 2x4 右半 Column → List(可滚动)
   病根不在条数而在容器:Column 根本不滚动 + 硬截 3 条,
   3x(38+10=48) + 2x6 = 156vp 而可用仅 ≈130vp ⇒ 第三条被压扁/裁掉。
   条数上限 3 → MR_2X4_MAX=12 / TK_2X4_MAX=30;滚动条 BarState.Auto
   (条目少时不露空滚动条)。数据侧 FbCardMediaData 两处 `rows.length < 4`
   同步放到 30,否则卡上拿到的还是 4 条,改了容器也滑不出东西。

④ 观影 4x4 / 6x4 补列间距 + 6x4 三列改两列
   病根:`List({ space: 8 })` 的 space 在 lanes 多列模式下只管行间距,
   列与列之间不会自动留缝;海报格 .width('100%') + .aspectRatio(0.68)
   又贴着 lane 边排 ⇒ 两列严丝合缝贴死。
   改法:.lanes(2) → .lanes(2, 8),第二个位置参数才是列间距 gutter。
   ⚠️ gutter 是位置参数(SDK list.d.ts: lanes(value, gutter?: Dimension),
      Dimension = PX|VP|FP|LPX|Percentage|Resource)⇒ 传数字,
      写成 { gutter: 8 } 是类型错误(中途踩到,编译期才炸)。
   6x4 同时从 .lanes(3) 改成两列,与 4x4 一致;文件头那句
   「这块卡更宽、一排放三张」已过时,已注明别再照着它改回去。

同批带上 v92 未提交的观影海报墙实现(此前只在工作树里,从未提交过):
  - 新增 data/FbCardImages.ets(下载 + desiredSize 缩图 + fd 管理)
  - FbCardMediaData / FbFormAbility / FbCardForms 里的 formImages 管道
  - string.json 卡片名带尺寸(「家书 · 日程 4×4」/「家书 · 观影 4×4」)
  ⚠️ FbCardImages.ets 已被 FbCardMediaData import,不一起提交就编译不过。

验证:
  - 静态守卫 _fb_arkts_guard.js:1303 通过 / 0 失败
    (新增 groupV95() 共 28 条,钉住按钮尺寸、List 容器、条数上限、
      两列 + gutter>0、且不许写成 { gutter: N } 对象)
  - 反向验证 _fb_v59d_falsify.py:289 通过 / 0 失败
    (新增 9 条注入,逐条证明上述断言真的会红)
  - 真编译 hvigorw assembleHap:BUILD SUCCESSFUL,HAP 产物已生成

⚠️ 真机验收未做:hdc list targets 为空(设备未连/未解锁)。
   这四项全是观感与手感,静态断言证明不了「点得着、滑得动、看着舒服」,
   待设备连上后补真机探针(重点:真的点「今」、真的滑列表、量两列实际间距)。
2026-10-26 07:28:06 +08:00
laoyang 7ae34e6e23 v91 观影/任务卡加 4x4/6x4(滚动列表+新建+卡上完成任务);修任务卡无数据(解析器用错:看板嵌套需 parseTasks) 2026-10-25 09:29:41 +08:00
laoyang 6711197a94 v90 新增观影卡与任务卡(2x2/2x4):卡片按类型分流推数据,key 带 version 前缀 2026-10-24 23:44:46 +08:00
laoyang 7ebe18b2d1 v89 修复服务卡片翻页后时间轴不重绘:ForEach key 加 version 前缀(key 撞车复用旧组件) 2026-10-24 21:53:13 +08:00
laoyang d69beb629f v88 旅程查看器全屏化:视频内联全屏播放、播放中直接滑动切换、只留底部计数 2026-10-24 21:53:05 +08:00
laoyang c77370f521 v87 旅程查看器 Swiper 化:图片+视频混排翻页,滑动带动画
用户 2026-10-24:「图片和视频是不是可以都支持滑动啊?……滑动到视频
的时候,点击就直接播放。另外滑动的时候有没有个动画呀?现在就滑动,
整个一下就变化,特别生硬。」

实现:
- photoViewer 从「单张 Image + PanGesture(松手瞬跳 index)」改成
  Swiper:跟手滑动 + 换页缓动动画(duration 350ms),loop 循环翻页
- 媒体列表改 mediaOf() = 图片在前视频在后(与缩略图墙同序),
  viewerKeys:string[] → viewerPhotos:FbTripNodePhoto[](要带 mime)
- 视频也是翻页项:滑到视频页显示 ▶ + 文件名 +「点一下播放」,
  点一下走 openVideo() 弹原有全屏播放器(系统控件不变)
- 底部 ‹ › 按钮走 viewerCtl.showPrevious()/showNext()(与手势同一条
  动画通道;直接改 index 是瞬跳);计数由 onChange 统一更新
- 打开查看器用 changeIndex(at, false) 跳到所点那张,开局不播动画
- viewerStep()/imageKeysOf() 随 PanGesture 方案一并退场

真机实测(Mate X7,"2026国庆回老家"段附件 3 个媒体):
开查看器 1/3 → 左滑 2/3 → 3/3 视频页(点一下播放)→ 点击后视频
真实播放(5.mp4,00:05/00:29)→ 关播放器回查看器原位 → 按钮翻页
2/3→1/3 → 循环回绕 1/3→3/3 全部通过。

验证:守卫 1177/0(v87 组新增 10 条)· 反向验证全量 249/0
(v87 组 7 条全部命中)· 装机成功
2026-10-24 20:52:51 +08:00
laoyang 051d3974ba v86 首页补齐五项入口(就地编辑 + 4 张卡新建 + 新增观影/联系人卡)
用户 2026-10-24「端上也全部都实现」,即 v84 验收时留下的三条:
名册新增成员 / 首页日程弹框后的编辑 / 首页其他卡片的新增。

实现:
- 日程详情卡「编辑」改成就地叠编辑表单,不再 closeEvent()+switchModule
- 近期任务 / 最近纪念日 / 最近观影 / 联系人 四张卡加「+ 新建」
- 观影卡与联系人卡端上原来没有,本次新建(数据走本地仓储,/home 不返回)
- backOnce 按层加 6 个分支;@Monitor 覆盖 8 个标志位
- FbSectionTitle 扩 addText/onAdd(带默认值,老调用点不改)

真机验收抓出三个「断言全绿但用不了」的 bug,一并修掉:
1. 编辑表单所有按钮点不动 —— Stack 层叠顺序写反,详情卡的整屏遮罩
   盖在表单之上。dumpLayout 坐标全对、节点全在,只有真点才会发现。
2. 「+ 新建」点了没反应 —— syncNote 在本页只有赋值没有渲染点,
   失败分支静默 return。统一改成 note() = 赋值 + 当场 Toast。
3. 改选「不重复」存完还是「每天」—— recognize 的保护逻辑(防清掉自定义
   重复规则)误伤了「用户主动清空」:原规则非空 + 选 none ⇒ rrule 整项
   不发。加 fRepeatTouched 区分「归入 none」与「主动选 none」。

验证:守卫 1167/0 · 反向验证全量 242/0 · 装机成功
端到端落库 建→改→删 全走通,测试数据已清理
2026-10-24 20:22:35 +08:00
laoyang cdbf0a9593 v84: 首页「+ 新建」日程 + 红线去端点 + 新建表单两处可见性修复
用户 2026-10-23 原话:「日程,没有新建日程按钮,名册,也没有新增成员这个按钮。」
「首页上的日程,红色线不对,变短了左侧缺了一块。在日程模块中,那个红线左侧
是不是还有一个红点?如果有,去掉。」

① 首页日程区块加「+ 新建」(FbHomePage / Widgets.FbSectionTitle)
   - FbSectionTitle 扩两个可选参数 addText / onAdd,带默认值 ⇒ 老调用点一行不改
   - 按需懒拉 /calendar/books,缓存进 calBooks,同一会话只拉一次
   - todayIso() 用本地年月日拼,**不用 toISOString**(UTC 会让早 8 点前算成昨天)

② 时间红线两端统一成「一根满宽线、无端点装饰、无行首文字」(FbDayTimeline)
   - 老写法 `Row { Text(时间) width(34); 线; 圆点 }` ⇒ 行首 34vp 把线顶掉一块
     (与网页端 .ag-now「双重缩进」同一类病),行尾 6x6 圆点就是用户问的红点
   - 改成满高 Column + Blank 撑高 + 满宽 2px 线,与卡片端同一写法

③ 🔴 真机实测发现的两处可见性问题(FbEventEditSheet)
   - 「加入哪本日历」原来排在「提醒」之后 ⇒ 表单高于卡片(maxHeight 86%),
     它被推到滚动区下方,首屏只看到被底边切掉半行的标题(实测 bounds 高 7px)。
     它是必选项(没选就「请选一本日历」存不上)⇒ 挪到「全天」之后、日期时间之前。
   - 错误提示原来在滚动区**内部最底部** ⇒ 用户在顶部点「创建」时提示在几百 px
     之下看不见;就算滚到底也被 Scroll 底边切半行。现象是「点了创建什么都没发生」。
     ⇒ 挪到滚动区外面、按钮区上方(实测:Scroll 底 1953,提示 1984~2024,按钮 2074)。

真机验收(设备 5NC0225B27004718,线上 family.yangyq.net):
  - 静态守卫 1112/0;BUILD SUCCESSFUL;install bundle successfully
  - 「+ 新建」bounds [868,931][1018,1045],点击真的弹出「新建日程」
  - 诊断确认 books=15 canWrite=6(数据层完全正常,之前的「选不了日历本」是可见性问题)
  - 日历本挪位后首屏可见:标题高 40px(原 7px),10 本 chip 从 838 排到 1358
  - 错误提示挪位后不滚动即可见:命中「日程得有个标题」
  - **端到端建卡成功**:线上库确认 id=2411 cal=5 20:00~21:00,首页 1 条→2 条;
    测试数据已按 id 精确删除(remaining_test_rows=0),重启后恢复 1 条
2026-10-24 19:14:18 +08:00
laoyang 0bab0cdcc6 v83: 后台定期同步(四层调度,策略照 SyncCalendar)
用户原话:「另外,要有定期同步的功能,就是家书app在后台,也能自动同步
数据,这样打开app,基本上数据就是最新的。同步的策略,去synccalendar找,
里面有办法。」

四层调度(缺一层就会在某种场景下"永远不同步"):
1. 前台:onForeground 时若距上次同步超过 5 分钟,补一次
2. 退后台:requestSuspendDelay 短时任务(单次≤3分钟、单日10分钟配额)
3. 延迟任务:workScheduler(周期 2 小时 —— 系统按活跃分组压制,
   请求更短不报错但不生效)
4. 桌面卡片:updateDuration:1(=30 分钟,唯一不受压制的通道,
   每天上限 50 次);先推卡片再触发同步

关键约束(写错了都不报错,只是静默失效):
- WorkSchedulerExtensionAbility 单次回调上限 2 分钟,必须在 finally 里 stopWork
- 后台扩展跑在独立进程,EntryAbility.onCreate 的初始化一步都没跑过
  ⇒ prepare() 必须补齐 token store / module token / FbDb.open
- 回前台必须 cancelSuspendDelay 归还配额,否则两三天后用光
- workInfo 要带 isPersisted,否则重启后任务永久消失
- abilityName 必须与 module.json5 声明一致,对不上时 startWork 不报错但不回调
- ApplicationInfo 没有 bundleName 字段,包名在 .name

其他:
- 卡片驱动的同步有节流(CARD_MIN_GAP_MS 25 分钟 < 卡片刷新 30 分钟,
  反了就是每次刷新都被跳过、一次都不跑)
- 设置页新增「同步」分组:开关 + 上次同步时间 + 说明

同时修:首页日程红线左侧缺一块(nowLayer 从 Row{圆点;线} 改成单个
Column 满宽 —— Row 里圆点占行首,线只能从第 5vp 起画)

守卫 1106/0、v83 反向验证 10/10 全中、已装机。
2026-10-24 04:25:15 +08:00
laoyang 1c6af8ac56 v79b 任务面板:补「看板」一行 + boardOf 反查兜底
真机验收时发现 boardOf() 定义了却从没被渲染,面板上看不出这条任务
属于哪个看板 —— 而「挪看板」正是 v79 新做的动作,做完看不见结果等于没做。

- infoRows() 增加 kvRow('看板', boardOf())
- boardOf() 在 base.board 为空时按 boardId 反查 boards,最后回落到「默认看板」

验证:静态守卫 1068/0(新增 ⑰ 三条)· 反向验证 222/0(新增 2 条注入)
     真机探针 _fb_v79_verify.py 57/0
2026-10-22 18:46:04 +08:00
laoyang 559d79b2e2 v79b 端上:修 ForEach 键——改完状态卡片还显示旧状态
自查发现的隐患,**编译通过、静态断言全绿、节点也都在**,只是内容陈旧:

任务行是 `@Builder taskRow(t: FbTaskItem, …)` / `taskCard(t)` —— 入参按值传递、
不参与状态观察(本仓库文件头第 A 条反模式)。而 `ForEach` 的生成键只有
`'htk_' + t.id` ⇒ 改了状态 → 重拉列表 → `this.tasks` 换成新对象,但**键没变**
⇒ ArkUI 按 key 复用旧节点、不重跑生成器 ⇒ 卡片仍显示旧状态。

用户会看到:「面板里改成已完成了,关掉面板那张卡还是待办。」

修法:新增 `model/Types.taskKey(t)`,键里带**所有会被画到行上的字段**
(status / boardId / dueAt / overdue / childDone+childTotal / title),
首页与任务页两处 ForEach 都改用它;面板里子任务行的键也补上 `k.status`。

⚠️ 代价是状态一变这一行会重建(丢的是这一行的局部动画,不是滚动位置)。
   这是刻意的取舍:**宁可重画,也不要不刷新**。

新增守卫 ⑯ 组 10 条(计数/关系型:
「键生成器必须调 taskKey」「taskKey 必须含这 6 个字段」「子任务键要带 status」),
并补 5 条反向验证注入(退回只拼 id / 漏 status / 漏 boardId / 子任务键缺 status)。

守卫 1064/0 · 反向 220/0 · 编译 BUILD SUCCESSFUL。
2026-10-22 17:33:32 +08:00
laoyang 25a0797a74 v79 端上:任务面板(首页/任务页共用)+ 任务模块功能补齐
用户原话:「鸿蒙端的功能,也需要同样适配,在首页上可以直接修改编辑任务,
另外任务模块的功能需要完善。」

端上原来只有"看列表 / 勾完成 / 新建改标题",网页 v78 那套「就地处理」
(改状态、挪分组、子任务、附件、留言、删除)一件都没有;首页点任务
弹的还是 v73 的只读卡。

一、新增 FbTaskPanel(首页与任务页共用同一组件)
  · 改状态三段(待办/进行中/已完成)+ 标题左侧状态圈
  · 挪看板 chips、勾/加子任务、加图片附件并删、发留言
  · 编辑 → 交给宿主开 FbTaskEditSheet;删除要**点两次**(端上没有 undo)
  · 每个动作成功后再 getTask() 重拉一次:一次动作会牵动子任务计数、
    附件张数、看板色,逐处同步 DOM 必然漏一处

二、退役 FbTaskDetailSheet(v73 只读卡,387 行)
  ⚠️ 它已无人挂载,而守卫还在逐条"通过" —— 等于给死代码盖章。

三、任务模块补齐
  · 卡片点击 → 面板(不再直接进编辑表单:误触一下就开始改标题,
    且子任务/附件/留言没地方放)
  · 「只看已完成」改真打 ?view=done:看板视图服务端就写死
    status <> 'done',在客户端筛永远是空
  · 看板列表改走 ?view=boards:原来从任务反推 ⇒ 空看板永远选不到
  · 编辑表单补分类/循环两排

四、顺带修掉一个自查发现的真 bug
  任务页里 sheetCount 有**两个** @Monitor 都监听 boardSheetOpen、都写同一个值
  ⇒ 关掉设置浮层时谁最后执行不确定,会把还开着的面板"顺便让位结束",
  底部悬浮栏浮上来盖住面板按钮。已并成一个取并集的 monitor。

服务端(另一仓):新增 GET /tasks?view=boards、POST/DELETE /tasks/<id>/attach,
详情补 board_name/board_color_calc,越权删别条任务的附件 → 404。

验证:静态守卫 1055/0(新增 v79 一组,含"全页只许一个 sheetCount 赋值点")、
反向验证 215/0(新增 15 条注入,逐条证明新断言会红)、
服务端 e2e 1558/0、_fb_v79_api.py 26/0、编译通过并已装机。
2026-10-22 16:40:44 +08:00
laoyang c7bd26dd1b v77 旅程:列表只留整卡入口 + 动作区改成 2×2 + 地址不截断 + 题图
用户 2026-10-22 的五条:

① 列表卡去掉「看行程」「编辑」——「点进去之后会有编辑,直接点进去
   看行程就可以」。卡尾整行删除,整卡可点变唯一入口(标题行尾留一个 ›
   作提示);openEdit() 一并删掉,不留死代码。

② 详情页动作区重排——「排列得不好看,乱七八糟的」。旧版是四颗彩色实心
   胶囊分两行、右端对不齐。现在 ☰ 挪到标题行右侧,四颗动作做成 2×2
   等宽(layoutWeight(1));主操作保持实心,编辑/删除改浅底同色字。
   配色与顺序仍是 v41 硬要求,一个 hex 都没改。

③ 出行方式:十种方式**不能增删**(网页版也只是只读 + "常用信息",
   而端上加行程表单里根本没有车次/车牌,那条备忘在端上无从使用)
   ⇒ 方向反过来,给加行程表单补上「车次/航班号」(火车/飞机/轮船/
   公交/地铁)和「车牌」(自驾,从车辆库选一台,服务端按 vehicle_id
   join 出车牌)。没有这两项,"出行方式"在端上就只剩一个图标。

④ 行程详情里的地址显示不全——「从哪到哪,如果地址特别长就显示不全」。
   原来出发地与到达地挤在一条 Row 里各 maxLines(1) + 省略号;
   改成「从」「到」上下两行、整段铺开不截断。

⑤ 旅程题图:端上原来根本没有上传链路(服务端只有观影的 poster 槽位)。
   服务端已加 POST /trips/<id>/cover;端上新建/编辑浮层加 16:9 题图选区,
   流程「先建 → 拿 id → 再传图」,传图失败记 id 再点只补传(不会建出
   两趟一样的旅程)。详情页空题图那块也能直接点开去加。

顺带把 multipart 上传收敛成一份(data/FbUpload.ets):原来头像 / 海报 /
观影照片各抄了一遍,旅程题图会是第四份。
2026-10-22 10:15:03 +08:00
laoyang 79e942d0e4 v76 旅程:信息铺得开 + 每段行程各自成卡 + 照片可点开 / 视频可播
用户 2026-10-22(四条毛病,同一个根因:信息往一行里挤):
「那个城市……如果一行显不下的话,应该显示在下边一行,把所有的城市全列出来。
  就跟那个人员一样,你人员特别多的时候,一行显示不下,就直接往下放就行了。」
「点进详情之后……现在人员显示不下的话,你直接都放在右边,都溢出到这个块外面去了……
  应该直接顺着往下铺就行了,所以它长一点无所谓。」
「实际行程它应该也是一段一段的,不要把它整个实际行程放在一块,
  就是每一个行程都是一个一个块。然后里面的照片要可以点击查看,视频要能够播放。」

根因不是"忘了加滚动",而是 **`Row` 在 ArkUI 里不换行**:
塞不下的子节点会被压缩、或直接横向溢出父容器。四处都改成
`Flex({ wrap: FlexWrap.Wrap })`。

- `Types.ets`:`FbTripItem` 新增 `cities: string[]`(整串只能截断);
  `FbTripNodePhoto` 新增 `mime` / `size`
- `FbParse.parseTrips` 新增 `cityListOf()`:优先用服务端 `cities` 数组,
  只给整串时按 `·` 拆回来("显示不全"不再从别的入口漏进来)
- 列表卡:城市从「📍 一行长串 + 省略号」改成一颗颗 chip,日期单独占一行
- 详情页:城市 / 参与的人 / 每段的参与人 全部改 `Flex wrap`
  (实测「黑龙江」那趟 5 个人自动排成 2 行,不再溢出卡片)
- **每段行程各自一张卡**:组头退成章节标题(不带卡片底),
  `nodeCard` 自己带白底 + 圆角 + 左侧阶段色条(取 `n.stage`)
- 照片:点缩略图开全屏查看器(`PanGesture` 左右滑、上一张/下一张、关闭)
- 视频:按 `mime` 前缀认出视频(`is_image` 只有两态),缩略块 + ▶ 角标,
  点开用系统 `Video` 播放(`media.php` 支持 Range,可拖进度条)

🔴 真机抓到的布局 bug:`Stack` 子节点上的 `.align(Alignment.Bottom/TopEnd)`
  **完全无效** —— 关闭按钮和翻页条双双掉到屏幕正中。改成铺满的 Column +
  `layoutWeight(1)` 撑开,并给控制层 `hitTestBehavior(Transparent)`
  保证"点背景关闭"仍穿透。

验证:真机 33/0(含"城市 chip 数 == 统计里个城市"、"相邻行程卡有实际间隙"、
"点视频块起播放器且无放不出来");守卫 953/0;反向验证 181/0
(其中一条注入暴露了我新写断言的假绿,已改成结构化顺序判据)。
2026-10-22 09:02:16 +08:00
laoyang 8c92195558 v75b 详情页卡片溢出父容器(题图贴到屏幕左右边缘)
真机实测抓出来的:详情页题图 x=0..1080(满屏),而列表卡 x=50..1030。
图比下面的信息区宽 100px,左右各突 50px 贴到屏幕边缘。

根因:详情页每张卡写的是 `.width('100%')` + `.margin({left/right: pagePad})`。
ArkUI 里 `width('100%')` 量的是**父容器宽度**,margin 再往外挤 ⇒ 溢出父容器。

修法照列表页一直以来的写法:**父容器给 padding,卡片只写 width('100%')、
不写左右 margin**。改了三处:heroCard、nodeGroups 空态、stageGroup。

实测:详情题图 x=50..1030 (w=980),与所属卡片 50..1030 完全对齐。
探针 24/0,守卫 932/0,反向验证 165/0。
2026-10-22 08:07:05 +08:00
laoyang 3e2904cc3d v75 旅程重构:列表页 + 详情整页,卡片改「图在上、信息在下」
用户 2026-10-21:
「每一个旅程里面的行程才是重头戏……点击一个旅程,应该进入到一个新的界面,
  在里面列出所有的行程,就跟网页版的一样。
  这个旅程的图片是比较重要的……图片在正上方,就是充满整个的这个列表的宽度。
  然后呢把这个标题还有其他的一些信息放在下面。如果是图片放左边,信息放右边的话,
  它总是被挤占,显示不全。……想看其他的这个旅程的话,直接往下滑就可以了。」

- 新建 `pages/trips/FbTripDetailPage.ets`(照 `trips/show.php`):题图 16:9 占满整宽、
  五项统计、行程按 计划/实际/未成行 三组(空组不显示)、页头两行动作区
  (+计划=蓝 / +实际=绿 / 编辑=黄 / 删除=红 / 设置)
- `FbTripsPage` 846→573 行:删掉就地展开全套,`tripCard` 改「题图在上、信息全在下面」
  卡尾「看行程」自己带 onClick,整卡也可点
- `Types.ets` `FbTripNode` 8→18 字段 + 四个新类型;`FbTripRepo` 新增
  `fetchTripDetail` / `fetchTripSummary` 与四个解析辅助
- 修真 bug:`parseNodes` 读 `expense_cent`,服务端写的是 `spent_cents`
  ⇒ 每段「💰 这一段花了」从来没显示过
- `Store.ets` 新增 `@Trace tripDetailId` 与 `leaveTo(parentKey)`:
  删完整趟回列表要走它(内部 `applyModule` 会 `resetScroll`),
  直接赋 `activeModule` 会让回列表后轻轻一滑就收起悬浮栏
- 接线:`Index` 分发 / `TopBar` 标题「旅程」/ `Modules.fbModuleColor` 归到 trips 色
- `FbTripNodeSheet` 新增 `@Param startStage` 预选阶段(选择器保留,点错还能改)

守卫 931/0,反向验证 164/0,编译 BUILD SUCCESSFUL,真机待设备连上后跑 _fb_v75_verify.py
2026-10-22 00:07:16 +08:00
laoyang 6e7cdaf5fc v74 端上删掉家谱 + 名册页头改「醒目搜索 + 添加」
用户 2026-10-21:「算了,你画不好。这个功能直接从鸿蒙端去掉吧,不要了。
这样名册这个功能,进来以后,上面就是搜索和添加按钮,搜索,做的醒目一点,
现在看不清。家谱和名册的标签,都不要了。家谱的相关功能,都删除。」

家谱链路整条删除(不是隐藏):
- 删 common/KinLayout.ets、components/FbKinGraph.ets 两个源文件
- 删 Types 的 FbKinPerson / FbKinGraph / FbKinRelation、FbParse 的 parseKin
- 删 FbModuleLoad 路由、FbModuleRepo 模块名、FbSyncAll 预同步清单里的 kin
- 删 DemoData 的 FB_DEMO_KIN_RELATIONS、Theme/Settings 的家谱图标映射
- 删死资源 fb_ic_kin.png
- Index 的 kin 分支去掉(不再有「名册/家谱」两个模块共用一个页面)
- FbPeoplePage:删 subView 分段切换、kinHeader/kinView、loadKin、segBtn;
  openDetail 不再借家谱字段兜底,改成纯 id 占位 + 异步补全
  (保留"本地没缓存也要立刻开浮层",那是 v73 修过的"点了没反应")

名册页头:
- 第一行就是搜索 + 添加,上面不再有任何东西
- 搜索框从「看不清」改成醒目:白底 #FFFFFF(原来 inputBg 与页面米色几乎同色
  ⇒ 没有边界)、1.5 主题色描边、高度 36→44、图标 15→18 并染主题色、
  字号 fontSmall→fontBody、提示词收短成「搜索姓名 / 首字母 / 手机」

验证:
- ArkTS 静态守卫 917/0(新增 6 条:家谱五条链路全清 + 搜索框三个具体数值)
- 反向验证 154/0(新增 7 条注入全部有效;另修好两条因代码演进失效的旧锚点)
- 编译 BUILD SUCCESSFUL + 装机 install bundle successfully
- 真机 v74 探针 22/0(高度实测 44.2vp、白底、占屏 66%、同排对齐、
  家谱在页面与抽屉里都不存在、搜索输入与清空真的生效)

顺带修掉探针三处假红(都不是应用问题,是量错了节点):
- 搜索框在界面树里是两层,TextInput(75px) 只是内部输入区,
  白底容器 Row(138px) 才是盒子;只按"有文字/可点"收集会把它漏掉
- uitest uiInput inputText 是 (x,y,text) 三参数,只给文本会静默不输入
- 空态文案是「换个关键词试试…」,按"没有/找到"判永远为假
2026-10-21 23:28:19 +08:00
laoyang 8cabc35400 v73b:家谱连线改成「每对父母一条母线」,去掉重合的横段
旧画法每个子女各画一条独立折线,在真实数据上会画出两条横段完全
重合的线(岳父母→我户 216→72 与 我父母→姐户 72→216,拐点同为
132.9),视觉上是一个「口」字框,看着像父母家和岳父母家连在一起。

新画法:父母单元中点 ──竖──→ 母线高度 ──横(只覆盖自己子女)──→
各子女 ──竖──→ 子女卡片上沿。一条线只服务一对父母,不再交叉重叠。

关键修正:子女挂点必须是**子女本人那张卡**,不是「子女所在户的中点」。
我这一户同时是两对父母的子女户(我父母→我、岳父母→妻子),
用户中点时两条竖线都落在 x=72、完全重合;改用本人卡的 x 后
妻子那条落在 105,两条线才分开。

⚠️ 这类病端点断言完全抓不到(端点都好好贴在卡片上),所以测试侧
新增「没有任何两条线段完全重合」,并在视图侧说明 bus 段是同一折线
公式的退化情形、不必单独写分支。

验证:离线 30/0、反向 13/0、真实数据+屏幕对照 11/0、
像素级 9/0、真机几何 15/0、ArkTS 守卫 905/0。
2026-10-21 22:57:56 +08:00
laoyang 13f716c936 v73:家谱自绘重做(Canvas 画线 + 原生卡片),去掉列表只留图谱
用户原话:「手机端这个家谱实在是很难看,能有什么库之类的,让它好看点吗」

改法:不再全 Canvas 手绘(只有彩色圆点 + 首字,中文又平又硬、做不出圆角阴影),
改成「Canvas 只画线 + 原生 Column/Text/FbAvatar 绝对定位」。
不能改用 Path 组件:Path.commands 单位是 px,而 .position() 是 vp,混用会缩 1/3;
Canvas 绘制坐标系本身是 vp,与 .position() 自洽。

布局算法抽成纯函数 common/KinLayout.ets(可单独验证,不再和"画"揉在一起):
- 横向单位是「夫妻单元」而不是「人」,夫妻并排紧邻
- 子女挂在夫妻单元中点下方(family-chart 语义)
- 迭代松弛 24 轮容纳冲突约束(家谱不是树:姻亲的"挂父母下方"与"夫妻并排"互相打架)
- 全程确定性(排序带 minId 兜底),避免每次重画卡片抖位置

真机抓到并修掉两个真 bug:
1. 户号 0 被当成"没分户":服务端 unit_of 从 0 编号,端上判 > 0
   ⇒ 我与妻子被拆两户(真机 Δx=680px)。修:布局层 >= 0,解析层缺失给 -1,
   不把任何数值当哨兵。
2. 线没连对(用户原话「主要是线没连对」):buildBloodEdges 用 gen 数值当行号,
   minGen=-1 ⇒ 长辈线从 y=-148 起(屏幕外),整组线串到上一代卡片。
   修:新增 rowIndex(gens, gen),用走标换算而不是数值。

验证:
- 离线布局 29/0(含户号 0、脏数据一户三人、镜像常量与端上源码一致)
- 反向验证 11/0(9 处注入,含"户号 0 当没分户"与"用 gen 数值当行号"两条真 bug 复现)
- 真机几何 15/0、真实数据+屏幕对照 9/0(反解密度 3.125x,残差 0.23/0.32px;
  12 个血缘线端点全部贴在卡片上下沿)
- ArkTS 静态守卫 905/0
2026-10-21 22:18:43 +08:00
laoyang 67ce443915 v73b:给卡片刷新加一条可观测的日志(排障用,不改变行为)
真机排障时发现:修复前 pushAll 在注册表为空时是**静默 return**,
hilog 里一个字符都抓不到 ⇒ 出现"卡片不刷新"时只能一路读代码反推
(v73 的跨进程 preferences 根因就死在那行 return 上)。

现在留两条:
· 为空 → warn,明确说"注册表为空(没卡片 或 跨进程没读到)"
· 推完 → info,报"已推 N 张卡(M 种偏移)"

真机实测:勾一次任务后日志出现
  pushAll:已推 4 张卡(1 种偏移)
⇒ 应用进程确实读到了卡片进程写下的 4 个 formId。
2026-10-21 20:13:28 +08:00
laoyang 48a0507455 v73:桌面卡片刷新修复 + 首页日程/任务就地弹详情卡 + 一键导航
用户 2026-10-21 三条:
  「卡片里面的数据并不刷新……应用里面的日程已经有了,但卡片上的日程却并不刷新」
  「首页上的那个日程和任务,点击之后都应该像日程和任务里面的卡片一样弹出来,
    不要进入到日程里面……有地址的还可以直接一键导航」
  「任务也是一样,点击任务之后弹出来详情,可以标记它是不是完成了」

① 卡片不刷新(根因)
   卡片进程(FormExtensionAbility)与应用进程靠 preferences 传 formIds / 翻页偏移。
   SDK 明文禁止跨进程用 preferences,且 getPreferences() 在**进程内**缓存实例、
   同名再取不读盘 ⇒ 应用进程可能永久持有空注册表 ⇒ pushAll() 首行
   `if (list.length === 0) return` 直接返回 ⇒ 卡片只靠系统 30 分钟自刷。
   修:store() 每次先 removePreferencesFromCacheSync 再取;并写死单写者纪律
   (register/unregister/setOffset/clearOffset 只由卡片进程调,应用进程只读)。

② 首页点日程 → 弹 FbEventSheet(canEdit: false,首页不给编辑入口)
   有地址的行整行可点 + 右侧「➤ 导航」胶囊;「编辑日程」才跳日历模块
   (编辑是日历页的职责,不在首页复制第二份表单)。

③ 首页点任务 → 新的 FbTaskDetailSheet
   状态圆可点(勾完成)、标题带删除线、底部「标记为已完成/未完成」+「改一改」。
   状态**必须走 POST /tasks/<id>/status**(PATCH 会忽略 status)。

④ 一键导航 FbNav.ets:startAbility 隐式 Want(不是 openLink,那是 App Linking);
   canOpenLink 需 module.json5 声明 querySchemes(未声明恒 false 且不抛错);
   高德/百度/花瓣/腾讯按序探测,一个地图都没装则复制地点到剪贴板 + Toast。

⑤ 首页登记 FB_BACK(与其余 10 个模块一致),根容器 Stack(浮层是兄弟节点,
   不能当 Column 子项排,否则卡片被挤出屏幕)。

静态守卫 905/0、v73 反向验证 17/0、assembleHap 通过、已装机启动。
2026-10-21 20:01:37 +08:00
laoyang 15bc3c8599 v69b:大卡抬头「星期 + 农历」合成一行(真机上第二行被 Divider 切掉半截)
真机截图看到 4×4 卡的抬头是「10月20日」下面压着半截字(看着像"一十三"):
原来是 12sp 日期 + 9sp 农历**两行**,4×4 卡高度紧,
第二行被下面的 Divider 压掉了一半。

⇒ 四处统一走 `subLine()` 拼成一行(`星期二  十一`):
2×2 / 2×4 本来就是单行、真机上是正常的,大卡跟它们对齐。

守卫加 v66 ⑭(四块卡都必须走 `subLine()`,且不许退回"农历有值就吞掉星期"的写法),
反向验证加一条注入(把 4×4 改回 `Text(this.lunarText)` ⇒ 守卫变红)。
2026-10-20 22:09:51 +08:00