Commit Graph
87 Commits
Author SHA1 Message Date
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
laoyang 719c68202b v69:日程桌面卡片完全照 SyncCalendar 重做(四种尺寸 + 翻页 + 时间轴)
用户 2026-10-20:「跟日程有关的桌面服务卡片,你就完全按照 synccalendar 来做就行。」

上一版卡片是"首页概览"(今日日程 + 在做/待办/逾期 + 纪念日),
是照着首页数据自己设计的版式。这一批按用户要求换成 **SyncCalendar 那套日程卡**:

## 四块尺寸(与 SyncCalendar 一一对应)
- 2×2  日期 / 星期+农历 / 当天条数大数字 + +
- 2×4  左半同 2×2,右半「接下来两条」
- 4×4  `‹ 10月20日 / 九月初一 ›  [今|+]` + 当天日程(时间轴 / 列表)
- 6×4  同 4×4,更宽(小时 40vp)

## 翻页(这是"照 SyncCalendar"的核心)
- 卡片按钮走 `postCardAction({action:'message', params:{pageAction}})`,
  `FbFormAbility.onFormEvent` 改偏移 → 重新取数 → 推回卡片;
- **每张卡一份偏移量**,落 `preferences`(`data/FbCardForms.ets`):
  SyncCalendar 只放内存 Map,进程被回收就丢 ⇒ 系统每 30 分钟刷新一次,
  会把用户翻到的"下周三"悄悄拽回今天。家书这边连 `updateDuration` 的定时刷新
  也按各自偏移重建(`onUpdateForm` 先 `getOffset` 再 `push`)。
- 偏移夹取 ±366 天:message 是外部输入,算到离谱年份会 Invalid Date(白卡)。

## 点卡片要落到"卡片显示的那一天"
- 卡片 router 带 `openDate`(**毫秒字符串**,数字参数是 int32,13 位会溢出)
  与 `widgetAction=add`(点的是+);
- `EntryAbility.applyWantModule` 解析后写 `FbAppState.calJumpIso/Create/Seq`;
- 日程页两条入口都接上:冷启动 `aboutToAppear`、热启动 `@Monitor('app.calJumpSeq')`
  (应用开着时点卡片不会重建页面,只靠 aboutToAppear 会"点了没反应")。

## 数据
- 换接口:`/api/v1/calendar?from=<那天>&to=<那天>`(原来打 `/home`)。
  `authProbe: true` + 显式带 token 的硬要求不变(后台 401 不能把用户踢下线)。
- 农历行(`kind=lunar`)抽成抬头那一行小字,其余衍生条目(节气/节假日)照常进列表。
- 时间轴复用 `common/FbTimeline`(与日程页同一套算法),
  卡片侧只做"比例 → vp"的换算(`fbCardTlLines`,纯函数、可断言)。
- 显示方式(时间轴/列表)跟「日程设置」联动;今天才画当前时间红线。
- 应用内改完日程 → `FB_CARD_FORMS.pushAll()` 立刻刷新桌面卡片
  (不必等 30 分钟;推失败自动注销该 formId)。

## 验证
- `assembleHap` BUILD SUCCESSFUL;已装机(5NC0225B27004718)。
- `_fb_arkts_guard.js` **856 / 0**(v66 组按新契约重写:四尺寸登记、
  字段名与数据类配对、数组必须 JSON 字符串、翻页三动作、
  `onUpdateForm` 按偏移重建、冷/热启动两条入口)。
- `_fb_v59d_falsify.py` **146 / 0**(新增 6 条注入:定时刷新丢偏移、
  偏移不落盘、4×4 少「下一天」、「热启动不响应」、不读 openDate、
  字段名拼错、去 authProbe)。
  ⚠️ 其中"热启动"那条第一次注入**打偏了**:它先命中了文档注释里那半句
     `@Monitor('app.calJumpSeq')` ⇒ 真正的装饰器没被改,守卫照样绿。
     已把字面量从注释里去掉(本项目"注释里别抄字面标签"的老坑)。
- 真机跳转 `_fb_v69_card_jump.py`:
  · `openDate=今天+3天` + `widgetAction=add` ⇒ 日程页落在 **10月23日**,
    且「新建日程」浮层打开、日期就是 10月23日(dump 里 26 个浮层节点);
  · 只带 `openDate` ⇒ 同样落在 10月23日,**不**弹新建浮层。
2026-10-20 21:59:23 +08:00
laoyang 030a9c7fbb v68 真机验收:修两处真机才暴露的问题(账本权限标签 / 查看行程点不动)
设备接上后第一次真机验收(5NC0225B27004718),25 条断言里抓到两个**真 bug**:

## ① 主人账本也挂一颗「可写」
账本切换条的判据是 `if (b.myPerm !== 'owner')`,而服务端 `Ledger::mine/find`
**从来不给 `owner`**(主人也是 `write`)⇒ 这个 if 恒成立,自己建的账本
也被贴上「可写」,看着像"只是别人共享给我的"。
v68 上一批已经把共享浮层改成 `isOwner`,但这处列表标签漏了 ——
同一批里 `myPerm` 的注释还写着 `owner/read/write`,属于照着错误注释抄。
⇒ 改 `if (!b.isOwner)`,并把那段过期注释纠正。

## ② 卡尾「查看行程」是一颗点不动的字
屏幕上明明写着「查看行程」,点它**什么都不发生** —— 因为展开逻辑只挂在
**卡片主体**的 onClick 上,卡尾这颗 Text 是纯文本,没有 onClick。
用户看不到"要点头像/标题",只会去点这三个字。
⇒ 展开逻辑抽成 `toggleExpand(trip)`,卡片主体与卡尾文字共用;
   卡尾那颗补 onClick 并加内边距(fontTiny 的纯文字热区太小,真机不好点)。

## 验证
- 真机验收 `_fb_v68_verify.py`(v66~v68 三批):**25 通过 / 0 失败**
  (改前 19/6 → 修完 25/0;A2、C2/C3/C4 由红转绿)
- `_fb_arkts_guard.js` → **832 / 0**(新增 v68 ③ 两条、v66 ⑫ 三条)
- `_fb_v59d_falsify.py` → **141 / 0**(新增 2 条注入,都能让守卫变红)
  ⚠️ 「查看行程」那条注入第一次写的是"往后 500 字符内找 .onClick",删掉 onClick
     守卫**照样绿**(500 字符内还有「编辑」那颗)⇒ 改成按段切(Text → 下一个 Column())。
2026-10-20 21:35:38 +08:00
laoyang 2ff90519fa v68:财务补齐(改一笔/删一笔/账本改名/共享给谁)
用户 2026-10-20:「还有这个财务的显示也有问题」——v65 修的是**显示口径**
(端上按正负号重加 vs 服务端负债取绝对值);这一批补的是**动作**:
设置里「共享给谁」「账本名字」一直是灰条,账目也改不了删不掉。

## 根因:不是端上没写,是服务端没有路由
`Ledger::rename / setShare / unshare / shares / updateEntry / removeEntry`
**一直都有**,`ApiHandler::ledger()` 却只开了「建账本 / 记一笔 / 删账本」
⇒ 端上只能把这两条画成"暂未开放"。

## 端上做了什么
- 点一行账目 → 同一个浮层进**编辑态**(类别 / 金额 / 名称 / 备注 +
  删除二次确认)。`aboutToAppear` 里把 @Param 拷进 @Local,
  点第二行才不会还是上一笔的内容。
- 设置「账本名字」→ 改名浮层;「共享给谁」→ 共享浮层(列表 + 选人 +
  逐个人取消)。列表解析走 `{items:[…]}`(`FbApiResult.data` 是原始 JSON)。
- 三个浮层共用 `recountSheets()` 让悬浮栏让位(原来只有"记一笔"有)。

## 顺手修的两处「对着服务端抄错」
1. `FbLedgerBook.myPerm` 的注释写 `owner / read / write`,
   而服务端**从来不给 `owner`**(主人也是 `write`)⇒ 端上"是不是我建的"永远 false。
   现在用服务端新补的 `isOwner` 布尔。
2. `FbLedgerEntry` 没有 `note` 字段 ⇒ 编辑时备注框永远是空的。补上类型与解析。

## 顺带
- 撤掉名册家谱那颗「补关系」按钮:从来没有 `onTap`,服务端 `relations`
  只有读接口、网页版也没这个功能 ⇒ 留个永远点不动的按钮只会被当成"坏了"。

## 验证
- `hvigorw assembleHap` → BUILD SUCCESSFUL
- `_fb_arkts_guard.js` → 827 / 0(新增 groupV68 29 条)
- `_fb_v59d_falsify.py` → 139 / 0(v68 新增 9 条注入全部变红)
- ⚠️ 设备仍断连(`hdc list targets` → `[Empty]`),真机未验收
2026-10-20 20:18:38 +08:00
laoyang ec2cb14c4c v67:读书模块补齐(书能点进去 / 加一本 / 读书笔记 / 统计)
用户 2026-10-20:「其他的功能也逐步完成……读书点不进去,也没法加书,
设置也没有任何的东西。」

## 先修三处「对着服务端抄错的口径」(都不报错,只是显示是错的)
1. 状态值:服务端 `Book::STATUS` 只有 wish/reading/done/paused,**没有 want**;
   `Book::status()` 遇到不认识的值会静默改成 `wish`,而端上分组键写的 `want`
   ⇒ 书架里「想读」那一组**整组不显示**(页头还数着它,看着像书丢了)。
   `paused`(搁置)同样没列 ⇒ 搁置的书也人间蒸发。
2. 评分:服务端 `rating` 是 0–10,端上当 0–5 用 ⇒ 8 分显示成 `8.0/5`。
   现在解析时折半、写回时 ×2。
3. 进度:服务端 `progress` 是「读到第几章」,端上当百分比 ⇒ 读到第 30 章 = 30%。
   现在按 `chapter_count` 换算,没解析过章节的书不画假进度。

## 补齐三个死掉的入口
- 书行整行可点 → `FbBookSheet`(状态 / 评分 / 章节进度 / 读书笔记 / 删除二次确认)
- 页头「+ 加一本」→ 同一浮层的新建态(书名 / 作者 / 状态)
- 设置「读书统计」→ 端上现算(藏书/在读/读完/想读/搁置/笔记条数);
  另两条(导入笔记 / 微信读书)要文件选择与授权,仍标「网页端」,不摆假入口

## 服务端能力边界(照实做)
- `PATCH /books/<id>` 只认 status / rating ⇒ 详情里不给改书名/作者的输入框
- 章节进度只读(`progress` 由阅读器翻页写,接口没有回写入口)
- 读书笔记走真实接口 `GET/POST /books/<id>/notes`

## 验证
- `hvigorw assembleHap` → BUILD SUCCESSFUL
- `_fb_arkts_guard.js` → 797 / 0(新增 groupV67 28 条,含「端上分组键 ==
  服务端枚举」的跨语言对照)
- `_fb_v59d_falsify.py` → 130 / 0(v67 新增 13 条注入全部变红)
- ⚠️ 设备仍断连(`hdc list targets` → `[Empty]`),真机未验收
2026-10-20 17:14:40 +08:00
laoyang 58f5509c1c v66 续:旅程三项设置(车辆管理 / 出行方式 / 节点类型)
用户 2026-10-20:「旅程的……车辆管理、出行方式、节点设置,这些东西都还没完成。」
这三项的 `action` 一直是**空串**(点了没反应),是本次批量需求里最后一块。

三项的真实能力并不一样,按真实情况做,不摆假入口:
· **车辆管理**:`vehicles` 表 + `Vehicle::save/destroy` 早就有,缺的只是 API
  ⇒ 新增 `GET/POST/PATCH/DELETE /api/v1/vehicles`;端上列表 + 内嵌表单
  (车型 / 车牌 / 自家·租的·借的 / 车主或租车公司 / 订单号 / 默认车 / 备注)
· **节点类型**:`node_types` 表 + `NodeType::save/destroy` 同理
  ⇒ 新增 `GET/POST/PATCH/DELETE /api/v1/nodetypes`;改名会**级联**到已有行程
  (`trip_nodes.node_type` 存的是名字,服务端 `NodeType::save` 里做)
  · 内置类型**不画删除按钮**(服务端会拒,画了就是"点了没反应")
· **出行方式**:**枚举常量**(`Trip::TRAVEL_MODES`),没有"管理"这回事
  ⇒ 只做一份只读对照表 + "加行程时选",**不做可编辑表单**
  (反向验证里有一条专门锁"这个浮层里不许出现保存/删除")

几个实现上的要点:
· 两条新路由是**平级资源**(`/vehicles`、`/nodetypes`),不是 `trips/xxx` 子路由 ——
  `Api::dispatch()` 只解析三段,写成四段会落到「trips 的第 0 条」= 列表
· 删不掉(还有 N 段行程在用 / 内置类型)回 **422 + 服务端原话**,不是 500:
  那是"现在不能删",不是"服务器出错"
· 表单输入框**不按值传 value**:`@Builder` 的值参数不参与状态观察,
  而这个浮层是"列表 + 内嵌表单"(点另一行的「编辑」时表单已经渲染着)
  ⇒ 会显示上一辆车的内容。改成传 key、builder 内部直接读 `this.fXxx`
· 三个浮层都遵守浮层铁律(根 `Stack` 叠放 + `hitTestBehavior(Transparent)`),
  各挂一个 `@Monitor` 让底部悬浮栏让位;`backOnce()` 按层级顺序关

服务端(`app/ApiHandler.php`)已随 `familybook_push.sh` 部署,线上核对:
`/api/v1/ping` 的 features 里 `trips_vehicles` / `trips_nodetypes` 均为 1,
两个端点无令牌时回 **401 unauthorized**(说明路由存在,而不是 404 not_found)。

验证:`hvigorw assembleHap` BUILD SUCCESSFUL;静态守卫 769 条全绿;
反向验证 116 条注入全部能让守卫变红(含"字段名拼错""按值传 value"等假绿陷阱)。
⚠️ 设备仍断连,真机未验收。
2026-10-20 16:36:30 +08:00
laoyang b3e1330244 v65/v66:六个空壳模块接线 + 桌面服务卡片
用户 2026-10-20:
「做几个和 syncCalendar 差不多的卡片。另外其他的功能也逐步完成……旅程的
新建旅程啊,车辆管理、出行方式、节点设置……资源也点不进去……任务也点不
进去,也没法完成……纪念日、新增纪念日……财务的显示也有问题……笔记现在
也点不进去,记笔记也记不了。」

v65 —— 六个模块(任务 / 资源 / 纪念日 / 笔记 / 账本 / 旅程)原本是**同一套
空壳模板**复制出来的:`FbPrimaryButton` 没有 `onTap`、`ListItem` 没有
`onClick`、设置菜单的 `action` 全是空串。这一版把它们全部接上真实接口:

· 任务:新建 / 编辑(`FbTaskEditSheet` + `FbTaskRepo`)、列表可点、勾完成
  (走 `/tasks/<id>/status` —— 服务端 `PATCH` **显式忽略** status,走 Patch
  会得到"看起来成功的假成功")、按看板筛选
· 资源:新增 / 查看 / 改 / 删(`FbResourceSheet` + `FbResourceRepo`);
  过滤维度从**并不存在的**「证件 / 资料」改成按权限与归属
  (`resources` 表没有 category 列,那个筛选一直是筛空的)
· 纪念日:新增 / 编辑(`FbDayEditSheet` + `FbDayRepo`);派生生日
  (id=0、来自名册)只给提示不给编辑 —— 点开 `/days/0` 必然 404
· 笔记:记一条 / 打开 / 改(`FbNoteSheet` + `FbNoteRepo`),正文按 blocks 存
· 账本:记一笔(`FbLedgerEntrySheet` + `FbLedgerRepo`);修掉财务显示自相
  矛盾 —— 资产/负债改回**服务端 summary**(原来端上按金额正负号重算,与
  服务端"负债取绝对值"的口径打架,会算出 净资产 ≠ 资产 − 负债)
· 旅程:题图卡片(封面 + 标题 + 城市)+ 行程 + 新建旅程 + 新建节点

服务端同时修掉两处**静默数据丢失**(网页端因提交格式凑巧没暴露,只有走
API 的客户端会中招):
· 新建笔记传的是 `body`,而 `Note::create()` 只读 `blocks` ⇒ 新笔记只有标题
· 改笔记时 `blocks` 恒为空,而 `Note::update()` 是整组替换 ⇒ 只改标题就把
  正文全删光;另修 `annivPeopleOf()`(`find()` 的 SQL 不含 people,原来那个
  兜底恒为空 ⇒ 只改纪念日标题会清空关联人)

v66 —— 桌面服务卡片(用户选了「桌面服务卡片」这一项):
· `FbFormAbility`(FormExtensionAbility)+ `form_config.json` 三块尺寸:
  2×2 今日概览 / 2×4 左右分栏 / 4×4 今日全景
· `FbCardData` 只打 `/home` 一个接口(与网页首页同源),数据**扁平化**后
  由 `formBindingData` 推给卡片;断网时用上次成功的缓存并注明"离线"
· 卡片是后台进程 ⇒ 请求带 `authProbe: true`:401 **不能**触发全局"踢回
  登录页"(那会顺手把正在用应用的人踢下线,v62 那次事故同一个坑)
· 点卡片 → `postCardAction(router, fbModule)` → `EntryAbility` 读 Want 参数
  直接落到对应模块;补了 `onNewWant`(没有它,第二次点卡片"什么也没发生")
· 4×4 右上角 ↻ 走 `postAction(message)` → `onFormEvent` 重取数再推回

验证:`hvigorw assembleHap` BUILD SUCCESSFUL;打包后的 hap 里确认
`FbFormAbility`(type=form)与 `$profile:form_config` 都在,卡片代码进了
`ets/widgets.abc`。静态守卫 730 条全绿,反向验证 110 条注入全部能让守卫变红。
⚠️ 设备仍断连(`hdc list targets` 为空),真机尚未验收。
2026-10-20 15:27:32 +08:00
laoyang 3b7238a418 v64b:修真机弹层"点了没反应"(根容器 Column → Stack)+ 日期行热区
四个日期/选择类浮层在真机上**完全打不开**:编译无告警、`aboutToAppear` 照常调用、
`dumpLayout` 里连弹层一个节点都看不到、点它会"穿透"到底下的列表。

根因(逐层二分定位):根容器写成 `Column` 且里面**先排一个整高遮罩**
(`Column().width('100%').height('100%').backgroundColor(FB_MASK)`)再排卡片。
`Column` 是**线性**布局 ⇒ 遮罩占满整高后,卡片被排到屏幕**外面**。
⇒ 全屏遮罩与卡片要"叠"(`Stack`),不能"排"(`Column`)。

改动:
- FbDateTimeField / FbCinemaPickSheet / FbPersonPickSheet:根容器改
  `Stack({ alignContent: Alignment.Center })`,遮罩与卡片作并列子项叠放;
  去掉原先用于"居中卡片"的 `justifyContent(FlexAlign.Center)`(Stack 已对齐)。
- FbDateTimeField:「日期行」的 `.onClick` 由内层 `layoutWeight(1)` 的 Row
  挪到**根 Row** —— 挂内层时那一行会被表单所在的**两列容器**盖住,
  点击穿透到底下的观影列表(表现为"点日期行,列表反而响应了")。
- FbSheetClose:`onClose` 由 `@Param` 改 `@Event`(语义归位;实测 `@Param`
  声明回调**并不会失效**,✕ 一直是能点的 —— 这不是本轮 bug 的根因,别混为一谈)。

验证:
- 构建 BUILD SUCCESSFUL;
- 静态守卫 644/0(新增 8 条:三个组件的 Stack 叠放 + 整高遮罩非空转 +
  全项目无"根 Column 先排整高遮罩"坏形态 + 日期行热区在根 Row);
- 守卫反向验证 92/0(新增 6 条注入,含把根 Stack 改回 Column、删遮罩、
  热区挪回内层,逐条证明断言真会红)。
2026-10-20 08:55:27 +08:00
laoyang 8146aab1a7 v63+v64:影院管理补齐(v63)与观影「记一场」豆瓣/弹框选择(v64)
⚠️ 本提交含**两批**改动(同一批文件里交织,无法按文件拆开):
  · v63(影院管理与搜索/统计/年报拆成平级入口)—— 本会话之外的工作,
    随本轮一并提交以免工作区长期留半成品;
  · v64(本会话,用户 2026-10-19 六条)。

v64 六条:
① 片名可经豆瓣自动获取简介与海报(与网页端同一份服务端输出:
   summary 全文 + 已落盘的 poster_key);简介**只填空白的**,不覆盖用户写过的。
   联想 320ms 防抖(与网页端同值)。
② 开始/结束改成**与日程同一个**日期时间选择控件 —— 新抽 components/FbDateTimeField.ets
   (行 + 弹层 + 一组换算函数),日程那边旧的自绘实现整段删掉改用它
   (用户要的就是"两处一样")。
③ 影院、同看人改成**弹框选择**:
   · FbCinemaPickSheet:顶部搜索框,匹配「地方 + 名称」,「不记录影院」恒在列表;
   · FbPersonPickSheet:顶部搜索框,拼音首字母**子序列**匹配(yyq 打成 yq 也能搜到),
     多选走"暂存 + 确定"两段式(点一个不关窗)。
   两排横向胶囊/头像全删(用户:"这种方式太难找了")。
④ 允许上传**多张**图片:海报 1 张 + 观影照片可一次选 9 张、可多次追加、可逐张删。
   ⚠️ 顺序是**先建记录再传图**(建记录失败就不会留孤儿图);图失败**不挡记录**。
⑤ 删掉「地点」输入口(location 字段仍发,网页端与历史数据不受影响)。
⑥ 观影页搜索框改明显:白底 + 实色描边 + 图标放大到 18 并染主题色 +
   占位文字提到 muted/fontBody + 右侧「筛选 ›」暗示(用户:"根本看不清")。

FbMovieRepo 新增 doubanSuggest / doubanFetch / uploadPoster / uploadPhoto
(multipart 手写,与头像上传同一形态,抽成共用的 uploadOne)。

验证:静态守卫 636/0 · 端上反向验证 87/0 · 接口 e2e 57/0 · 服务端反向验证 18/0 ·
本地全量 e2e 1521/0 · hvigor 编译成功并已装机。
⚠️ 真机验证脚本 _fb_v64_verify.py 已写好(六条全覆盖),因设备锁屏未能执行。
2026-10-20 07:35:37 +08:00
laoyang 4a063114c5 v62:修 P0 登录被踢 / 关闭按钮统一右上角 / 观影三处 / 影院管理补齐 / 名册三处
用户 2026-10-19 第二批八条(原话逐条对应):

═══ ⑨ P0:「现在还是不稳定,会退出,提示:这台设备已经被退出登录,请重新登录。」

真因**是我 v61 自己埋的**:「个人设置」页(`FbAccountPage`)里两处 `fbApi`
调用**取了 `token` 变量却没传进请求选项** ⇒ 没有 Authorization 头 ⇒ 服务端 401
⇒ `FbHttp` 的全局回调 `unauthorizedCb` **清掉本地令牌 + 踢回登录页**。
而本页在 `aboutToAppear` 里就调它 ⇒ **点一下右上角头像,App 就退出登录**。

⚠️ 我上一轮把同一现象归因成"残留旧令牌 / 设备会话被顶掉"(还去线上查了
   `api_tokens` 表,一切正常)—— **那个结论是错的**,等于把 bug 归给了环境。

修法(两处调用 + **两道架构防线**,缺一不可):
  ① 修两处调用,把 token 传进去;
  ② `fbApi` 内部:**本地有令牌、调用方又没传时自动补上**
     (大部分接口本来就要鉴权,补上是"纠正";`/login`、`/ping` 带了也无害);
  ③ **401 只在"确实带了令牌"时才算被踢** —— 否则"没带令牌"也会触发全局清令牌,
     那正是这次事故的机制。

═══ ④「你这个按钮到底是在弹框的右上角关闭,还是在下面关闭……全放在右上角。」

选**右上角 ✕**(底部已被 编辑/保存/删除 占满,再塞关闭会堆三四个按钮,分不清主操作)。
新增 `components/FbSheetClose.ets`:
  · `FbSheetClose`  —— 32vp 圆形 ✕(热区 40+,小图标在手机上点不中);
  · `FbSheetTitleBar` —— 左标题 + 右收尾(设置类给 `confirmText` 显示「完成」)。
9 个浮层全接;名册详情 / 日程详情 / 简要卡**底部不再有「关闭」**。
⚠️ 浅色底走 `fbAlpha`,**不能拼 `+ '14'`** —— ArkUI 八位色是 `#AARRGGBB`
   (本项目曾因写 `#00000066` 导致遮罩**从来没显示过**)。

═══ ①「观影……弹出的框左上角有一条灰色的线,不知道是什么。」

那是浮层顶部的**抽屉把手**(36×4 的 `line2` 浅灰短线)。本意是"可下拉关掉"的提示,
但 ① 它落在左上角、旁边没有文字;② 这个浮层的关闭本来就不靠下拉。
⇒ 用户认不出它 = 它没起作用,**删掉**(不改成居中 —— 那只是把灰线挪个位置)。

═══ ②「然后这个观影人的头像并没有带进来。」

同看人的 `FbAvatar` **漏传 `imageUrl`** —— 服务端 `Movie::attendees()` 本来就
`SELECT p.avatar`、端上 `parseAttendees` 也解析进了 `a.avatar`,
纯粹是**渲染时忘了用它** ⇒ 有头像的人也退化成色块。
⚠️ 与名册详情 2026-10-15 那个是同一个 bug(当时是 `FbPersonSheet` 漏传)。

═══ ③「点击这个图片呢,不能左右滑动看所有的这个图片。」

全屏查看器原来**只有底部「上一张/下一张」文字按钮**。现在整张大图左右滑翻页。
⚠️ 用 `.gesture(PanGesture(Horizontal))` 而不是 `SwipeGesture`:
   `SwipeGesture` 要"快速划动"才识别,在有滚动容器的页面容易被先吃掉
   (日程页实测:`gesture`/`priorityGesture` 都不触发)。`PanGesture` 看**累计位移**。
⚠️ 判定要求 `|dx| >= 40 && |dx| > |dy|`,否则竖划会被误判成翻页。

═══ ⑤「影院管理……增加影院呢、修改影院呢、高级搜索、统计、年度报告,这些全部都应该补充进去。」

端上原来**只有一个只读列表**(连"新增"都没有)。补成四个页签:
  · **影院**:新增 / 修改 / 删除(服务端 CRUD 早就有,端上一直没接)+ 删除二次确认;
  · **统计**:总览(看过/影院/人数/人次)+ 按影院/按年/按月的条形图;
  · **搜索**:片名 / 影院 / 起止日期(条件全空时不查,与网页 `hasCond` 同行为);
  · **年报**:按年切换(第一场/最后一场、最常去的影院、一起看最多的人、每月分布)。
⚠️ **统计/年报必须联网**(`/movies/stats|report`):口径要与网页**完全一致**
   (同一份 `MovieStats`)。本地库算出来的是"已同步的那部分",
   少同步一次就少一部 ⇒ 用户会发现"网页 124 部、App 121 部"。
   ⇒ 断网时明说"需要联网",而不是给一个偏低的数字。
⚠️ 影院写操作后**立刻回写本地库**(`refreshCinemas`),否则刚建的那家要等下次同步
   才出现,看着像失败;并用 `INSERT OR REPLACE` 而非"清空再插"
   (中途失败会留下一个空表 = 用户的影院列表整个消失)。

═══ ⑥「名册搜索,搜索后,下面有一个白色的框,什么内容都没有,删除。」

那是列表**末尾给悬浮栏留高度的占位块**(`height(FB_DOCK_RESERVE)`),
底色是 `FbColors.surface`(**白**)而列表容器是 `paper`(**米**)——
列表内容一短(搜索只剩一两条)它就露出来,看着像"什么都没有的白框"。
⇒ 改底色与列表同色。**不能删**:删了最后一张卡片会被悬浮栏压住。

═══ ⑦「如果一个人没头像,在列表是有一个文字的头像的,那么点击后弹框,也应该有。」

`FbAvatar` 原来是 `if/else`:有 `imageUrl` 就**只画 Image**,
而图片**加载失败时 `Image` 什么都不显示**(只露出纯色底)⇒ 看着是个光板。
(`alt()` 只在**加载中**起作用,失败后是空白。)
⇒ 改成**叠加**:底色 + 文字头像**常驻**,图片有就叠在上面
(加载成功即盖住文字,失败则自动露出文字头像)。
⚠️ 之前注释说"图层叠会错位"——那是因为当时想靠 Image 的 intrinsic size 撑尺寸;
   现在两层都显式给 `dia`,尺寸来源唯一,不存在错位。

═══ ⑧「现在名册的列表太空了,应该显示更多内容,例如工作单位、职务。」

列表卡片加一行「单位|职务」(独立成行,避免与标签/生日挤)。
⚠️ 用 `persons.org` / `persons.title` 而**不是** `person_jobs`:
   列表读的是**本地库**,而本地库只存 `persons` 那一行
   (`person_jobs` 是一对多,端上没同步那张表)⇒ 服务端加了字段也到不了列表。
   线上实测 801 人里 192 人有单位、48 人有职务,列表立刻有内容。

验证:
· 编译 BUILD SUCCESSFUL
· ArkTS 守卫 536/0(新增整组 groupV62,含 P0 两道防线)
· 反向验证 51/0(新增 13 条注入,逐条确认能变红)
· Schema 列守卫 12/0
· **真机 22/0**(6 组,含照片横滑 `1/5 → 2/5`、四个页签、点头像不被踢)
2026-10-19 15:27:31 +08:00
laoyang 853b0e0b20 v61:顶栏标题=模块名 / 抽屉瘦身 + 头像进个人设置 / 删除按钮红底 / 浮层动画 / 日程卡片与手势
用户这一批九条,逐条对应:

1)「页面上左上角的标题,不再是家书了,而是切换到那个模块,就显示哪个模块的标题。
   这样在页面内容部,就不再出现模块的名字标题了。」
   → TopBar 主标题改读 moduleName()(通知/设置/个人设置三个非模块路由也有名字);
     观影/纪念日/读书/笔记/通知五处页内大标题删掉。

2)「按钮,也需要和主题色保持一致,出现文字的时候,例如观影模块,
   在左上角出现的时候,颜色也需要和主题色保持一致。」
   → 标题色走 fbModuleColor(app.activeModule)(一处算,11 个页面不用各写)。

3)「弹框的外圈颜色,跟模块的主题色保持一致。」
   → 日程详情原来传 bookColor(日历本色)⇒ 改 accentColor();
     7 个模块设置浮层补传 accent: this.accent。

4)「删除按钮全部用红色背景。」
   → 日程编辑/人员编辑从红描边改红底白字;详情「删除联系人」从浅红底改红底白字;
     两处确认框统一 FbColors.red。

5)「做几个动画,例如,点击名册列表,应该有一个过渡动画出现弹框,
   点击详情,框变大了,也应该有一个过渡动画,点编辑,也有动画。」
   → 新建 common/FbAnim.ets(fbMaskTransition / fbSheetTransition /
     fbSheetGrowTransition / fbViewTransition),9 个页面共 17 遮罩 + 16 浮层挂上;
     简要卡→详情用 0.96→1 的"继续长大"(0.94 会先缩一下)。

6)「左侧抽屉,把通知消息、个人设置、修改密码、系统设置,都去掉,
   放在个人设置页,就是在点击右侧头像那里。消息通知不用放进去,
   直接在头像旁边那里就可以了。」
   → 抽屉底部那四行整块删除(抽屉只剩品牌区 + 模块列表);
     新建 pages/account/FbAccountPage.ets(整页,__account 分发),
     点头像 switchModule('__account');改密码真打服务端新接口。

7)「日程……日期下面:12本日历可见,本月124条,这句话去掉。
   点击日程弹出的框,编辑按钮也颜色也要修改。
   全天日程,例如农历,上面显示无标题,读不到这条日程,这个有问题。」
   → 删统计行;编辑按钮从写死名册青 #4A7C82 改模块主题色;
     真根因:衍生条目(农历/节气)的 detailFull **一直是 null**
     (它们没有 /events/<id> 可拉,原来直接 return)⇒ 垫 shellOf(),
     并把"整卡报错"改成"照常画内容 + 一行小字";
     空标题兜底从「(无标题)」改成日历本名。

8)「再做几个卡片,先做日程的,这个参考 synccalendar。」
   → FbDayEventPanel.eventRow 照 SyncCalendar:左侧 4vp 圆头色条 + 标题 +
     时间/全天 + 「↻ 重复」徽标 + 右端日历本名(限宽 30%、同色);
     一行一张独立圆角卡(12)+ 描边,卡间 space 8,不再用 Divider 连成一片。

9)「日程,有很多手势,你参考 synccalendar。」
   → 月历上滑→周视图、周条下划→月视图、周条左右滑→换周。
     真机实测两次踩空后定下写法:**竖向必须 parallelGesture(PanGesture)**
     (外层是 Scroll,SwipeGesture 的"快速划动"会被它的内置滚动先吃掉,
      gesture/priorityGesture 都不行);**横滑用 gesture(PanGesture)**
     (同一节点两条方向不同的 parallelGesture 会互相压制);
     方向按累计位移判、并要求 |dy| > |dx|。切视图走 animateTo + fbViewTransition。

验证:
· 编译 BUILD SUCCESSFUL
· ArkTS 守卫 493/0;Schema 列守卫 12/0;反向验证 39/0(新增 22 条注入逐条确认能变红)
· 服务端接口 e2e 20/0(独立副本 + 真 MariaDB,5 种错误路径 + 原令牌仍有效)
· 真机 _fb_v61_verify.py 21/0(6 组全过);像素采样确认浮层主题色环实测存在
2026-10-19 12:19:54 +08:00
laoyang 1a67247407 v60b:所有弹框统一主题色描边环 + 编辑改居中弹框 + 修名册下拉回弹吃掉悬浮栏
1)9 个浮层统一描边环(抽组件,不复制 8 遍)
   新增 components/FbSheetRing.ets:环宽 FB_RING_W=2 全工程唯一一处;
   实现是"外层 Column 铺色 + padding",不是 border(border 画在盒子内侧);
   外圆角 = 内圆角 + 环宽。简要卡/详情/编辑/日程详情/日程编辑/观影详情/影院/
   日程设置/模块设置全部改用;各浮层的环色 = 所属模块的主题色。

2)编辑从"贴底满宽 + 只有上圆角"改成居中弹框
   人员编辑与日程编辑:Stack 对齐改 Center,卡片 92% 宽 + 四角圆 + 环;
   constraintSize 留在卡片自己身上(挪到外层会让内层 Scroll().layoutWeight
   拿到 0 高度,资料区塌成一条线)。日程的日期时间选择弹层、两个删除确认框
   一并套环,避免同一次交互里出现两种形态。

3)修名册下拉到头回弹后悬浮按钮消失
   EdgeEffect.Spring 在越界松手后把内容弹回,每帧 offset>0 被当成"用户在往上滑"
   ⇒ 收起悬浮栏。判据 = 手指已抬起 + 最后一次方向是下滑 + 抬手不到 500ms;
   拦截必须排在 offset >= HIDE_AT 快路径之前(回弹第一帧位移常 >= 12vp)。

4)真机截图暴露的两个观感问题(静态检查全绿也照错)
   - Scroll().layoutWeight(1) 让短内容也撑满 maxHeight ⇒ 卡片空半屏
     (日程详情/模块设置),改为 flexShrink(1);内容本来就长的浮层不动。
   - 人员编辑"虚拟人物(还没弄清真实身份)"被卡片裁掉半句:两个小开关从
     并排 Row 改为 Flex(wrap) 换行,间距改走 margin。

验证:ArkTS 守卫 444/0;反向验证 17/17;真机 _fb_v60b_verify.py 11/0,
且关掉回弹拦截重编重装后该用例确实变红(证明测试不是假绿);
9 个浮层逐个真机截图确认。
2026-10-19 08:29:44 +08:00
laoyang 7858de27e3 v0.8.4 简要卡观感 / 分享 / 名册搜索 / 全局返回手势(v59e)
八条需求的落地与真因:

1. 浮层与白底"糊在一起"、看不清 —— 根因不是配色,是**遮罩从来没画出来过**:
   ArkUI 的八位色值是 #AARRGGBB(alpha 在前),而全应用写的是 '#00000066'
   (本意"黑 40%")⇒ alpha = 0x00 ⇒ 全透明。同型还有 brand + '1A'(拼到了蓝通道)
   与 shadow('#00000033')。⇒ 17 个文件统一改走 fbAlpha() / FB_MASK / FB_SHADOW /
   FB_HAIRLINE;真机像素采样复验:遮罩 (148,146,140)、卡片白、浅底 (237,242,243)。
2. 简要卡外圈加**主题色描边环**(外层铺色 + padding,不是 border —— border 会被
   内层圆角切出白缝)。
3. 简要卡**去掉删除按钮**,删除挪到完整详情卡最下面(Divider + 红字块)。
   顺手修一处静默失败:confirmDelete() 原来只取 quickSource(),
   从家谱直接开详情时 quickPerson 为 null ⇒ 点删除毫无反应。
4. 信息行间距 10 → 16(每行再各加 3vp 上下内边距,实测行距 134/118px)。
5. 主电话整行可点**直接拨号**(call.makeCall,落到系统拨号盘)。
6. 按钮改为 详情 / 编辑 / 分享 / 关闭;分享走系统分享面板,
   内容为纯文本「姓名 / 电话 / 邮箱」三项(用户点名的就是这三项,
   不多带单位),面板拉不起来时兜底复制到剪贴板并提示。
7. 名册搜索"完全不能用"有两个独立真因:
   ① 搜索框是 List 的第一个 item,无结果时整个 List 被空态替换 ⇒ 输入框被卸载、
      焦点丢失 ⇒ 输不进第二个字。改:List 无条件渲染,空态自身做成 ListItem。
   ② rowToPerson 把 initials 写死成空串,服务端下发的拼音首字母从未落库 ⇒
      拼音搜索恒 0 结果。改:FB_DB_VERSION 6→7 + PRAGMA 探测补列 + 同步时落库;
      匹配同时遍历全部电话(原来只比主号)。
   另加搜索框「清空」(24×24 命中区)。
8. 全局返回手势:应用是单页 + 模块分发、没有路由栈 ⇒ 自建 FB_BACK 登记表
   (倒序询问)+ moduleHistory + applyModule()(不记历史,否则 A/B 两页来回弹)。
   9 个页面登记/注销;Index.onBackPress 先关浮层 → 回上一模块 → 才 return false
   交给系统退桌面(onBackPress 必须声明 boolean,返回 void 会被当成没接管)。

验证:
- 静态守卫 417 / 0(新增 ⑤阴影·描边·色环 ⑥透明度色 ⑦返回手势 ⑧搜索 ⑨分享)
- 反向验证 10 / 0(9 条注入全部能让守卫变红;其中"遮罩换纯黑"那条暴露了
  一条存在型假绿,已改成数个数 + 禁止 Color.Black)
- 建表/迁移列一致性守卫 12 / 0
- 真机全模块遍历 14 / 14;真机逐项复核 12 / 0 与 7 / 0

工具侧顺带修好两个过期的探针:
- 遍历探针仍用"应用内横滑"打开抽屉,而该手势早已按用户要求去掉、入口改为顶栏图标
  ⇒ 一次全量遍历里 12 个模块全报"抽屉里没找到"(像应用坏了,其实是探针过期)。
- 资源/笔记两页写死了早已不存在的条目(户口本/房产证/阳台改造清单)⇒ 改成
  结构性文案(副标题 + 常驻筛选胶囊),不随用户数据变化而恒红。
2026-10-18 23:12:57 +08:00
laoyang 1c1a859464 修「名册卡片右侧贴屏幕」的真凶:width('100%') 与 margin 打架(不是 flexShrink)
真机实测纠正了我第一轮的误判。

现象(用户):「名册列表左侧有空隙了……怎么右侧不是呢?右侧还是紧贴屏幕。」
像素实测:左米色 52~72px、**右米色 0px**;布局树里卡片盒子 x = **50~1080**。

🔴 真根因:卡片**同时**写了
    .width('100%')
    .margin({ left: 16, right: 16 })
  `100%` 是相对**父容器全宽**(= 屏宽 1080),margin 再把盒子往右推
  ⇒ 盒子变成 50~1080(左边留 50、右边 0)—— 正是用户看到的形态。
  ⇒ 去掉 `.width('100%')`,Row 按"父宽 − margin"自适应,两边都是 16。
  真机复验:卡片盒子 x = 50~**1030**(左右各 50,对称);
            像素扫描 y=860/880/1090/1310 全部 **左=右**。

⚠️ 我第一轮误判成"内容把卡片撑破",去加了 `flexShrink(1)` —— 那是**错的**:
   `flexShrink` 防的是"长名字把同行的小标签挤没",与本 bug 无关。
   真正的证据是**布局树里卡片自己的 x 是 50~1080**(不是子节点越界)。
   ⇒ 教训:**先量"卡片本身"的盒子,再怀疑子节点。**
   (`flexShrink` 保留 —— 它本身是有价值的防护,但注释已改正,
     不再宣称它是本 bug 的修法。)

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 378/0;
_fb_v59c_falsify.py 16/0(本批新增第 16 项注入:把 width('100%') 加回卡片,
必须被抓住。该判据本身也反复调了三轮才既不恒红、又能抓住注入:
"别处也有" ⇒ 恒红;"取卡片尾部窗口" ⇒ 覆盖不到注入点;
"取 personRow 整段" ⇒ 段里天然含 4 处 width(3 处在注释里)⇒ 基线红;
最终 = **剥注释 + 只看卡片自己那一层**)。
2026-10-18 21:56:34 +08:00
laoyang 412d2ae2f7 v0.8.3 头像显示 / 主题色标签 / 右侧贴边 / 简要卡片 / 浮层分离
用户 2026-10-18 第二批五条:

①「右上角的个人头像没显示。」
   🔴 真根因:`/me` 与 `/login` 返回的 `avatar_url` 是**相对地址**
      (`media.php?k=avatars%2Fxxx.jpg` —— 不带域名、不带令牌),
      那是给**浏览器**用的(同源 + cookie);`Image()` 要完整 URL ⇒ 加载失败。
      真机日志:`GetAsset failed: media.php?k=avatars%2F20261002_...jpg`
   ⇒ 新增 `fbMediaUrl(raw, token)` 归一化,**三种形态都认**
      (绝对地址 / media.php 相对式 / 裸 key)—— 少认一种就会出现
      "有的头像能显示、有的不能",那类 bug 最难查。
   ⚠️ 不能复用 `fbImageSrc()`:那是"附件 key → 本地缓存"的通道,
      把整条 URL 当 key 传进去会得到一个必然 404 的地址。

②「名册、家谱这两个标签,用的不是主题色。」
   🔴 两个调用点**各硬编码 `'#A84A28'`**(品牌砖红),而名册模块主题色是
      `#4A7C82`(青)⇒ 与整页不在一个色系。
   ⇒ `segBtn` 不再由调用方传色,内部取 `fbThemeVars(fbModuleOf('people').color).brand`;
      顺手消掉"两处各写一遍字面量"这个漂移源。

③「名册列表,左侧有空隙了……怎么右侧不是呢?右侧还是紧贴屏幕。」
   🔴 实测(像素扫描):左米色 50~72px、**右米色 0px**。
      根因是 `Row` 内 `layoutWeight(1)` 的子项默认 `min-width: auto`
      (按内容最小宽度)⇒ 长名字/多标签把**卡片布局盒撑破** ⇒ 右边越界贴屏幕
      (左边先被 margin 占位、所以还留着空隙)。
   ⇒ 弹性子项开 `flexShrink(1)`,姓名那行连同 `width('100%')`。

④「点击列表,不打电话了,容易误触。而是弹出一个卡片,卡片中是头像、名称、
   电话号码、邮箱等简要信息,下面三个按钮,详情、编辑和删除。……
   列表页面中,也不要详情按钮了。」
   ⇒ 新建 `FbPersonQuickSheet`(@ComponentV2):
      头像 / 姓名+称谓 / 电话(主号 + 其他)/ 邮箱 / 单位 / 生日
      + 三个等宽按钮「详情 / 编辑 / 删除」。
   ⇒ 点整行走 `openQuick()`(**不再拨号**);列表里的「详情」按钮**删除**。
   ⇒ 删除走 `AlertDialog` **二次确认**(服务端物理删除、无回收站 ⇒ 误触不可逆),
      文案带姓名("确定把「老杨」…")—— 误触时最需要"是不是点错了人"这个信息。
   ⚠️ 两级浮层分开两个状态(`quickPerson` / `detailPerson`);详情关掉**回到简要卡**,
      而不是一路退回列表。

⑤「这个卡片,弹出来之后,是白色的,和下面的列表中的白色,都融合在一起,
   根本看不出是弹框。」
   🔴 原来只靠 `#00000066` 遮罩区分 —— 而本应用**列表本身就是大面积白底**,
      白卡压在"被压暗的白"上边界仍发飘。
   ⇒ **三层分离**:① 遮罩(宿主)② 卡片**阴影**(抬起来)③ 极浅**描边**
      (同色背景上仍有一条可见界线;只靠阴影在浅底上不够稳)。
   ⚠️ 三个浮层(简要卡 / 详情 / 编辑)**同手段** —— 否则从简要卡切到编辑会突然"变平"。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 377/0(新增 28 条,
另修 3 条旧判据);_fb_v59c_falsify.py 16 项注入 ⇒ 16/0。

⚠️ 本轮反向验证又抓出 2 条假绿 + 1 条"没真注入",已逐条收紧(记入 pitfalls):
  · `flexShrink(1)` 在本页有**两处**(姓名 Text / 整个 Column)⇒ "出现过"型判据
    删掉关键那处仍绿;改成锚"相邻关系"(`.layoutWeight(1)` 之后 220 字符内);
  · `showAlertDialog` 被 `if (false)` 包起来(确认框永不弹)⇒ 字符串还在、判据仍绿;
    改成查因果(窗口内有 `value: '删除'` + `this.doDelete(who)`);
  · 有一条注入只改了注释、没删那行代码 ⇒ 报 [MISS],那是"注入没打到点上"。
2026-10-18 21:47:18 +08:00
laoyang 9bf21cd853 修「点头像没反应」:FbPersonSheet 塞进 bindSheet 会被立刻关掉(宿主契约冲突)
真机验证时抓到 —— 三条里第 ① 条第一轮**完全没用**:
点头像什么都不出来。每次点击日志都出现一组:
  AceSheet: radius is not correct type
  AceDrag: Drag gesture has been canceled     ← 浮层起来立刻被取消
⇒ sheet **确实创建了**,但被测量失败关掉;界面上看就是"点了没反应"。
(我第一轮据此误判成 "onClick 没接上",白查了一轮点击链路 —— 点击是好的。)

根因:FbPersonSheet 的根节点是
    Column() { /* 92% 宽的圆角卡 */ }
      .width('100%').height('100%').justifyContent(FlexAlign.Center)
—— "铺满全屏 + 内部居中",因为它的**设计宿主**是
"页面里叠一层遮罩 + Stack 居中"(名册页、日程页都这么用)。
而 bindSheet 要求内容**按自身高度自适应** ⇒
"内容要铺满 100% 高" vs "sheet 要按内容定高" 互相打架 ⇒ 判为无效、立刻关。

修法(最小侵入,老调用点一个字不改):
① 给 FbPersonSheet 加 `@Param embedded: boolean = false`:
     .width(this.embedded ? 'auto' : '100%')
     .height(this.embedded ? 'auto' : '100%')
     .justifyContent(this.embedded ? FlexAlign.Start : FlexAlign.Center)
     .padding(bottom 在 embedded 时去掉"贴底手势区留白"那 16)
   ⇒ 默认 false = 老行为;顶栏那处传 embedded: true。
② sheet 高度用 SheetSize.LARGE 而不是 FIT_CONTENT
   (卡片自带 maxHeight:72%,FIT_CONTENT 下会跟它抢高度)。
③ 删掉外层 myDetailPanel 上的 constraintSize({maxHeight:'82%'})
   —— 高度由 sheet 定,三方都设 = 三方打架。

真机复验(装机后立即点):
  浮层完整弹出且用的是**接口那份完整数据**(含本地没有的字段):
  「杨永全 · 副教授 · 家中排行第 2 个 · 生日 11-16 · 邮箱 i@yangyq.net ·
    地址 青岛市 · 身份证 372****4856 · 其它邮箱 yangyq@ouc.edu.cn ·
    网址 https://yangyq.net · 工作经历 中国海洋大学|副教授」+ [编辑][关闭]
  客观判据:uitest dumpLayout 的布局树字节数 119207 → 162736。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 352/0(再增 5 条钉住
embedded / SheetSize.LARGE / 不铺满 / 组件有开关);_fb_v59b_falsify.py 11/0。
另修一条旧断言(L 组"不再占满宽"原来锚死 `width('92%')` 字面量,
放宽为接受条件式,但没退化成"文件里有 92% 就行")。
2026-10-18 21:22:43 +08:00
laoyang f0f0d9047a 头像热区改用 40×40 容器撑(直接给 FbAvatar 加 width 会拉成椭圆)
自查发现的一个"顺手就踩"的坑:为了扩大点击热区(32 的头像手指容易点空),
本想给 `FbAvatar` 直接加 `.width(40).height(40)` ——
但 `FbAvatar` 的**根节点就是那个 `Image/Text`**(自己已设 `width(dia)`),
外层尺寸修饰会把它拉到 40×40,而圆角仍是 `dia/2 = 16`
⇒ **头像变成椭圆**,而且编译不报错。

⇒ 改成外面包一层 40×40 的 `Row`(居中),头像仍是正圆 32、热区是 40。

同时补一条守卫钉住它(`_fb_arkts_guard.js` 348/0):
断言"`Row{ FbAvatar{...} }` 之后跟着 `.width(40)`",并在 falsify 里加了
第 9 项注入(把容器撑宽改成直接给头像加 width)⇒ 反向验证 9/0。

验证:hvigor BUILD SUCCESSFUL;守卫 348/0;反向验证 9/0。
2026-10-18 21:00:53 +08:00
laoyang 2b0401052c v0.8.2 头像点进我的详情 / 未读数接真实数据 / 名册圆角卡(容器要换成页面底色)
用户 2026-10-18 夜三条:

①「右上角,显示的应该是登录者的头像,点击头像进入我的详情页面。」
   头像**本来就是登录者本人的**(v59 已接 session.me.avatarUrl);
   真正缺的是**点击** —— TopBar 里那个 FbAvatar **根本没有 onClick**,
   只有一句写着"点开账号菜单"的注释,而"账号菜单"这个东西在代码里并不存在。
   ⇒ 接 openMyDetail() → bindSheet 弹 FbPersonSheet(复用名册那个人详情组件)。
   🔴 数据必须走 repo.byId(meId)(本次为它新增 byId),不能从名册列表那批里找:
      **"我"常常不在自己的名册里**(名册是"我挑出来的那一小撮"),
      从列表里查会**静默落空**、点了没反应 —— 最难查的那种失败。
   ⚠️ 三种状态都说清楚:加载中 / 拿到了 / 拿不到,绝不留空白浮层。

②「提醒按钮,明明没提醒,怎么还显示个 2?」
   🔴 真根因:`@Trace unread: number = 2;` 是**写死的占位初值**,
      而全工程只有一处给它赋值(通知页「全部已读」清 0)。
      没进过通知页的人**永远看到 2**;进了通知页又"永远 0"(把真实未读也盖掉)
      —— 两个方向都错。
   ⇒ ① 初值改 0(拿到真值前必须是"没有未读",不能凭空多出 2);
     ② 首页把 /home 的真实 unread 写进全局状态。
        (服务端 Notification::unreadCount,与通知页同源;/home 本来就返回它、
          FbParse 也早就解析了 —— 只是**没人往全局状态里写**。)

③「名册页面,不好看,太丑了,应该是每个人都是圆角矩形,往中间收一点,
   距离左右都有点距离,每个人中间都有一点空隙。」
   🔴🔴 真凶**不是卡片形状**(v51 就做了 borderRadius + margin)——
      而是**列表容器也是纯白** ⇒ 白卡压白底 ⇒ 圆角与左右收边**全看不见**,
      看上去就是一整块通栏白。这解释了"改过一轮还是丑":上次改的是卡片,
      问题在"背景没有对比"。
   ⇒ 容器 + 外层兜底改 FbColors.paper(页面底色),卡片保持白;
     圆角 radiusS(10) → 新增 radiusCard(14)(列表项专用档:16 显胖、10 显方);
     左右各 pagePad(16)、卡间 10(原 8 偏挤)、行内边距 11→12。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 347/0(本批新增 18 条,
另**收紧 2 条**旧判据 —— M 组"圆角卡"那条原来锚"逐字相邻两行"、且**测错了对象**:
没查容器背景,所以对"白卡压白底"这个真实缺陷毫无把关能力);
_fb_v59b_falsify.py 8 项注入 ⇒ 8/0。

⚠️ 本轮反向验证头一轮漏了 3 条(都是"判据太宽"),已逐条收紧并记入 pitfalls:
  · 删掉头像 onClick 后仍绿 —— openMyDetail 这个标识符在**方法定义**里也出现;
  · 容器改回白底后仍绿 —— 本页有**两处** FbColors.paper(容器 / 兜底);
  · /home unread 改成写死 2 后仍绿 —— Notification::unreadCount 在 PHP 里也有两处。
2026-10-18 20:58:40 +08:00
laoyang 77b494a274 修「名册永远是空的」:person_roster.phones 只在迁移里加过、没进建表语句
用户 2026-10-18:「名册现在没有任何数据啊,看看咋回事。」

真机日志(唯一直接证据):
  E .../Config: table person_roster has no column named phones
  (SQLite errno 2,名册整批 batchInsert 全部失败)

根因(两条安装路径的行为分叉):
  `person_roster.phones` 当初**只加在 `FbDb.migrate()` 的 v3→v4 ALTER 里**,
  **没进 `FbSchema.FB_SCHEMA_SQL` 的建表语句**。于是:
    · 老库升级 → 走 migrate() → 执行 ALTER → 有 phones ✅
    · 全新安装 → store.version 一上来就等于 FB_DB_VERSION(5)
                 ⇒ migrate() 首行 `from >= FB_DB_VERSION` 直接 return
                 ⇒ 那句 ALTER **永远不执行** → 没有 phones ❌
  用户这次是**卸载重装**(前端流程要求),正好走的是坏的那条路。

为什么藏了这么久(两个"静默"叠加):
  ① `pageHeaderItem` 的空状态文案是「登录后会从服务器拉取你名册里的人」——
     一个**已经登录**的人被提示去登录,完全猜不到真实原因;
  ② `syncInBackground()` 里 `repo.sync()` 的**异常没人接**:
     异常从 async 函数冒出去,`bootstrap()` 之后没有任何 catch
     ⇒ 整个后台同步静默死掉,`syncNote` 还是 ''(看着像"同步成功")。

三处修复:
  ① `FB_SCHEMA_SQL` 的 person_roster 补上 `phones`(新装用户直接对);
  ② `FB_DB_VERSION` 5→6 + 新增"结构自愈"迁移:**主动探测列是否存在**
     (`PRAGMA table_info`,不靠异常控制流 —— 否则会把库损坏之类的真原因
      一起吞成"大概已经加过了"),缺则补 `ALTER`。这一条是给**已经装坏的库**
     (version=5 但缺列)用的:它们的版本号已经到位,不会再进任何 `from < N`
     分支,只能靠新版本号再捞一次。
  ③ `syncInBackground()` 包 try/catch,把异常落成**用户看得见的文案**
     (「名册写入本地库失败:<原因>」)。纪律:后台任务的异常必须就地
     变成一句话,不能指望"调用方会 catch"——调用方往往是 fire-and-forget。

真机验证(两条路径都跑了):
  · 旧库自愈:`本地库迁移:v5 → v6` → `v6 迁移完成:补上 person_roster.phones`
    → `[5] 本地库就绪,版本=6`;随后名册页**数据全出来了**
    (标签云 全部/大学同学 55/同事 40/实验室 23/合作伙伴/爸爸/家人;
      人员 爸爸/妈妈/岳父/岳母/杨永全 等),缺列报错 0 条。
  · 全新安装(清数据重来):`version=0` → 首装建表 22 条 → 直接 v6,
    迁移无事可做(正确:建表已含全部列)。

新增守卫(防这类"新装 vs 升级分叉"复发):
  `_fb_guard_schema_columns.js`(11 条)静态解析 `FB_SCHEMA_SQL` 的建表列
  与 `migrate()` 的全部 `ALTER ... ADD COLUMN`,断言**每个 ALTER 加过的列
  都必须在建表语句里**;另钉住 v6 存在性探测、版本号、后台同步 try/catch。
  `_fb_schema_falsify.py` 6 项注入 ⇒ 6/0(每项都成功打红对应断言)。
  已挂进流水线(run_guarded_suite 全量 b59b)。

顺带修 `_fb_v57_falsify.py` 的一处**过时注入锚点**:它注入的是
`allowSkip: this.app.lockReason === ''`,而该参数已按用户要求整个删除
⇒ 改成反方向注入「把 onSkip 后门加回来必须被抓」(12/0 通过)。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 328/0;b57 12/0;新守卫 11/0。
2026-10-18 20:40:30 +08:00
laoyang f020a671c0 v0.8.1 界面六条:删离线入口 / 标语统一 / 最近纪念日 / 图标真上色 / 真头像 / 日程四修
用户 2026-10-18 六条(全部已在真机逐条验证):

① 登录页「先不登录 / 离线使用」彻底删除
   用户:「把那些不登录、看离线数据之类的提示全部去掉。以后就不再拥有这样的
   功能了,把这些功能也全部删除。」
   ⇒ 两个按钮 + onSkip + allowSkip 参数 + Index 里的 skip 分支一起删净。
   这是产品决定(不是隐藏):该路径本身就是安全缺口,v58 上了整库加密后更要堵死。

② 标语统一为「一家人用岁月写成的一本书」
   用户:「这也是我们"家书"这个名字的由来。所以你不要再乱换这个标语了,
   只要是想出现标语或者副标题的地方,就用这样一句话。」
   · 端上 FB_DEFAULT_SLOGAN 是唯一定义处(登录页副标题 + 抽屉回落都用它);
   · 抽屉左上角原来是家庭名,现在显示**标语**;
   · 三层优先级(个人 slogan → 家庭 motto → 应用默认口号)**在服务端算**
     (Family::slogan,与网页端首页 heroSlogan 同一套),/login 与 /me 都返回。
   用户还说:「如果这个用户自己有标语……这个地方就是用户自己自定义的标语。」
   真机实测:登录者杨永全自己写了「修身,齐家」⇒ 抽屉就显示他那句。

③ 首页新增「最近纪念日」(放在近期任务之后、往年的今天之前)
   用户:「未来最近的三个纪念日,包括每个人的生日,以及手动填写的各个纪念日。
   以倒计时形式展示:距离哪个纪念日还有多少天,距离谁谁谁的生日还有多少天。」
   · /home.recent_days = Anniversary::upcoming(fam,pid,370) 取前 3 ——
     取满一年:窗口太小会让"刚过完生日"的家庭一个都取不到,模块直接空着;
   · 数据源 all() = 派生生日 + 自建纪念日,正好覆盖用户说的两样;
   · 倒计时:0 天显示「就是今天」(写「还有 0 天」读起来像已经过了)。

④ 悬浮栏图标也上色(用户:「除了下面的汉字,上面图标的颜色也变成跟汉字一样的颜色」)
   🔴🔴 真根因两层,都是"无报错失效":
   · 第一层:Image.fillColor() **只对 SVG 生效**,而本工程 30+ 图标**全是 PNG**
     ⇒ 全工程 24 处图标染色一直是空操作(文字走 fontColor 所以只有文字变了)。
   · 第二层(真机才现形):改用 colorFilter 后,矩阵写成了"系数乘 RGB"
     (R' = R_原 × 系数) —— 对白色图标成立,但本工程图标是**纯黑**,
     黑 × 任何系数 = 仍是黑 ⇒ 真机上图标像素依旧是 (0,0,0)。
   ⇒ 最终形态:`fbTint` 用 **alpha 当载体**(R' = (r/255) × A_原):
     系数是归一化约定(官方灰度示例系数和为 1,量纲确定);
     形状完全由原图 alpha 决定 ⇒ 黑/白图标都成立,描边抗锯齿自然过渡。
   真机像素实测:日程项图标与文字同为 (176,120,64)≈#B0763A;
   未选中项图标 (44,39,35) 深灰(不再是纯黑)。
   ⚠️ 全工程 24 处 fillColor 一并改掉;FbIconButton.tint 类型改成 string,
      让"误传 Resource"变成编译错误而不是又一个静默失效。

⑤ 顶栏:真实头像 + 拉开按钮间距
   用户:「右上角的头像应该是真实的……现在提醒的按钮距离头像的按钮太近了。」
   · /home 给 me_avatar、/login 给 avatar_url(avatar_url() 按线上存储驱动拼好);
   · 三颗按钮(同步 / 铃铛 / 头像)统一用 FbMetric.gapTopActions = 8。

⑥ 日程四处
   · 「日期下面有几个点」⇒ 月视图与周视图的圆点全部删除,
     连 FbDot / dots() / count / dotColor 一起清(不留死代码);
   · 「农历全部都是初一」⇒ 真根因是端上 lunarOf() **硬编码 return '初一'**,
     而服务端 lunarRange() 本来就每天给真实农历(title)⇒ 改取 title;
   · 「列表这个视图,今天的数据缺一条」⇒ 服务端 /calendar 的 from/to **默认只给本月**,
     而列表视图要显示 -7 ~ +60 天 ⇒ 端上显式传范围
     (loadModule 新增 pathOverride 参数);
   · 「点击了某一天之后并不明显,应该把这一天的颜色变深,而今天用一个较浅的颜色」
     ⇒ 照 SyncCalendar:选中 = 深色实底白字、今天 = 浅色底。
       真机实测:17 号(选中)bg=#FFB0763A,18 号(今天)bg=#FFF5F0EA。

另:FbKeyStore 补上一个真机抓到的 HUKS 坑 —— isKeyItemExist 在密钥不存在时
**会抛 12000011**(不是返回 false),首次运行必被误判成"本机不支持 HUKS"
⇒ 永久明文降级。改成把"不存在"当正常分支,且降级态每次启动重试一次
(把同一把 DEK 重新用 KEK 包起来,不换密钥以免重下全部附件)。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 328/0;
_fb_v59ui_falsify.py 19 项注入 23/0;_fb_home_api.py 60/0;
六条已在真机逐条取像素/布局树确认。
2026-10-18 20:14:40 +08:00
laoyang 2132ee42d2 v0.8.0 本地加密 / 被踢清数据 / 增量同步 / 附件队列 / 顶栏同步进度
用户 2026-10-18 两条:
「直接做 L3 锁界面 + 本地加密(HUKS,踢设备即吊销密钥):数据在但读不出,
  重登后恢复密钥即可用。」
「右上角头像和提醒前面加同步进度…首次全量、之后增量;进来先展示本地数据,
  一边同步一边更新;附件后台队列同步(名册头像 / 电影海报 / 观影照片 / 资源附件)。」

① 整库加密(FbSchema v4→v5,新增 module_sync / attach_cache 两张表)
   RDB encrypt=true。加密属性一经创建不可修改 ⇒ openWithEncryption 三段式:
   直接开 → 失败删库重建 → 再失败降级明文(degraded 字段,设置页如实显示,
   不静默降级)。

② 两层密钥(FbKeyStore)
   HUKS 里 KEK(fb_kek_v1,不可导出)包裹 32 字节 DEK;revoke() 删 KEK
   ⇒ 已落盘附件密文在数学上解不开,这就是"踢设备即吊销密钥"。
   重新登录重新生成 DEK 并重下附件(不做服务端托管,为了省一次重下
   把能解你本地数据的密钥放到服务端不值)。
   会话属性里 PURPOSE 只出现一次:原来把含 ENCRYPT|DECRYPT 的生成属性直接
   拿去开会话再 push 一个 PURPOSE,同一 tag 出现两次在 HUKS 里行为未定义
   ⇒ 拆成 kekBaseProps / kekGenProps / kekSessionProps。

③ AES-256-GCM(FbCrypto)
   落盘 iv(12)|ct|tag(16);变换串必须带 padding 段(AES256|GCM|PKCS7,
   缺了抛 801);解密从尾部切 tag。

④ 附件后台队列(FbAttachQueue)
   attach_cache 台账(pending/done/fail,MAX_TRIES=3);密文放 filesDir、
   明文临时放 cacheDir;扩展名白名单筛 key。
   图片地址收成统一入口 fbImageSrc(key,token):本地有就用 file://,
   没有才回网络 —— 收成一个函数才漏不掉(5 个页面全部改过来)。

⑤ 被踢 / 退出清数据
   lockOut → 清令牌 → FB_ATTACH.wipe(第一步就是吊销 KEK)→ FbDb.wipe
   (先关句柄再 deleteRdbStore,并发去重,open 会等它)→ 同步状态复位。
   重新登录后重建密钥 + 库 + 附件索引,否则之后只读网络不写缓存且不报错。

⑥ 增量同步(FbSyncAll 重写)
   先问 /api/v1/changes;指纹相同就跳过。本地没水位必须拉(否则老用户升级后
   一个模块都不同步);24h 陈旧上限兜"改无 updated_at 表的已有行";
   /changes 拿不到时判 ping status>0 —— 断网直接收工,不退回全量
   (否则 12 个模块各等 15 秒)。
   家谱也进了预同步列表(原来只在打开时拉,离线是一片空白);
   总步数由 CACHE_MODULES.length 算出,不写死。

⑦ 删除传播
   服务端是物理删除、没有墓碑 ⇒ 观影同步改成"先清后插"
   (persons 跨模块共用,不清)。

⑧ 顶栏同步进度
   头像与铃铛之间加三态按钮(转圈 / 红感叹号 / 灰对勾)+ bindSheet 面板:
   进度条、「N 个模块没有变化」、附件行、失败清单、立即同步(防连点)。
   状态中心 FbSyncState(@ObservedV2 + @Trace,AppStorageV2 单例);
   界面直接读 @Trace 字段,不提供派生 getter。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 290/0(本批新增 59 条);
_fb_v58_falsify.py 31 项注入 38/0;b57 反向验证 12/0(锚点跟上本批 logout 改动);
本地 e2e 1519/0;全量流水线 PUSH_RC=0。
2026-10-18 16:01:41 +08:00
laoyang 776c95ba0b v0.7.0 接上真实服务器:登录 / 设备会话 / 全量同步与各模块真实数据
从「演示数据壳子」走到「真的能连家里的服务器用」。

登录(用户 2026-10-17「完整的登录功能」)
  · 首次启动先填服务器地址 → /api/v1/ping 自报门户(认得出"这地址上真是家书")
  · 手机号或用户名 + 密码 → 拿令牌;失败提示统一,不泄露账号是否存在
  · FbDeviceStore:首次启动生成并持久化设备 UUID(三方应用拿不到 UDID),
    设备名取机型(marketName),并上报 platform / app_version

设备会话(v57,用户 2026-10-17)
  · 一把令牌 = 一台设备的一条会话;每次启动打一次 /me(有网就验)
  · 两种锁分开:need_login(401/被踢 → 清令牌)与 offline_expired
    (离线超 24h → 只锁界面、不清数据、联网自动恢复)——
    混成一种就会违背用户要的"一直联网就不用每天输密码"
  · 运行期任何请求 401 → 网络层全局回调立刻锁定跳登录页(FbHttp 的 authProbe
    让登录/校验类请求跳过,避免"两个地方同时跳登录页")
  · 退出登录:先清本地令牌(主防线),再尽力通知服务端
  · 设置页显示版本号与「这台设备」

数据层
  · FbDb / FbSchema / FbParse / 各 Repo(日程 / 名册 / 观影 / 模块)+ FbSyncAll
  · 首页改为服务端 /home(农历/节气/纪念日/生日/天气/统计/往年的今天)
  · 日程页用 FbDayEventPanel,与网页端显示方式/红线/并排三处一致

验证:ArkTS 静态守卫 231/0;hvigor 编译 BUILD SUCCESSFUL(entry-default-signed.hap)。
2026-10-18 03:36:04 +08:00
laoyang 7eaa641437 v0.6.0 图标改为开抽屉入口 + 新增底部悬浮快捷栏(照鸿蒙 6 悬浮导航规范)
用户 2026-10-12 三条要求。

## ① 点顶栏图标 = 开抽屉
用户:「把那个图标改成这个功能吧,就是当用户点击左上角那个图标的时候,把抽屉列出来」。
⇒ `TopBar` 的 logo 加 `.onClick { drawerOpen = true }`。
⚠️ 点击直接挂在 Image 上、**不套 Button** —— Button 自带最小尺寸与内边距,
会把图标挤离左边线(用户要"图标靠左")。
⇒ 开抽屉现有三个入口:点图标 / 应用内水平滑动 / 底部悬浮栏。

## ② 底部悬浮快捷栏 FbFloatDock(新增)
用户:「页面正下方设置几个浮动按钮…比如日程、名册、任务和笔记…
        这样用户不用拉抽屉,就可以直接点击使用。
        但一定要注意,使用浮动的方式,**不要固定在下面**,而是**浮动着的**。」

🔴 **形态照鸿蒙 6 官方设计规范的「悬浮导航(Float Navigation)」**:
「导航栏脱离底部边界,以**四周留白、圆角卡片**形态悬浮于内容之上」。
参考实现参数直接用上:`height(56) + margin({left:16,right:16,bottom:16})
+ borderRadius(28)`,材质用系统沉浸材质
`uiMaterial.ImmersiveMaterial({ style: ULTRA_THIN })`(设计规范的"超薄"档)。
⚠️ 用了 systemMaterial 就不再叠加 backgroundBlurStyle/backgroundColor ——
   材质自带模糊+边框+阴影,叠加会双重模糊发灰。

⚠️ **摆法关键**:`Index` 的根容器从 `Column` 改成
`Stack({ alignContent: Bottom })` —— 写在 Column 里它就是"占一行的固定底栏"
(老样式);用 Stack 覆盖 ⇒ 内容从它下面穿过、它浮在上面。
(实测印证:栏体 y=2132..2307,而"我的家人"标题在 y=2336 仍可见 ⇒ 内容穿过去了。)

⚠️ 内容底部加 `FB_DOCK_RESERVE`(88vp) 预留,否则最后一屏内容会被悬浮栏压住。
12 个页面统一处理。
⚠️ 宽屏不显示(抽屉常驻,且屏宽用不上);抽屉展开时也不显示(避免浮层叠浮层)。
⚠️ 选中项用模块主题色高亮,用户一眼知道"我在哪"。

## 验证(真机,全部通过)
· 点顶栏图标 ⇒ 抽屉打开 ✓
· 点悬浮栏「任务」⇒ 切到任务页 ✓;点「日程」⇒ 切到日程页 ✓
· 悬浮栏几何:栏体 x=21..1060、y=2132..2307(高 58vp),
  栏外 x=0..20 为纸色 ⇒ **四周留白** 符合规范 ✓
· 栏内呈半透材质色(#EFEBE2) ⇒ `ImmersiveMaterial` 生效 ✓
· 14 页遍历 **14/14**;深验 **12/12**;编译 **0 error / 0 warning**

## 过程中的一个坑
给 12 个页面补 `FB_DOCK_RESERVE` 的 import 时,脚本统一写成
`'../components/FloatDock'` —— 但**页面在 `pages/<模块>/` 下,需要 `../../`**
(`Index.ets` 在 `pages/` 下才是 `../`)。批量改 import 路径要按**文件深度**区分。
2026-10-12 12:06:00 +08:00
laoyang 705a65ae2e v0.5.0 抽屉改「应用内左右滑动」触发 + 去掉汉堡按钮 + 统一区块边距
用户 2026-10-12 三条反馈。

## ① 抽屉改「应用内水平滑动」触发(关键更正)

用户实测指出:「左滑右滑不是从边上滑,而是在应用的内部从左往右滑或者从右往左滑。
如果从边上往里边滑的话,它是系统手势,我们是没有办法去控制它的。」

⇒ **这是之前手势完全没生效的根因**:屏幕边缘是系统手势区(返回/侧边栏),
  从边缘往里滑的事件根本轮不到应用。判据改为「在应用内部水平滑动」:
  · 向右滑 ⇒ 抽屉从左边出;· 向左滑 ⇒ 抽屉从右边出。
  (左手持机往右划、右手持机往左划,单手都顺。)

🔴 同时修掉一个真 bug:**`offsetX` 的单位是 vp**(SDK 原文 "in vp"),
   我之前又除以 density,把位移缩到 1/3 ⇒ 阈值永远达不到。
⚠️ 用 `parallelGesture`(不是 gesture):内容区还有竖向滚动与
   纪念日/财务/任务的横向滚动条,用 gesture 会把它们抢掉。
   阈值 80vp,轻扫横向列表不会误开。

## ② 去掉顶栏汉堡按钮
用户:「那个三个点、三个横线的按钮就不要了,图标靠左,显得更好看一些」。
⇒ TopBar 删掉按钮,`padding.left` 由 14 改为 `pagePad`,图标与下方内容左边线对齐。

## ③ 🔴 统一区块边距(修掉 15 处真 bug)
用户:「页面里的方框大小不一样,有的左右充满,有的有缝隙 —— 最好都留一点距离」。

实测界面树 + 像素采样找到根因:**卡片同时写了 `.width('100%')` 和
`.margin({left, right})` ⇒ width 先把宽度占满、margin 失效,卡片顶到屏幕两侧**。
(首页个人信息卡实测 x=0..1080 满宽,而别的区块是留边 17vp 的。)

全项目扫出 **15 处**同款写法(calendar 3 / days 2 / ledger 2 / movies 1 /
people 2 / settings 5),**全是卡片**(backgroundColor+borderRadius+margin)。
统一删掉那条 `.width('100%')`,宽度交给 margin 撑出。

### 验证(真机)
· 手势:向右滑 ⇒ 抽屉「家书」x=187(贴左);向左滑 ⇒ x=442(贴右)—— 双向 ✓
· 边距:像素级逐行采样,17 行里 15 行左右均 50px(17vp)完全一致;
  另 2 行是"标题文字起点"(文字有额外缩进),不是边距问题 ✓
· 14 页遍历 **14/14**;深验(每页特征文本)**12/12**
· 编译 **0 error / 0 warning**

### 工具链修复(都很关键)
· 🔴 `hdc file recv` 在 **Git Bash** 下会被路径转换改写:`/data/local/tmp/x`
  被当成 `C:/.../tmp/x` ⇒ **静默失败、界面树永远抓不到**。
  三个验证脚本已统一注入 `MSYS_NO_PATHCONV=1`。
· 探针不再点汉堡按钮开抽屉(按钮已删),改用「应用内向右滑」。
· 修探针一处误判:顶栏与抽屉**都有**「家书」,原来没区分 ⇒
  两种滑动都读到顶栏那个固定 x,误报"手势没生效"(其实生效了)。
2026-10-12 11:37:45 +08:00
laoyang 0816e25988 v0.4.0 顶栏改白色 + 通知栏同色 + 顶栏随页面滚动
用户 08:56 的三条要求:
  1. 家书标题栏用浅色(下面色块的背景色);
  2. 上面通知栏也是浅色,两者融为一体;
  3. 向上滑动时标题栏跟着向上滑,不固定在原位。
(追问确认:顶栏底色 = 白;「品牌条 + 页面标题条」一起随滚动消失。)

## ① 系统栏改「不透明 + 与顶栏同色」,弃用透明方案
原来走「系统栏全透明 + 让紧邻元素透出同底色」。这版顶栏要用白色,
若仍走透明方案就得再造一层"白色压条"去刷白状态栏,太绕。
⇒ 直接 `statusBarColor = surface`(白)、`navigationBarColor = page_bg`(纸色)。
这是**配置出来的同色**,比"指望紧邻元素恰好同色"稳得多(后者顶栏一改底色就出色带)。
副产品:不再需要 `setWindowBackgroundColor` ⇒ 连那条 unavailable 告警也消失,
编译回到 **0 error / 0 warning**。

## ② 顶栏随滚动:整页包进 Scroll,顶栏作为内容第一个元素
原来 `Column { 页头; List/Grid(layoutWeight 1) }` —— 顶栏在滚动区**外面**,
所以固定不动。改为 `Scroll { Column { this.topBar(); 页头; 列表 } }`。
顶栏由 Index 以 `@BuilderParam topBar` 插槽下发(`topBarSlot`),13 个页面全部接入。

⚠️ 同步必须处理的两点:
· 列表的 `.layoutWeight(1)` 要删 —— Scroll>Column 里高度由内容决定,
  layoutWeight 会去分"剩余空间"(=0) ⇒ 列表塌成 0 高、整页只剩顶栏和页头;
· List/Grid 不能自己再滚动(去 scrollBar/edgeEffect)—— 两层滚动容器会抢手势。
⚠️ ArkUI 限制:带尾随 lambda 的组件只能有 1 个 `@BuilderParam`
(首页原有闲置的 topActions 已删)。

## ③ 顶栏白色 + logo 可辨认
logo 是「米色底(74%) + 砖红图案(24%)」的实心方图,放在米色上底板隐形、
只剩图案发飘(用户说"看不清")。今加一层极浅描边把底板"托"出来。
顶栏高度 56→48vp。

## 验证(真机,全部通过)
· 沉浸式:状态栏色 `#FFFFFF` == 顶栏色 `#FFFFFF` ✓
· 顶栏随滚动:「家书」标题 y=140 → 滑动后**已滚出可视区** ✓
· 顶栏滚走后状态栏仍为 `#FFFFFF`(未露出页面内容)✓
· 14 个页面遍历 **14/14**;深验(每页特征文本)**12/12**
· 编译 **0 error / 0 warning**

## 过程记录:批量改 12 个文件失败两次(已回滚重做)
第 1 版按缩进正则切分 ⇒ 闭合括号错乱;第 2 版跳字符时**没消费 Column 的
修饰符链** ⇒ 吃掉 `@Builder` 声明、**636 个报错**。
第 3 版改为**按行 + 括号深度解析**,且**改完立刻做结构断言**
(括号配平、`this.topBar()` 恰好 +1),不满足就不写入;先单页试跑再推广。
教训:批量改多文件时正则不可靠,必须配结构自检 + 单页试点。

配套修掉 `_tooling/familybook_harmonyos_run.sh` 一个"假成功":
`hdc list targets` 无设备时输出字面量 `[Empty]`,原来"非空即通过"
⇒ 拿 `[Empty]` 当序列号去装、还打印「✓ 设备:[Empty]」。已显式排除并 die。
2026-10-12 10:06:55 +08:00
laoyang d6bda758d8 v0.3.0 沉浸式按 SyncCalendar 重做(通知栏与页面完全同色)+ 个人信息卡改白块
## ① 沉浸式:改用本机 SyncCalendar 的方案(用户指定参考)

用户原话:「上面那一块的颜色和通知栏的颜色完全融为一体;
紧接着就是应用图标和标题,尽量靠上」。

三件套(缺一不可):
1. 系统栏涂**全透明** `#00000000`;
2. `setWindowBackgroundColor(页面底色)` 供透明系统栏透出;
3. **紧邻系统栏的元素不设自己的背景色** + 根容器 `expandSafeArea`。

🔴 第三件是最关键、也是我先前漏掉的一环:
我原来给顶栏设了白色 `backgroundColor`,于是状态栏(透出纸色)与顶栏(白)
仍上下拼接。读 SyncCalendar 源码发现它的 `header()` **根本没有 backgroundColor**
—— 这才是"融为一体"的真正机制。已在代码里写明原因,防止回退。

配套简化:删掉自己算安全区的那套(`statusBarH`/`bottomBarH`/
`loadWindowInsets`/`applyAvoidAreas`,共约 110 行)—— 不再手算,也就不会算错。

## ② 顶栏改「图标 + 标题」

按用户要求:应用图标 + 「家书」标题(下行小字显示当前模块)贴左上,
右侧铃铛与头像。顶栏高度 56 → 48vp,上下内边距压到 4,内容整体上移。

## ③ 个人信息卡恢复为白色卡片

用户明确要求("还是得用一个白色的块,这样显得好看")。
曾一度做成"不套卡片的头部"(理由是省首屏高度),现按用户观感改回白卡。
⚠️ 与顶栏的区别:顶栏**不能**有背景色(否则与状态栏割裂),
而这张卡在顶栏下方、与状态栏不相邻,套白卡没有这个问题。

## 验证(全部基于真机)

逐行像素采样(`_tooling/familybook_immersion_check.py`,
模型读不了图 ⇒ 用数字裁定,不靠目测):
· `y=0..271` 全为 `#F7F3EA` ⇒ **状态栏与顶栏同色,融为一体** ✓
· 底部 `y=2224..2444` 为纸色,无异常条 ✓

真机界面遍历(`_tooling/familybook_harmonyos_probe.py`):
· A 组抽屉 11 个模块 **11/11**
· B 组家谱 **1/1**;C 组通知/设置 **2/2**
· D 组深验(特征文本)**12/12**
合计 **14 个页面全通过**。

⚠️ 顺带修掉探针自身两个坑(都记进注释了):
· `nodes_of` 原来只收集**有文字**的节点 ⇒ 找不到纯图标的汉堡按钮,
  导致"抽屉打不开"、10 个模块连锁失败(看着像应用坏了,其实是探针瞎了);
· 汉堡按钮坐标写死 ⇒ 顶栏变矮后点空。改为在界面树里动态探测、
  并**排除系统状态栏区域**(那里也有可点击元素,曾误取到 (103,56))。

## 编译
0 error(保留 1 条 `setWindowBackgroundColor` 的 unavailable 告警,它是透明方案的必需组成)
2026-10-12 08:43:32 +08:00
laoyang 0548a45da1 v0.2.0 沉浸式 + 按手机重排版 + 去掉导航模块
三条用户反馈一次处理完。

## ① 不够沉浸:首页距顶太远 + 底部白条

根因是**重复避让 + 颜色断裂**,两处都是我自己的错:
· 早先没开全屏时,系统已把窗口推到状态栏下方,我却又加了一个等高的白色
  Column「让出安全区」⇒ 重复避让,实测顶部空出 156px;
· 状态栏底色被设成「页面底色(宣纸米)」,而顶栏是白色 ⇒ 顶部出现
  「米色状态栏 + 白色顶栏」的拼接感,看着就是"没对齐"。

改法:
· EntryAbility 调 setWindowLayoutFullScreen(true),窗口铺满整屏;
· 状态栏底色一律取**顶栏底色**,底部手势条取**页面底色** —— 各自与紧邻元素同色;
· 页面侧用 getWindowAvoidArea 自己让位(顶栏顶部内边距 = 状态栏高度,
  底色一直画到屏幕最上沿);
· 底部那条全宽白 Column 删掉。

避让值走**异步** window.getLastWindow + windowSizeChange 监听
(早先"Ability 取窗存 AppStorage 再同步读"的做法实测读不到值,恒为 0);
旋转/折叠后会自动重算。

验证(像素级,非目测):y=0..200 全为顶栏白(原先 y=0..121 是米色状态条),
底部 y=2224..2444 全为纸色、白条消失。

## ② 去掉「导航」模块

用户原话:「导航是专门为电脑端开发的,都是网站,主要用在浏览器的主页,所以不需要」。
⇒ 删掉 FbNavPage、FbNavLink 类型、FB_DEMO_NAV、fbGroupNav、图标分支与演示数据。
⚠️ 模块清单自此与 Web 端 Module::LIST **有意不等**,注释里写明了别补回去。

## ③ 排版按手机重做(不照搬电脑端)

首页:删掉 hero 招牌整块(它只重复信息面板已有的信息),信息面板升为首块
并改成**无卡片的页面头部**(问候/日期大字、天气右上、三数字贴底)——
套白卡片会在首屏就切出一块白、压缩可用高度,也破坏沉浸感。
卡片从「今天的日程」才开始用,页面层次反而更清楚。

其余按手机重排:观影两列定格(原瀑布流)、任务按状态三段(原多列看板)、
家谱圈层(原折线树)。README 里列了逐项取舍与理由。

## 编译
hvigorw assembleHap --mode module -p product=default --no-daemon
→ BUILD SUCCESSFUL,0 error / 0 warning
2026-10-11 23:20:49 +08:00
laoyang 41e3aa3d95 v0.1.0 界面框架:抽屉骨架 + 13 个模块页面
对应服务端 FamilyBook(家书)的鸿蒙客户端首版。本轮只做**界面框架**,
数据全部是演示数据 —— 服务端尚无客户端接口,所有增删改只做入口与样式,
不落任何数据。

## 骨架
左侧抽屉 + 右侧内容。抽屉两套形态由窗口短边决定(≥600vp 为宽屏):
宽屏常驻侧栏、窄屏滑出层。判据取「短边」而非「宽度」,因为短边在旋转时
不变,比例接近 1:1 的设备横竖屏布局天然一致。

## 模块(与 Web 端 Module::LIST 一一对应,key 即路由名)
首页、家谱、名册、观影、旅程、日程、资源、任务、纪念日、财务、读书、
导航、笔记;另有通知消息、个人设置两个附加页。

## 与 Web 端保持一致的几处
· 模块清单唯一来源在 model/Modules.ets,加模块只改那一处;
· 模块主题色三档由主色按 HSL 压明度算出,预设色板与 Web 端逐值一致;
· 术语沿用 Web 端固定下来的口径(旅程 ⊃ 行程;观影的「影院」不单独占
  抽屉位,从模块内设置进);
· 页面结构照 Web 端的信息层次(首页信息面板 / 财务概览在账目之前 /
  纪念日「即将到来」在顶 / 任务按状态而非按看板)。

## 编译验证
hvigorw assembleHap --mode module -p product=default
→ BUILD SUCCESSFUL,0 error / 0 warning(含一次删 build 后的干净重建)

过程中修掉的编译期问题:
· Circle().fill() 是 API 26 才有的重载(compatibleSdk 是 24)
  → 换「方块 + 圆角」画圆,零版本风险;
· setWindowBackgroundColor 对 phone/tablet 不可用 → 去掉(页面已铺满同色);
· window.getLastWindow() 只有异步版 → 由 Ability 侧同步取窗存 AppStorage;
· 颜色类 @Param 声明成 string 导致传 Resource 报错 → 统一改 ResourceColor;
· 页面在 pages/<模块>/ 下,公共件 import 相对路径要多退一级。

(首轮编译 95 error / 18 warn,逐条定位到 5 类根因后归零。)
2026-10-11 22:07:48 +08:00