Commit Graph
130 Commits
Author SHA1 Message Date
laoyang cca83a826c 补齐工单号:上一条 7d7973a 的 #92 是笔误,实际对应 #4 (#4) 2026-11-06 12:41:13 +08:00
laoyang 7d7973a319 名册编辑:表单取消分组、网址补常驻输入框、地址合一(#92)
用户 2026-11-06 的要求:
1. 「联系方式」这一句(分组标题)去掉;
2. 网址要跟邮箱一样有标签、有常驻空输入框;
3. 地址的格式跟邮箱/手机号一致:国家、省市、区县、详细地址
   都放进同一个输入框的 placeholder 里;
4. 「工作」「其它」两个分组标题也去掉,直接写单位和职务、直接写座右铭;
5. 不需要分多级,就是一层。

改动:
- 删掉全部 4 处 this.section() 调用与 section() 构建器(表单只剩一层);
- 新增 urlField():常驻「网址」标签 + 空输入框;服务端无「主网址」列,
  取 person_contacts kind=url 的第一条当主,提交时由 urlsOut() 拼回组首;
- 地址五级合成一个 addressField(),层级写进 placeholder;
  没改地址时原样回传旧的 address 列 + 旧五级,改了才整串写入并清空五级。

同时修掉验证时抓到的**数据损坏**(不是假想,已在真机复现):
从名册列表点「编辑」时传进来的是本地列表行,列表行缺
birth_rank / 补充联系方式 / 工作经历 / 完整地址,而服务端
replaceContacts / PersonJob::replace 是整组替换语义
⇒ 用户随手保存一次就会把这些字段清空(实测 birth_rank 2 → null)。
修法:编辑模式无条件补拉一次详情,逐字段/逐组只覆盖「用户没动过」的部分;
save() 先 await 补拉任务,未拿到权威值时对没动过的字段一律不提交。

附带:
- addressField()/urlField() 用内部直读字段的写法(@Builder 值参数不参与
  状态观察,异步回填刷不出来 —— 实测停在中途值);
- 保存时 contacts 增加「原本有条目、现在删空」与「动过任意一组」两个
  提交条件,否则删掉仅有的那条网址永远删不掉。
2026-11-06 12:40:04 +08:00
laoyang d1f27937b9 名册编辑表单:重建视觉层级 + 删冗余文案 + 删除按钮移至滚动底部 (#91)
用户:编辑名册资料这里,全部字段都能改。点完改完点保存,这句话不要,
"基本"这两个字也不要。性别、颜色样式不对;联系方式不对;地址不对;
工作不对;其他不对;标签不对;备注不对;我的备注也不对。
还有,删除按钮还是不要放在这里,也容易误触。还是跟之前的惯例一样,
放在最下面,滚动条最下面。

关键判断:用户列的 8 个"不对"**不是内容问题,是同一个视觉层级缺陷**。
逐项走查确认:
- 分组标题(联系方式/地址/工作/其它)是 fontTiny + faint(淡灰小字),
  **比字段标签还弱**;而 field() 早已是 fontSmall + Bold + 主题色;
- area()(备注/我的备注)、tagPicker()、genderPick()、省市标签全是 fontTiny + faint;
- 内层输入框(联系方式行/工作经历行)字号也比主表单小一号。
⇒ 淡灰分组标题压在主题色加粗字段标签下面,分组层级被压扁,
  整张表看着是一堆同级小字糊在一起。

① 删两处文案:
   - 「全部字段都能改,改完点保存」整行删(编辑模式)。
     ⚠️ 新建模式那句"填多少算多少,之后都能再改"保留 —— 它说明可以不全填,
        有信息量;编辑那句只是重复"点保存",是废话;
   - 「基本」删除。它是表格第一个分组,上面没内容需要区分。
     其余分组标题保留(确实在分隔字段集合)。

② 重建三层级(核心):
   分组标题(fontBody+Bold+主题色+左侧竖条) > 字段标签(fontSmall+Bold+主题色)
   > 输入值(fontSmall+正文墨色)。section() 重写,area/tagPicker/genderPick/
   省市标签全部对齐 field();内层输入框 fontTiny → fontSmall。

③「编辑资料」标题改主题色(原 FbColors.ink 与普通文字无区别)。
   新建的"添加成员"一起改 —— 同一类标题不能"新建黑、编辑紫"。

④ 删除按钮从底部固定条移到**滚动内容最末尾**(前面加"危险操作"小标题):
   - 要删必须先滑到底,天然隔开误触;
   - 不再与"保存"并排抢注意力;
   - 放在 err 提示之后(报错要在删除按钮上面);
   - 仍红底白字、整宽(用户 2026-10-19 定的"删除全部用红色背景")。
   底部固定条只剩「取消」「保存」两颗,等宽平分。

验证(真机 5NC0225B27004718):
- 含「全部字段都能改」= False、含「基本」= False;
- 颜色(最深5%像素均值):编辑资料(69,115,116)、我对他的称呼(69,115,116)、
  性别(70,117,119)、家中排行(68,116,117)、工作分组(73,118,120)、
  单位字段(71,117,119)、其它分组(68,117,118) —— 均为主题色 #4A7C82;
  姓名(168,74,54) 为必填红,符合规范;
- 滚到底:「危险操作」y=1761、「删除这个人」y=1820(滚动区最末);
- **滚回顶部时「删除这个人」不可见**(必须滑到底才出现);
- 底部固定条:取消[105-524]、保存[555-975],等宽平分;
- 像素结构:y1826~1934 整宽红块(删除),y2042~2150 两颗按钮。
hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 12:04:55 +08:00
laoyang 19e1333958 首页联系人:点编辑就地弹框,不再跳名册 (#90)
用户:现在在首页上,弹出名册信息之后,点击编辑,怎么跳转到名册去了?
点击编辑应该直接弹框编辑。

改前 onEdit = closePersonDetail() + switchModule(people) —— 用户被甩到
名册模块,还得自己重新找到那个人再点编辑,绕了一圈。
当时注释的理由"首页没有人员编辑表单"站不住:首页**已经**在用
FbPersonEditSheet 做新增成员,同一个组件换参数就是编辑模式。

① onEdit 改为就地弹表单(同一个 FbPersonEditSheet,不另写一份,
   否则两处字段/校验/保存逻辑必分叉);
② 新增 @Local pfEditing: FbRosterPerson | null —— 用对象不用 boolean
   (表单要拿它回填,只存 true/false 还得再查一次库)。
   与 pfCreating 分开两个状态:新建时 person = null 是合法值,
   合并成一个 boolean + person? 会让两种情形撞车;
③ 取消 ⇒ 回详情卡(不是直接回首页)。用户刚才是从详情进来的,
   取消只是"我不改了",不该连"我在看谁"一起丢掉(与名册页同口径);
④ 保存 ⇒ 关表单 + reloadSideCards + refreshPersonDetail(id)。
   这里有个必须处理的坑:编辑表单 save() **只调接口、不写回本地库**,
   直接把编辑前快照塞回 pfDetail 会让用户看到"改完还是旧名字"。
   ⇒ 新增 refreshPersonDetail(),两级刷新(与 FbPeoplePage.openDetail 同一思路):
     ① byId() 读本地库(快,可能是旧值)
     ② fetchDetail() 打 GET /people/<id>(服务端权威值,回来原地替换)
     断网保持①、不弹错;读不到就什么都不做,不拿 null 覆盖正常的详情卡。

验证(真机 5NC0225B27004718):
- 首页点爸爸→详情卡→点编辑 → **就地弹出编辑表单**,页面仍是首页
  (下方最近观影/联系人/往年的今天都在);
- 表单标题「编辑资料」、姓名回填「杨培振」、生日(阳历)1955-03-30;
- 点取消 → 回到详情卡(含手机号/拨打/编辑),不是直接回首页。
hvigorw assembleHap BUILD SUCCESSFUL。

⚠️ 刻意没执行"保存":这是用户真实联系人数据;保存路径与名册页完全
   同一份代码(同组件同 onSaved),无需在首页重复踩。refreshPersonDetail
   的两级刷新未走真机保存验证 —— 若日后发现"保存后详情显示旧值"先查这里。
2026-11-06 11:56:05 +08:00
laoyang 8f38cfa98b 联系人详情卡:点手机号直接拨号 (#89)
用户:点开以后的弹窗,点击手机号可以直接拨打电话。

事实核查:详情卡一直没有这个能力。手机号是一行纯 Text
(this.infoRow("phone", ...) 无 onClick)。而 FbPersonSheet 文件头
注释里写着它"自带手机号拨号入口"—— 那句是错的,实测点了没反应。

对照:名册的简要卡(FbPersonQuickSheet)v59e 就做了可点即拨,
详情卡一直缺 —— 同一份数据两处体验不一致。

① FbPersonSheet 加 @Event onCall,默认实现为空
   (TopBar.myDetailPanel 等宿主没接时仍可点、只是不拨,不崩不误触发)。
   卡片**不自己拨号**:call.makeCall 需要 UIAbilityContext,
   组件自己拿容易在浮层销毁瞬间拿到 undefined。

② 手机号那一行改成 phoneRow(不复用 infoRow)
   纯展示行加 onClick 会出现"整行看着一样、只有手机号能点",
   用户不知道点哪。新行给明确交互外观:整行浅主题底 + 号码主题色加粗
   + 右侧「拨打」胶囊,与 FbPersonQuickSheet.phoneRow 同一套语言。
   补充电话(逗号拼接的多个号)仍是普通展示行 —— 没法一次拨、也不该猜拨哪个。

③ 两个宿主接上:FbHomePage 新增 dial()、FbPeoplePage 复用已有 dial()。
   实现刻意与名册页一致(不另写一套),并抄上关键约束:
   必须 call.makeCall 而非 startAbility(tel:) —— 后者是隐式 Intent 会弹
   「选择打开方式」(2026-10-15 真机误点进"同步日历");上下文必须是
   UIAbilityContext;call.makeCall 不需要权限、只填号不代拨。

验证(真机 5NC0225B27004718,详情卡「爸爸/杨培振」):
- UI 树:手机号[208,772]、号码 17706392406[208,818]、拨打胶囊[837,795];
- 点号码文字(342,842) → 拉起拨号盘 177 0639 2406 / 山东青岛 电信 / 老爸;
- 点「拨打」胶囊(893,819) → 同样拉到该号码;
- 全程**没有**「选择打开方式」框,证明走的是 call.makeCall 而非 tel: 隐式 Intent;
- 返回 App 后详情卡状态保持。
hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 11:47:47 +08:00
laoyang 6a5d351e79 首页联系人:修头像显示 + 排除自己 (#88)
用户:首页联系人,需要带头像,排除自己。

① 头像(真 bug:从来没显示出来过)
   改前 Image(p.avatar) —— 而 p.avatar 存的是**存储键**(avatars/xxx.jpg),
   不是能喂给 Image() 的地址。Image("avatars/xxx.jpg") 被当成相对沙箱路径,
   静默失败 ⇒ 永远只剩首字色块,看起来像"这个功能没做"。
   正确出口项目里早就有:fbImageSrc(key, token)(先查本地解密缓存,
   没缓存才拼 media.php?k=&token=)。名册页/观影卡都这么写,唯独首页这处漏了。

   ⇒ 改用 FbAvatar({ name, color, dia, imageUrl }):
     · 底色+首字常驻,图片成功才盖上去 —— 名册绝大多数人没头像,必须回落;
     · 有 key 但图坏了(OSS 文件缺失/token 过期)时 Image 是纯空白,
       FbAvatar 会露出下面的首字,不会变成一块空光板。
   personInitial() 因此成了死代码,删掉(FbAvatar 内部按 name 取首字)。

② 排除自己
   FbPersonRepo.list() 返回整个名册(含"我"),自己占掉一个最显眼的位置
   (一屏只露 6 个,3 列 × 2 行)。
   - 用 session.me.id 判,不用名字/昵称比(重名、改名会漏);
   - 取不到 id(0)时不剔任何人 —— 宁可多显示一个,
     也不要因空 id 把列表清空(本项目踩过判断写反 ⇒ 列表恒空的坑);
   - pfTotal 用剔除后的数量:它渲染成标题「N 人」,
     用原始总数会写 800 人却只列 799 个,看着像丢人。

验证(真机 5NC0225B27004718):
- 标题 800 人 → 799 人;6 格从「爸爸/妈妈/岳父/杨永全/红围脖/轻舟」
  变成「爸爸/妈妈/岳父/岳母/红围脖/轻舟」,即自己消失、后面依次前移;
- 底部说明同步为「共 799 人」;
- 6 个头像逐格取样(圆心 ±25px):色数 223~2162、主色占比 0.00~0.40,
  全部是多色真实图像(纯色首字头会是主色占比>0.75、色数<50)。
hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 11:40:26 +08:00
laoyang f45f4506b0 票根并入观影照片:端上不再「票根另算」(#87)
用户:现在不是已经没有票根的概念了吗?全部都换成观影照片了。
现在只分两类:一类是海报,一类是观影照片。

数据层已迁移(服务端 #87):220 条 ticket_key 搬进 event_photos,
旧字段置 NULL,Movie::find 也不再返回它。

端上跟着收:
- allPhotos() 不再把 ticketKey 拼进列表,直接 return d.photos.slice();
  原来那段还特意给它 id=0("删不掉"的标记),现在没这个特殊项了;
- FbMovieRepo 不再写/解析 ticket_key(本地列留着不改 schema,
  但恒为空串,字段标 @deprecated);
- FbSyncAll 收附件时 SQL 去掉 ticket_key(票根的 key 会在
  module_cache.payload 递归扫描里被收进来,不丢附件)。

⚠️ 特别注意 allPhotos 里**不留** ticketKey 兜底分支:留着就等于承认
   还有第三类,而那正是用户要消灭的东西。若线上还有残留(迁移漏跑),
   症状是"某些老记录照片变少",应该去补跑迁移,不是在这里兜。

hvigorw assembleHap BUILD SUCCESSFUL。真机验证待做(当时手机锁屏)。
2026-11-06 11:25:56 +08:00
laoyang 47bba2646e 观影照片支持单张删除:编辑表单加 ✕ (#86)
用户:编辑观影记录的时候,那个图片应该可以被删除吧?现在没有删除按钮。

服务端早就有 Movie::deletePhoto()(连存储对象一起删,含视频 thumb_key),
网页端 movies/photo_del 也在用 —— 缺的只是 API 那一层暴露。

服务端:
- photos 子路由从"只 POST"扩为"POST 追加 + DELETE 删除";
- photo_id 走 query(dispatch 只解析到 parts[2],parts[3] 会被丢),
  与 trips/<id>/nodes/attach 同口径;
- 删除前校验照片确实属于这条记录,否则 404 —— deletePhoto 内部遇到
  错配是"删 0 行 + 静默成功",会让调用方以为删掉了;
- poster 仍只 POST(换海报是覆盖语义,清空请走 PATCH 空 poster_key)。

端上:
- FbMovieMedia 加 id(删除必须按 id,key 只用于取图);
- FbMovieRepo.deletePhoto() → DELETE + query;
- 编辑表单已有照片右上角加 ✕,文案改「已有 N 张(点 ✕ 可删)」;
- deletingPhotoId 防连点(第二次会 404,报错看着像功能坏了);
- 立即提交、不等「保存修改」,与网页 movies/photo_del 同口径;
  不加二次确认,与旅程 removePhoto 和网页端一致。

验证:归属校验拦住越权(event_id 错配查询返回 false);临时图入库
5→4 张、库行消失、Storage::exists 由 true 变 false(驱动为 OSS);
真机 ✕ 角标 4 个等距、点掉一张后服务端 4→3、UI 变「已有 3 张」。
hvigorw assembleHap BUILD SUCCESSFUL。

⚠️ 事故:测试时删掉了用户一张真实照片(id=14),OSS 对象随之删除、
Storage::read 返回 404,无版本控制/无回收站/设备无缓存,无法恢复。
教训已写进记忆:验证删除功能绝不拿真实数据当被测对象。
2026-11-06 11:09:55 +08:00
laoyang 3430b1caea 观影照片查看器删掉底部翻页条,不再挡图 (#85)
用户:查看观影照片的时候,上一张、下一张,还有几张都在中间,把图给挡住了。
把这些内容都删掉吧,不要了。

那条控制条是 Row{ ‹ 上一张 / 第n/M张 / 下一张 › },
靠 align(Alignment.Bottom) + margin(bottom:40) 贴底,
而大图是 height(76%) **垂直居中** —— 换算后图占屏幕 12%~88%,
控制条正好落在图区间内,压在上面挡画面。

整条删除,翻页只留**左右滑动**(PanGesture,相册通用手势)。
⚠️ 没改成把控制条挪到图下方:76% 的图两侧只剩 12%,
  硬塞会把图压小;而且用户原话就是删掉。

验证(真机 5NC0225B27004718):
- UI 树里已无「上一张/下一张/N / M」,只剩状态栏自带的网速与时钟;
- 原控制条位置 y2100-2350 非黑像素 0/15183 = 0.000%,完全干净;
- 全屏结构:图 y≈700-1740,上下纯黑;
- 左滑翻页仍生效(相邻帧差异 895/1024、865/1024),滑到第3张后底部依旧全黑;
- 点背景能关闭。
hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 10:57:40 +08:00
laoyang 05b86262ff 观影照片支持视频+转码;同看的人改头像标签 (#84, #83)
用户:① 同看的人,按照头像加名字组成标签的形式进行展示。
      ② 确认观影照片是否允许上传视频,不允许则需要改,要求能上传视频,
         视频需要转码,参考旅程中的行程做法。

① 同看的人改头像胶囊(#83 追加)
- 原来一行纯文字「已选 2 人:杨永全、红围脖」,人多了被 maxLines(1) 截断;
- 改成每人一颗胶囊:头像(FbAvatar,有图显示图/没图回落色块字) + 名字 + 小叉,
  Flex(wrap) 流式换行(Row 不换行会横向溢出,本项目 v76 同款事故);
- ⚠️ FbAvatar 必须传 imageUrl,否则有头像的人也退化成色块字
  (首页观影卡 2026-10-19 踩过同一个坑)。

② 观影照片支持视频(#84)
事实核查:原来不允许。POST /movies/<id>/photos 走 Upload::image(),
只收 JPG/PNG/GIF/WebP,传视频直接拒。旅程走 Attachment::save(),
内部对 video/* 调 Video::toH264() —— 观影要支持就得接上这条路。

服务端:
- Schema VERSION 35→36,event_photos 加 kind/thumb_key/mime 三列;
  kind 默认 image(老数据全是照片,默认值即正确,不需要回填 UPDATE);
  建表语句同步改,新库直接是新结构。
- 上传按 MIME 分流:视频走 toH264 + 新增的 Video::poster() 抽首帧,
  图片仍走 Upload::image()(裁剪压缩行为完全不变)。
- Video::poster() 取第 1 秒(手机视频开头常是黑场),失败不阻断上传。
- deletePhoto / destroy 补删 thumb_key,否则删视频留孤儿封面。
- addPhotos 兼容裸键数组与结构化数组,只有一条 INSERT 路径。

端上:
- 选择器 IMAGE_TYPE → IMAGE_VIDEO_TYPE(与旅程 FbTripNodeSheet 对齐);
  海报选择器保持 IMAGE_TYPE(海报必须是图)。
- FbMovieDetail.photos 从 string[] 升到 FbMovieMedia[](带 kind/thumb/mime),
  靠类型检查确保所有读取点都改到。
- 网格里视频显示首帧+播放角标(不解码,一屏 9 个视频各挂解码器必卡死);
  全屏查看器用 Video 播放。⚠️ VideoOptions 只有 src/currentProgressRate/
  previewUri/controller,controls/autoPlay 是属性方法不是构造参数。

验证:造 H.265 视频实测 needsTranscode=true、toH264 成功转成 h264/yuv420p、
poster 抽出 13KB 封面;完整上传后接口返回 5 条(4 image + 1 video 带 thumb);
端上网格出现 ▶ 角标、点击进全屏查看器且视频在播。
迁移已跑(35→36),4 个 PHP 文件 md5 逐个核对一致。测试数据已清理。
hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 10:52:42 +08:00
laoyang ac666dd7f4 编辑观影记录:修豆瓣联想乱弹与多字段回填,标题主题色、删副标题 (#83)
用户:编辑观影记录后片名下面列出好几个条目(豆瓣的?输入框变更后才该出现);
影院、同看的人、观影照片、简介、备注都没带进来;
编辑观影记录要用主题色;副标题那句删掉。

五个问题三种根因:

A. TextInput/TextArea 把回填当成用户输入
- 片名:prefill 改完 this.title,下次渲染的 TextInput 把新值当 onChange 回调
  ⇒ scheduleSuggest 弹出一排豆瓣候选。改为 onChange 里先比较 v !== this.title,
  值没真变就不调度;prefill 末尾兜底清联想态。
  (先试过 @Local prefilling 标记,没用——prefill 同步跑完标记立刻复位,
   而 onChange 是下一帧才回调的。)
- 简介/备注:areaField 是**值参数** Builder,TextArea({text:value}) 只拿首次
  调用的字面量。拆成 infoField/noteField 两个 Builder,内部直接读 this.info/note。

B. Row { 标签.width(100%); 内层Row } 把内容挤出可视区
- cinemaField/personField/photoField 三个字段都是这个结构,内层 Row 塌成 0 高、
  完全不渲染 ⇒ 看起来像没带进来,其实没画。全部改两排版式。
  (这是 #80 修 posterField 时漏掉的同一个坑,同文件另外三处没扫全。)
- 观影照片:照片区只渲染 photoUris(新选图),已有图在 editingDetail.photos 里
  从未渲染。新增 @Local existingPhotos,显示「已有 N 张(保存后保持不变)」。

C. 字段压根没从接口接上
- FbMovieDetail / FbApiMovieDetail 都没有 cinemaId/cinema_id,但服务端
  Movie::find() 的 SELECT 明确有 d.cinema_id。所以只能按影院名反查,
  重名会选错、改名会失败。补齐字段并解析,cinemaIdOf 改为优先用 id。

另外:FbSheetTitleBar 加 @Param titleColor(默认空=正文色,老调用点零影响),
观影编辑传 accentColor;编辑模式 subtitle 传空串,新建模式保留必填提示。

验证(真机):豆瓣候选无;影院「黄岛城市传媒广场 · 青岛东方影都艺术影城」;
同看人「已选 2 人:杨永全、红围脖」;简介输入框深色像素 2181(916 字全文);
备注 0(该记录 note 本来就是 NULL,SQL 已核实);照片「已有 4 张」;
标题 RGB(99,69,112)=观影模块色 #7A4E8C 派生;标题下方 50px 内无文本。
hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 10:30:39 +08:00
laoyang 0f22aef689 弹框动画丝滑化:入场加位移+弹簧曲线,修加载态打断动画 (#82)
用户:首页,弹出所有弹框,动画,都要改一下,改的跟丝滑流畅,
尤其是观影,基本上没动画,都需要修改。

先核实:把时长临时放大到 3s 后连拍,遮罩 246→210→170→153→148 逐帧压暗,
说明**动画确实在播**。所以不是没接上,而是动效本身太弱:
220ms 太短、只有 0.94→1 缩放(差 6%,且无方向感)、FastOutSlowIn 落定硬停、
进出同长。

四处改动:
① 入场加纵向位移 SHEET_RISE_VP=22 —— 竖向是人眼最敏感的方向,
   纯缩放读不出从哪来;22vp 是看得出从下面来但不觉得它飞了的量。
② 时长分离:卡片 320/200、遮罩 240/160(出场必须比入场快,
   用户已决定关闭,等待就是拦他);缩放起点 0.94→0.90。
③ 曲线换物理弹簧 curves.springCurve(0, 1, 228, 26)(末段自然收敛)。
   ⚠️ 先写的 Curve.springMotion 编译失败——本工程 SDK 6.1.1 里没这个 API,
      技能包速查表也把它列在高频臆造 API里,是我没照表走。
④ 观影/日程详情卡加载态:**这是观影没动画的真凶** ——
   卡片高度按内容定,加载态只有一行字(~100px),详情回来瞬间撑到 72% 屏高,
   320ms 的入场动画只播了前 1/3 就被高度突变打断。
   改法:加载态放固定 460vp 骨架屏(FbEventSheet 同样问题,一并修 300vp)。
   用固定 vp 而非 %:builder 在 Scroll 里,百分比拿不到基准。

验证(真机):放大时长连拍 N2 卡片顶边 y=1073 → N3 y=1011,
上移 62px≈20vp,证实位移生效;临时给接口加 1.5s 延迟后抓图,
骨架屏(海报位+4 条文字条)正确撑住卡片高度;临时代码已撤除(grep 0 处)。
连开合 3 轮均正常、无 JS 崩溃、进程存活。
hvigorw assembleHap BUILD SUCCESSFUL。
FbAnim 是全站唯一动画入口,98 处引用 / 14 个页面,全部同时生效。
2026-11-06 10:18:22 +08:00
laoyang b0e12f14fe 首页/日程页:当前时间红线改为定时器驱动,不再冻结在冷启动时刻 (#81)
用户:首页今天的日程,那个红线,不会动态更新,只有刚打开应用的时候更新,
如果不清后台,就一直不动了。查一下这个更新机制,需要能够动态更新。

根因:nowMin: this.nowMinutes() 是**在 build 里直接调方法** ——
ArkUI 不会因为时间流逝重算任何东西,那个方法只在首次 build 时求值一次,
红线永远停在打开 App 那一刻。与 pitfalls 里 ForEach(this.xxx(), ...) 追踪不到
是同一条规则:build 里只有字段(@Local/@Param)建依赖,方法调用不建。

改法:@Local nowClockMin + setInterval 30 秒一跳改它;传参改成读该字段。
- 30 秒而非 1 分钟:1 分钟在整分附近显得迟一拍,30 秒只改一个 number,成本极低;
- 先立即刷一次,否则首屏红线停在默认 0(=00:00,跑到视窗外面);
- aboutToAppear 起、aboutToDisappear 停 —— 首页切模块走 if/else 分发,
  组件会销毁重建,成对钩子必然触发;
- 绝不在 build() 里起定时器(build 可能多次调用,定时器会越叠越多)。

顺带修 FbCalendarPage 完全相同的两处(月视图 + 议程视图)——
用户从首页切过去看到同样的死线。

验证(真机 5NC0225B27004718,红线 #FF3B30 像素扫描):
09:57:22 红线 y=1359.0,10:00:24 y=1366.5(下移 7.5px,
理论 3.03 分钟 = 6.9px,吻合);
切到别的模块再切回后仍在走:1368.5 → 90 秒后 1372.5(4px≈1.75 分钟,吻合),
说明 aboutToDisappear 清掉旧定时器、aboutToAppear 起了新的,无泄漏无停摆。
hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 10:02:45 +08:00
laoyang 6a8a97286a 观影详情卡:对齐、去「写评价」、补编辑入口与完整编辑 (#80)
用户:评价与评论和上面的观影照片对齐吗?下面的写评价不要了,三个字。
另外一个,是不是得有个地方放编辑按钮啊?观影其实可以编辑……后面会补几张照片。
但编辑这个功能应该还是有的。

① 对齐:FbSectionTitle 自带 padding left:4,简介/观影照片是裸 Text 无内边距,
   实测评价与评论起点 111 vs 观影照片 99(差 4vp)。用 margin left:-4 抵消。
   不改用裸 Text —— 那样会丢掉全站统一的字号/字重/字距/胶囊按钮口径。

② 去「写评价」:FbMovieReviewSheet 加 hideFormTitle;无评价时整行不画,
   但有评价时保留那一行(右边的修改/删除/收起按钮在那)。

③ 编辑入口 + 完整编辑(用户选定:按钮放卡片最底部、做完整编辑):
   - 服务端 PATCH /api/v1/movies/<id> 本来就有(movieData 支持缺省回退原值),
     这轮只补端上缺的那层;
   - FbMovieRepo 新增 updateMovie() → PATCH,成功后重拉列表回写本地库;
   - FbMovieNewSheet 加 editingId/editingDetail,aboutToAppear 走 prefill 回填,
     submit 按模式分派 POST/PATCH;标题/按钮文案随模式切换;
   - 宿主(首页 + 电影页)详情卡 onEdit → 关卡 + 同一张表单编辑;
   - 编辑时不发 poster_key(发空串会清掉已有海报);
     海报区显示「当前海报 / 不选新图就保持不变」,避免用户以为图丢了白传一次。

两个真坑:
 1) if/else if 条件里不能调方法:curPoster() 探针打出来是对的,
    界面却一直走 else —— ArkUI V2 只对字段/参数建依赖。改读 @Local 字段。
 2) Row { Text().width(100%); Row {...} } 会内层挤出:posterField 原来就是这个结构,
    标签吃满整行 ⇒ 内层塌成 0 高、完全不渲染(症状是图/字凭空消失)。
    按全局表单规范改两排。

验证(真机 5NC0225B27004718):评价与评论 x 起点 98 vs 观影照片 99 已对齐;
「写评价」不再出现;编辑按钮在 y=1987;点开标题为「编辑观影记录」,
片名/时间(10月9日 16:20)/简介全部回填;海报区三行文案齐全,
缩略图区域色数 13567(真图)。hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 08:44:36 +08:00
laoyang cd60aa7788 观影详情卡:评价评论内嵌到底部可直评;简介/观影照片按全局规范改样式 (#79)
用户:电影弹出框,评价与评论放在最下面,直接显示出来,可以直接评论,不用再打开了。
简介、观影照片,注意按照我们的约定进行颜色和字体的修改。

① 评价与评论内嵌
- 删掉 FbMovieDetailSheet 里的入口胶囊与 reviewsOpen 死状态;
- 在 body() 末尾内嵌 FbMovieReviewSheet({embedded:true}),顺序与网页端一致:
  海报/片名/时间/评分/影院/同看的人/简介/备注/观影照片/**评价与评论**;
- FbMovieReviewSheet 加 embedded 开关,两种形态共用 reviewContent(),
  不抄第二份 300 行(抄必然改一处漏一处)。

② 评论框默认隐藏(用户明确):点「发表评论/回复」才展开,发送成功或取消即收起
  (submitComment 成功路径本来就会把 commentTarget 归零)。

③ 我的评价两态(用户选定):已评过=摘要(★+感受+正文3行+修改/删除),
  没评过=展开表单。保存成功自动收起;删除成功自动展开并清空草稿。
  加 myFormTouched 防止 load() 重跑时把刚写完的表单又收起来。

④ 简介/备注/观影照片标签按《全局表单规范》:
  主题色 accent + Bold + fontSmall + letterSpacing 0.6,标签独占一行、内容第二行整宽。
  改前是 fontTiny + faint 灰,与正文挤在一起。「N 张」保持 fontTiny+faint(同 extra 口径)。

验证(真机 5NC0225B27004718):
- 逐段抓 UI 文本,顺序命中 简介→观影照片→评价与评论,评价在最下面;
- 标签取最深5%像素均值:简介(98,67,110)、评价与评论(98,68,110)、写评价(97,69,109),
  均为主题紫色相;改前的 faint 灰是 (150,150,150) 量级;
- 底部文本含 写评价/评分/观后感受/发表评价,未评过故表单默认展开;
- hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 07:58:45 +08:00
laoyang 723d028679 今天的日程:整块内容补顶部内边距,不再贴白卡顶边 (#61)
用户:首页“今天的日程”那个色块,距离下边白色背景的上边缘还是贴着的。
跟之前我们提到过的“全天日程”一样,上面得稍微留一点空间。

与上轮不同:#61 上轮修的是 FbDayTimeline 里全天色块自己的 padding,
这次是整块内容(时间轴第一个刻度 + 第一条日程色块)都贴边,位置更外层。

根因:FbDayEventPanel 在首页是 showHead=false(首页自带 FbSectionTitle),
头部一关就没有任何顶部内边距了 —— 空态/时间轴/列表三个分支的 padding
都只有 left/right/bottom。内容直接从白卡片圆角顶边开始。

改法:加在根容器上,且只在 showHead=false 时生效
  .padding({ top: this.showHead ? 0 : 10 })
- 加根容器而不是三个分支:分支会增删,漏一个就复现;根是唯一入口
- 带条件:有头时头自己带 top:12,再补会让标题离卡片顶太远(日程页会显形)
- 取 10:与本单上轮给全天色块的 top:10 同数,两处呼吸要一致

验证(真机):白卡顶边 y=1062、首个色块 y=1094,间距 32px=10.2vp
(改前 9px=2.9vp)。3 处调用点里只有首页 showHead=false 受影响,
日程页两处默认 true 零变化。hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 07:41:24 +08:00
laoyang a7947f31be 观影/联系人详情卡补内置蒙版 (#78)
用户:电影这个框弹出来的时候,应该在下面盖一层蒙版吧?跟近期任务类似。

首页各浮层里只有这两张卡没有遮罩:FbTaskPanel / FbDayEditSheet 都是组件内部
根 Stack 自带遮罩+卡片,而 FbMovieDetailSheet / FbPersonSheet 两处漏了,
卡片直接浮在正文上,底下的片名、影院、联系人名字透出来。

改法(按项目既有约定:谁铺满全屏,谁负责画遮罩):
- FbMovieDetailSheet.build 根节点 Column -> Stack,前面插全屏 FB_MASK 遮罩,
  onClick 关闭;卡片容器保持铺满+居中且 HitTestMode.Transparent;
- FbPersonSheet.build 同样套一层 Stack + 遮罩(embedded 分支跳过,
  那种形态宿主是 bindSheet,系统已有遮罩,再叠就是双暗);
- 宿主 FbHomePage 不再包遮罩,只留注释说明。

原因:组件根节点自己铺满全屏,会在外面先吃掉点击 —— 宿主包遮罩会
蒙版看着有、点它关不掉;包两层还会越点越黑(本轮真机实测踩到)。

验证(真机 5NC0225B27004718):
- 弹卡后卡片外像素 (247,243,234) -> (147,146,141),反推 alpha=0.40 = 0x66/255;
- 卡片内海报 (230,212,188) 不受影响;
- 点遮罩后恢复 (247,243,234),卡片关闭;
- 联系人卡同样三条全过;hvigorw assembleHap BUILD SUCCESSFUL。
2026-11-06 07:35:47 +08:00
laoyang 4e3038a0aa 首页最近观影补真海报(v88)
用户:首页最近观影没有图片呢,左侧都是占位符。

v86 建这张卡时为“首页少打几个请求”,把海报刻意砍成 34x34 的 emoji 色块,
所以左侧不是加载失败的兜底,而是本来就只有占位符。网页端 _card_movies.php
一直是真海报,两端由此不一致。

改法:
- 海报位 34x46(2:3,与网页端 .hc-mv-poster 同口径),Image(fbImageSrc(...))
  先查本地附件缓存,命中零请求;
- 加 mvPosterFailIds + onError:取不到退回 emoji 色块,不留半张白图
  (与电影页同一手法,整体换新数组才刷新);
- 不加 ?w= 压缩档位:media.php 只留 1600 一档,对缩略图无意义。

验证:hvigorw assembleHap BUILD SUCCESSFUL;线上库实测 poster_key/文件/代理地址三者一致。
2026-11-05 21:14:44 +08:00
laoyang 06e6e3798a 名册加内存缓存 + 编辑框自己拉一份 (#92)
用户 2026-11-05:「速度比较慢。刚点开的时候显示的是『成员 #1、成员 #3』
这样的样式,是不是因为没读到人的信息?」

是。占位项(`成员 #id`)就是 roster 还没到位时的兜底。

**慢的真因**:`FbPersonRepo.list()` 是**全表查询**
(800 人 + JOIN person_roster + 标签 GROUP_CONCAT 子查询),
而每个用到选人的组件都在首次打开时各自调一次 ——
首页、任务面板、观影、旅程、资源、账本…逐个来一遍,
同一份数据反复查、反复解析。

① FbPersonRepo 加内存缓存(5 分钟)
   · 缓存**只对整表查询**有意义;带关键词时在内存里过滤(同一份 `matches` 口径,
     行为与"查库再过滤"完全一致,且更快)。
   · 缓存的是**同一份数组引用**:调用方都只读不改(现有代码符合),
     不做深拷贝(拷 800 个对象本身就不便宜)。
   · 新增 `FbPersonRepo.invalidate()`。

② 同步完成后失效
   `FbSyncAll` 收尾时,只要这次不是"全部跳过",就 `FbPersonRepo.invalidate()`。
   ⚠️ 不清的话:用户改了头像/名字、同步完再点开还是旧的那份,要等 5 分钟过期。
   ⚠️ 只在 "skipped < total" 时清:全部跳过说明服务端没变,缓存本身有效。

③ 编辑框自己也在 aboutToAppear 里要一份
   父页面(首页)的 `dayRoster` 是异步加载的,点开那一瞬可能还是空。
   ⇒ 组件自己调 `list()`(有缓存时是**瞬时返回**),
     查找时"先查自己那份、再查父页面传的"。
   这样占位项一闪而过甚至看不到。
   ⚠️ 两份合并而不是只认一份:父页面可能后到(`@Param` 变化会重画),
      组件自己拉的也可能失败(那就靠父页面的)。

编译通过,已装真机。
2026-11-05 21:01:28 +08:00
laoyang 9047fb411d 领证纪念日实际关联 2 人;端上只读了老列所以只带出 1 个 (#91d)
用户 2026-11-05:「你去查一下这个领证纪念日到底关联了几个人,数据库里。」

查库结果:
  anniversaries #2 「领证纪念日」
    person_id = 1                    ← **老列**,只能存一个
  anniversary_people(多对多表)
    person_id=1  杨永全
    person_id=3  洪博为
    = **2 人**                        ← 真正的事实

根因链(两处都缺一不可):

① 服务端 `occurrences()` 从来没输出 `person_ids`
   `peopleMap()`(查多对多表)只在 `split()` 里用了 —— 那是**网页端**的路径;
   `occurrences()` 是 API(鸿蒙端)用的,只透传了 `person_id` 这个老列。
   ⇒ 端上永远只拿到 1 个人。
   ⇒ occurrences 开头一次性 `peopleMap(array_column($all,'id'))`,
     输出 `person_ids`(多对多表为准,为空才退回老列)。

② 端上编辑框只读 `dayDetail.personId`(单个数字)
   ⇒ 改用 `personIds.slice()`(完整列表)。
   FbRecentDay 新增 `personIds: number[]`;`parseRecentDays` 里
   "person_ids 非空就用它,否则退回 person_id"。

⚠️ 老列 `anniversaries.person_id` 是升级到多对多之前的遗留字段。
   两边不一致时**以多对多表为准**(用户在新版界面上改的就是那张表)。
⚠️ 部署时踩了一次:第一次 scp 没生效(服务端文件 grep 不到新代码),
   导致验证结果仍是空的。重新部署并 md5 比对后才确认生效。

实测(部署后):
  领证纪念日   person_ids = [1,3]  → 杨永全、洪博为 ✅
  杨永全 的生日 person_ids = [1]    (派生条目单个人)
  其余派生条目同理,各 1 人。

编译通过,已装真机。
2026-11-05 20:57:23 +08:00
laoyang 5f8de01033 修:点已有纪念日编辑时关联人一片空白 (#91b)
用户 2026-11-05:「刚才关联人还有1人呢,现在关联人怎么没了?是个空的。」

真根因(两层,缺一个都不会有这现象):

① `dayRoster` 只在 `openDayCreate()`(**新建**入口)里加载
   `openDayDetail()`(**点已有纪念日**)从不加载它 ⇒ roster 恒为空数组
   ⇒ 编辑框里按 id 查不到人。
   ⇒ 抽出 `ensureDayRoster()`,两个入口都调。
     `openDayDetail` 里**不 await**:先让卡片弹出来(点击要即时反馈),
     名册回来后 `@Param` 变化会自动重画 chips。

② `pickedPeople()` 是"查不到就 skip"
   roster 为空时返回 `[]` ⇒ chips 区什么都不画 ⇒ 看着像关联关系被清空了。
   而这数据其实一直在 `personIds` 里,保存也不会丢 ——
   典型的"查不到就静默丢弃",最吓人的那种:数据没坏,但界面像是坏了。
   ⇒ 新增 `pickedSlots()`:roster 里查不到**也产出占位项**
     (名字写「成员 #id」、known=false、灰色字),至少让人看到"关联了一个人"。
   ⇒ 判断分支改看 `personIds.length`(**事实**),
     而不是 `pickedPeople()`(可能因 roster 没到而为空)。

顺手删掉 `pickedPeople()`(改成 slots 后没人用了,712 字符)。

⚠️ 派生(生日)走只读卡不需要名册,但 `openDayDetail` 统一加载也无害
   (`ensureDayRoster` 有缓存判断,第二次直接返回)。

编译通过,已装真机。
2026-11-05 20:53:53 +08:00
laoyang 4963a1b520 纪念日编辑框:五项文案与形态调整 (#91)
用户 2026-11-05 五点:
  「只记"几月几号,每年自动提醒"这句话,把它去掉。」
  「"叫什么"不应该这么叫,应该换成"纪念日"标题。」
  「"按什么重复"也太复杂了,就写"重复"。」
  「日期右边为什么有个箭头呢?不要。」
  「分类这个地方平铺。关联人这个地方也显示出来:
    这个人已经关联的人是谁?名字、头像。」

① 删掉副标题「只记几月几号,每年自动提醒」
   标题已经说清这是纪念日,副标题在解释"格式",用户不需要看第二遍。

② 「叫什么」→「纪念日」

③ 「按什么重复」→「重复」

④ 日期行去掉右侧 ›
   `FbDateTimeRow` 内部的 `›` 是**写死的**(给"点开会选"的暗示)。
   不用它、改成手画一个与其它字段同形态的框(40 高 / inputBg / radiusS):
   ⚠️ 不动那个组件:它还有自己的标签行、42 高、箭头,三样都与本框不一致;
      硬改成参数化会波及任务/日程/观影几处的用法。
   ⚠️ 点击整框(不只文字)都能开选择器 —— Row 的 onClick。

⑤ 分类改平铺
   横滑 Scroll → `Flex(wrap: FlexWrap.Wrap)`,全部一眼看全。
   ⚠️ 不能用 Row:ArkUI 的 Row 不换行,塞不下会横向溢出
      (任务面板看板 chips 踩过:最后一个被裁掉且点不到)。
   ⚠️ Flex 里 `space` 不生效 ⇒ 间距靠 chip 自己的 margin。

⑥ 关联人改成「头像 + 名字」chips
   改前:一行纯文字(「张三、李四」/「全家的事」)
   改后:换行平铺的 chips,每个 = 24vp 头像 + 名字
   一个都没关联时显示「全家的事」框;下面单独一颗「+ 指定人 / 更换」按钮
   (只点 chips 会让人以为只能取消,看不出能加人 —— 与任务面板
      「负责人」那颗「更换」同一手法)。
   ⚠️ 顺序按 personIds(用户点选顺序)而不是名册顺序,
      否则"刚加的人跑到最后去了",看着像没加上。
   ⚠️ 顺手删掉已无人调用的 peopleText()(520 字符死代码)。

编译通过,已装真机。
2026-11-05 20:50:49 +08:00
laoyang 93d0e886fb 纪念日编辑框按任务面板的样式重做 (#90)
用户 2026-11-05:「现在我们来讨论纪念日的弹出框,也就是自己添加的纪念日
那个弹出框。这个做法和那个任务的做法是一样的,包括它的布局、颜色,
你都按照那个编辑,按那个方式去修改。包括删除按钮和保存按钮怎么放,
也都完全按照那个来做。」

逐项对齐 FbTaskPanel:

① 顶栏标题
   改前:fontTitle(17) + Medium + 墨色
   改后:fontBody(15) + **Bold** + **主题色**(与任务面板 headBar 同)
   副标题从「主题色 fontTiny」改成「faint fontTiny」(任务是「属于父任务」,那个是浅灰)
   关闭按钮 color 从 accent 改成 ''(与任务面板一致)

② 字段布局:横向 → 上下两行
   改前:`标签(44vp 浅灰小字) + 控件` 挤在一行
   改后:每个字段 = Column(space 6){ fieldLb(标签) ; 控件 }
         控件整行宽 / 40 高 / inputBg / radiusS,与任务面板逐项一致
   新增 fieldLb():fontSmall(13) + Bold + 主题色 + letterSpacing 0.6
   (与 FbTaskPanel.fieldLb 逐项相同)

   六个字段全部改了:叫什么 / 按什么重复 / 日期 / 分类 / 关联人 / 备注

③ 删除按钮挪到滚动内容末尾
   改前:和保存并排钉在底栏(红底白字)
   改后:最底部一行小字「删除这个纪念日」,要滚到底才看得到
         二次确认改成「删了就找不回来了,确定吗?」+ [确定删掉] [取消]
   —— 与任务面板 dangerZone 同一做法。
   理由(同任务面板):删除不可恢复,不该和"保存"这种高频操作并排常驻。

④ 底栏只留整行「保存」
   改前:「删除」+ 「记下来 / 保存修改」并排
   改后:一颗全宽「保存」,高 42(原来 44),圆角 radiusS
   文案统一叫「保存」—— 原来分「记下来」「保存修改」两个说法,
   与任务面板不一致。

⚠️ 字段**顺序**没有照抄任务面板(名称→重复→日期→分类→关联人→备注):
   两个框的字段集合不同,这几项之间的因果关系是"先起名、再定哪天",
   照抄顺序反而乱。用户说的是"布局和颜色"一致。
⚠️ fieldLb 在两端各有一份实现(FbTaskPanel 与 FbDayEditSheet 各自独立)——
   以后调字号/字重两处都得改,已在注释里写明。

编译通过,已装真机。
2026-11-05 20:47:06 +08:00
laoyang cebfbde887 生日卡:头像+姓名那一组真正居中 (#89b)
用户 2026-11-05:「看着效果不错,上面的头像和那个名字也是一样的,也往中间挪一挪。」

上一版给那个 Column 加了 `constraintSize({ maxWidth: '72%' })`,
但它里面两行 Text 都写着 `.width('100%')` ——
那是更早为了"长名字换行时占满整行"留的,副作用是把 Column 撑到上限:

  Row 里 Column 的宽度 = max(内容宽, 子项 100%) = 72%(顶到上限)
  ⇒ Row 的 `justifyContent(Center)` 只在这 72% 的外层居中,
    组内仍然左对齐、右边空掉 28% —— 正是用户说的"还偏左"。

改法:去掉两行 Text 的 `width('100%')`,让 Column 按**最长那行**收缩。
      这样 Row 的 Center 才能真正把「头像 + 姓名」整组挪到中间。

⚠️ 长名字仍然能换行(`maxLines(2)` + `TextOverflow.Ellipsis`,
   换行后 Column 自然变宽到内容宽度,`maxWidth: 72%` 兜着不会溢出)。
⚠️ 具体文字仍 `HorizontalAlign.Start`(左对齐)——用户要的是"块居中",
   不是"每行文字居中"。
⚠️ 方框那一行不用改:它右边那个 Column 里没有 width(100%) 的子项,
   本来就按内容收缩。

编译通过,已装真机。
2026-11-05 20:43:57 +08:00
laoyang 4dbfbb9218 生日卡内容块整体居中 (#89)
用户 2026-11-05:「这个文案是不是居中放一放会更好一点?现在所有东西都集中在
左边,右边太空了。我是想整体往右移,也就是居中,而不是把文字什么的居中放。」

关键是要分清两种"居中":
  · 用户要的 = **内容块整体居中**(头像+姓名这一组、方框+日期这一组,挪到卡片中间)
  · 用户明确不要 = **文字居中**(每行文字仍左对齐,读起来才不散)

根因:两个内容 Row 里的 Column 都挂着 `layoutWeight(1)`,
它把 Column 撑满剩余宽度 ⇒ Row 里没有多余空间 ⇒ `justifyContent` 无从生效,
于是整行永远贴左。

改法:
① 去掉那两个 `layoutWeight(1)`,改用 `constraintSize({ maxWidth })`
   (头像行 72%、方框行 68%)—— 按内容宽度收缩,同时防止长名字撑破卡片。
② Row 上加 `justifyContent(FlexAlign.Center)` —— 把这一组整体挪到中间。
③ 具体文字保持 `HorizontalAlign.Start`(左对齐),不变。

⚠️ 顶栏那行的 `layoutWeight(1)` **保留**:那里要"标题靠左、关闭靠右",
   去掉会让标题和关闭挤在一起。
⚠️ 底部「要修改生日…」那句本来就是居中的(width 100% + textAlign Center),
   与上面两个块的"块居中"是两件事,不动。
   —— 已在代码里写明,免得以后有人"统一"掉。

编译通过,已装真机。
2026-11-05 20:42:13 +08:00
laoyang 7ee4810435 生日卡重排:方框放"还剩几天"、右侧放农历日期 + 阳历日期 (#88)
用户 2026-11-05:「还有5天,也就是10月2号,这个不好。因为"还有5天"这个事情
比较重要,所以方形框里面应该写几天,比如还有5天就写5天;现在还有5天,
那个位置应该写上10月2日。如果是农历的话,就要写上农历十月二日。
下面还要写上阳历:换算完之后,今年阳历是多少?比如11月10日,
都放在右侧"满42周岁"这个位置。」

上一版信息分工正好是**反的**:方框里放了「日」(几号),右侧放了「还有 N 天」。
用户要的相反。

重排后:
  方框(84x84 浅主题色块)   = **还剩几天**(大数字)+ 下方「天」/「今天」
  右侧第一行                 = 日期(农历生日「农历十月初二」;其余「11月16日」)
  右侧第二行                 = 阳历换算(**只有农历生日才画**)
  右侧第三行                 = 满 N 周岁

服务端 Anniversary::occurrences() 补两个**成品文字**字段:
  lunar_label = 「十月初二」「闰四月初五」(农历生日才有)
  solar_md    = 「11月10日」(今年实际发生那天的公历月日)

⚠️ 农历文字为什么不在端上拼:Lunar 有完整的月名/日名对照表
   (正月/冬月/腊月、初一/廿三…),端上再抄一份必然漂移,
   而且闰月(闰四月)这种特例得一起处理。
⚠️ solar_md 用 $date(今年换算后的实际日期)而不是 $a['month']/$a['day']:
   农历生日的 month/day 是**农历**月日,直接拼出来会是错的公历。
⚠️ 公历生日时 dateSolar() 返回空串 ⇒ 不画第二行:
   那时第一行已经是「11月16日」,再画一次一模一样,纯噪音。
⚠️ days = 0(就是今天)时方框写「今」而不是「0」:
   0 看着像没数据,而且底下那行写「今天」,配成「今 / 今天」很奇怪。

实测服务端输出:
  高雪玉 的农历生日  days=5  lunar=十月初二  solar=11月10日  date=2026-11-10
  领证纪念日        days=7  lunar=(空)      solar=11月12日  date=2026-11-12
  杨永全 的农历生日  days=8  lunar=十月初五  solar=11月13日  date=2026-11-13
  杨永全 的生日      days=11 lunar=(空)      solar=11月16日  date=2026-11-16
  刘巧莲 的农历生日  days=29 lunar=十月廿六  solar=12月4日   date=2026-12-04

编译通过,已装真机。
2026-11-05 20:39:44 +08:00
laoyang e1152941af 纪念日生日卡:删副标题、改文案、头像兜底 (#87)
用户 2026-11-05:
  「现在还是没有头像。」
  「来自成员资料不能在这里修改这句话去掉。」
  「要修改生日,请到名册里打开这位成员的资料。这句话改成:
    要修改生日,请到名册里修改这位成员的资料。」

① 头像为什么没出来 —— 查清了,**链路是通的,是那个人本来就没头像**
   数据库实查:
     persons #1108 高雪玉  avatar=[]  avatar_color=[]
     全家 800 人里只有 **19 人** 有头像
   而首页「最近纪念日」前三条正好是:高雪玉 / 领证纪念日 / 杨永全,
   第一条就是没头像的那位 —— 点开自然只看到色块。
   (同前三条里的「杨永全」是**有**头像的,点他就能看到真图。)

   ⇒ 兜底做稳:新增 blockColor()
     · avatar_color 有值就用它;
     · 没有就**按名字算一个稳定色**(charCode 求和取模),
       同一人每次打开颜色一致 —— 不能随机,否则每次弹出来都变一个色;
     · 调色板用纪念日模块的暖色系(8 档),避免"别人暖色、这个是荧光绿"。

② 去掉「来自成员资料,不能在这里修改」
   顶栏「生日 / 农历生日」本身已说明这是什么,再挂免责声明显得啰嗦;
   底部那句出口已经表达了边界。

③ 文案改成「要修改生日,请到名册里修改这位成员的资料。」
   (原来写的是"打开这位成员的资料",用户改成"修改";
     另外"家人"改成"名册"——与模块名叫法一致。)

编译通过,已装真机。
2026-11-05 20:35:53 +08:00
laoyang 41e3354fc8 纪念日:生日改成只读卡、自建纪念日才可编辑 (#87)
用户 2026-11-05 四条:
  1.「如果这个纪念日来自生日,我们是不能编辑的。」
  2.「点击之后,像日程那样弹出一个框来显示就行:显示这个人的头像、姓名,
     以及生日的『日』和日期就可以了。」
  3.「如果是我们自己创建的纪念日,那么这里面就是编辑纪念日。」
  4.「这个框和刚才我们编辑任务的框是一样的做法。样式、格式全部都一样。」

🔴 先说一个**会改坏用户数据的 bug**(这次一并修掉):
   原来派生生日也走 FbDayEditSheet,而且传的是
     editingMonth: 1, editingDay: 1   ← 写死的 1 月 1 日
   结果点开任何一个生日都显示「1 月 1 日」,点保存还能把
   **成员档案里的真实生日覆盖成 1 月 1 日**。
   这不是样式问题,是数据损坏。

改动:

① 新增 components/FbDayBirthdaySheet.ets(派生生日的只读卡)
   · 外壳用 FbSheetRing + 遮罩点击关闭 + 右上角 FbSheetClose
     —— 与 FbTaskPanel / FbDayEditSheet 同一套版式(用户第 4 条)
   · 左上角「生日 / 农历生日」主题色粗体 + 一行小字
     「来自成员资料,不能在这里修改」——把"不能编辑"这条边界写明白,
     否则用户会以为卡片坏了(用户第 1 条)
   · 主体:FbAvatar(56) + 姓名(主题色粗体) + 完整日期
   · 「日」做成视觉焦点:84×84 浅主题色块里放 34 号大字数字 + 「11 月」
   · 右侧:还有几天 / 满 N 周岁 / 农历说明
   · 底部:「要修改生日,请到「家人」里打开这位成员的资料」
     —— 只读也要给出口,不然用户不知道去哪儿改

② FbHomePage 按 source 分发
   birthday_solar / birthday_lunar → FbDayBirthdaySheet(只读)
   custom                          → FbDayEditSheet(可编辑,原行为)
   ⚠️ 判据用 source 前缀而不是 id === 0:两者目前等价,但 source 是语义、
      id 是实现细节 —— 万一以后派生条目也带 id,用 id 判会静默错。

③ 服务端 Anniversary::occurrences() 补输出
   · person_name —— `birthdays()` 算出来了,但 occurrences 逐个字段挑着抄时漏了
   · avatar / avatar_color —— 只读卡要画头像(key,端上拼 URL)
   · day / month —— 用户要的「生日的『日』」
     ⚠️ 用 $a['day'] 而不是从 $date 截:农历生日的 day 是农历日
        (初五 = 5),date 是换算后的公历日 —— 两者不同,用户要看"生日那天"
   · birthdays() 同步补 avatar / avatar_color(它原来也没有)

④ Types / FbParse 补五个字段
   personName / avatar / avatarColor / day / month

编译通过,已装真机。
2026-11-05 20:32:02 +08:00
laoyang d9ca9dbe31 下拉框与网页端对齐(照 FeedLite 那套观感)(#86)
用户 2026-11-05:「好了,鸿蒙端也用类似的方式调,效果让它保持一致。」

端上**不需要像网页那样手写浮层** —— 网页端必须自绘是因为
「原生 select 的展开列表由浏览器绘制,CSS 改不动」;
ArkUI 的 Select 本身就是自绘的、选项列表也归框架管。
⇒ 把对应属性都设上就等价了:

  网页端                          ArkUI Select
  ──────────────────────────────────────────────────────
  按钮 40 高 / 圆角 / 细边框       height(40) + borderRadius + border
  箭头打开转 180°                 框架内置(自带动画)
  悬停 tint-1                     不支持(触屏没有 hover)→ 用打开态代替
  选中项底 tint-2                 selectedOptionBgColor(fbMixWhite(accent,0.80))
  选中项文字 主题色                selectedOptionFontColor(accent)
  选中项右侧对勾                   showDefaultSelectedIcon(true)
  浮层白底 + 细边框               menuBackgroundColor(White) + border
  浮层与按钮同宽                   optionWidth(OptionWidthMode.FIT_TRIGGER)
  未选值次要色                     fontColor(有值 ? accent : Faint)

① Theme 新增 fbMixWhite(hex, ratio)
   与网页端 fb_color_tint() **同算法同比例**(混 91/80/72% 白)。
   ⚠️ 为什么不用现成的 fbAlpha():那是「原色 + 透明度」,
      会透出父容器底色 —— 下拉浮层背景不确定,那样会时深时浅;
      这里是实色混合,颜色确定。
   ⚠️ 实测 16 组(4 个主题色 × 4 档比例)与 PHP 输出逐个比对,全部一致。

② 边框单独用一档
   按钮边框 fbMixWhite(accent, 0.62) —— 比 tint-3 再深一点,
   否则在 tint-1 的浅底上几乎看不见边界。

③ 修一个类型坑
   本来写了个 pickFontColor(): string 返回 accent / FbColors.muted,
   但 FbColors.muted 是 Resource($r(...)),返回 string | Resource
   会让 fontColor() 报「Type 'string | Resource' is not assignable to 'string'」。
   ⇒ 改成内联三元,且未选用 FbColors.faint(那个是 string 常量)。

编译通过,已装真机。
2026-11-05 20:25:59 +08:00
laoyang 389cb31df4 分类下拉:修「永远未分类」+ 菜单同宽 + 主题色 (#84)
用户 2026-11-05 三点:
  1.「分类那个地方没带进来。明明是有分类的,但是它却显示的是未分类。」
  2.「分类弹出来的框太窄了,应该和输入框一样宽。」
  3.「颜色也不对…文字颜色是主题色,背景色是比主题色浅一点的那个主题色。」

① tskCats 永远是空数组
   `FbHomePage.loadTaskMeta()` 开头:
     if (this.tskBoards.length > 0) { return; }
   **只看看板有没有值**。这个方法跑过一次、看板填上之后,
   以后再进来直接返回 ⇒ `tskCats` 永远停在上一次的赋值上;
   如果第一次调用时 cats 恰好是空(或 #83 之前的版本压根没赋过),
   就再也补不上 ⇒ 下拉里只有「未分类」。
   ⇒ 改成 `if (boards.length > 0 && cats.length > 0) return;`
     "两样都有值才跳过"。
   ⚠️ 不能只判 cats:新家庭可能真的一条分类都没建,那样每次都重拉。
   ⚠️ 任务页的 `loadMeta()` 没有这个提前返回,不用改。

② 下拉菜单太窄
   `width(100%)` 管的是**控件本身**;弹出的选项菜单有独立宽度
   (`optionWidth`),不给就按选项文字撑开 ⇒ 很窄。
   ⇒ `.optionWidth(OptionWidthMode.FIT_TRIGGER)` 让它跟触发器同宽。
   ⚠️ 顺手试过 `.optionHeight(auto)` —— SDK 里 `optionHeight` 只收
      `Dimension`,传 auto 直接编译报 "No overload matches this call"。

③ 颜色改主题色
   文字 `this.accent` + `weight: Medium`;
   底色 `fbAlpha(this.accent, 0.10)`(浅一档的主题色)。
   与面板里「+ 添加附件」那颗胶囊同一套 —— 整面板"可操作控件"观感一致。
   选项列表里:选中项用主题色、其余用墨色(全主题色会失去对比)。

编译通过,已装真机。
2026-11-05 20:19:43 +08:00
laoyang 79bc593d43 鸿蒙端:分类、循环改成可编辑 (#83)
用户 2026-11-05:「分类和重复,也就是循环,都需要加上,鸿蒙端也得加上。」

原来这两个只在**有值**时才画一行只读文字
(`if (this.categoryOf() !== )` / `if (this.repeatOf() !== )`)——
用户不仅改不了,连"这里本来有这一项"都看不出来。
(这与 #76 里网页端"负责人为空就整行消失"是同一个毛病。)

改法:与截止/负责人同形态(标签一行 + 控件一行),**始终显示**。

① FbTaskPanel 新增 `@Param cats: FbCatOpt[]`
   ⚠️ 与 boards 同一理由:**不能从任务里反推** ——
      反推只能看见"已经被用过的"分类,新建的分类在端上永远选不到。
② 下拉用 ArkUI 原生 `Select`,第 0 项固定「未分类」/「不循环」
   - 分类:index 0 → category_id = 0;index N → cats[N-1].id
   - 循环:index 0 → (不循环);index N → REPEAT_KEYS[N-1]
   ⚠️ REPEAT_KEYS 与 REPEAT_WORDS **必须严格对齐**,
      错一位就是"选每天存成每周"这种静默错数据。
③ `inlineBusy` 时退回只读文字:请求在飞的时候不给选,
   否则用户连点两下会发两次相反的值。
④ 首页 / 任务页两处宿主传 `cats`(tskCats / metaCats)。
⑤ 服务端 API 早就支持这两个字段(`Api::str(repeat_rule, ...)`),
   本轮只补了 `Task::setAttrs` 的 repeat_rule 分支(见服务端那次提交)。

编译通过,已装真机。
2026-11-05 20:15:25 +08:00
laoyang 8fe1131334 去掉截止、负责人右侧的小铅笔图标 (#77)
用户 2026-11-05:「截止日期和负责人右侧都有一个编辑用的、像小铅笔似的图标。
我感觉这个东西不需要了。如果要加的话,全部都得加上,台式机采购计划、测试
之类的都要加上。但现在我觉得不加也可以,就把所有小图标全部去掉,
也就是右侧那个小铅笔图标全去掉。」

删掉两处 `✎`:
  · 截止那行(rows: 到期时间 + 已逾期徽标 + ✎)
  · 负责人那行(头像 + 名字 + ✎)

上一版加它是想提示"这里能点"(本项目踩过"纯文字看着不可点"的坑),
但代价是:
  ① **只有这两行有、别的行没有** ⇒ 视觉上不齐,看着像漏做;
  ② 输入框本身(浅底圆角 40 高)已经足够表达"这是个可操作的控件",
     铅笔是重复提示。

⚠️ 全项目扫过:`✎` 只在 FbTaskPanel 这两处,已全部清零。
⚠️ 注释里记下取舍:以后若要恢复"可点"提示,必须**所有行一起加**
   (任务标题/截止/负责人/分类/循环/备注),不能只给其中两行 ——
   那才是用户不接受的地方,不是"有提示"本身。

编译通过,已装真机。
2026-11-05 20:04:59 +08:00
laoyang 619f800ae7 删除按钮从底栏移到内容最底部(危险操作区)(#77)
用户 2026-11-05:「删除按钮不能出现在那里,因为这样太容易误触了。
删除按钮应该放在哪里?你考虑考虑。」

放在**滚动内容的最末尾**。

为什么不在底栏(原来和「保存」并排):
  ① **误触**:底栏是"保存"这种高频操作的位置,手一滑就点到删除,
     而删除**不可恢复**,一次误触 = 数据没了。
  ② **语义错位**:底栏是"我改完了、要留下这个结果"的确认区,
     不该混进"我要毁掉它"。

为什么不放顶栏:顶栏是"知道我在哪 / 怎么退出"的位置,
  在那里放删除,误触代价同样不可恢复(还紧挨着关闭按钮)。

放最底部的好处:
  · 用户得主动滚到最底才看得见 —— 低频操作不必首屏可见;
  · 纯文字(fontTiny)、无填充底色,**不像按钮**,不招手指;
  · 与正文隔一条分割线,视觉上属于"其它",不属于"操作";
  · 这是通行做法(Notion / 备忘录 / 各类设置页的删除都在最底部)。

二次确认也重做了:
  第一下「删除这个任务」→
  变成「删了就找不回来了,确定吗?」+ [确定删掉] [取消]
  原来只有"再点一次"、没有取消出口,误点后只能点别处退出
  (不明显)。现在有明确的「取消」可退。

底栏现在只剩**全宽的「保存」**,视觉上更明确:这一条就是"确认改动"。

编译通过,已装真机。
2026-11-05 20:03:05 +08:00
laoyang ad63b4c89d 修:面板底部「保存/删除」按钮被裁掉看不见 (#77)
用户 2026-11-05:「你改的是什么?是鸿蒙版吗?怎么没有看到任何按钮啊?」

代码确实在(我解包 sy-77.hap 在 modules.abc 里搜到了"保存中"字符串),
是**布局把它裁掉了**。

真根因:中间那个 Scroll 用的是 `flexShrink(1)` ——
`flexShrink` 只表示"空间不够时允许收缩",但 Scroll 的**内容高度**
仍会把自身撑开(ArkUI 竖向 Scroll 默认按内容高参与布局)。于是:
  头部 + Scroll(按内容撑满) 吃光 `maxHeight: 86%`
  → 下面的 footBar 被推到 86% 之外
  → `FbSheetRing.clip(true)` 把它整条裁掉
  ⇒ 一颗按钮都看不见(而且连痕迹都没有,看起来就像"没做")。

改法:
① Scroll → `layoutWeight(1)`
   "占据剩余空间"而不是"按内容撑开"。高度被限定成
   "86% − 头部 − 分割线 − footBar",footBar 因此必然留在可视区内,
   内容再长也只在自己内部滚动。
② footBar 补 `flexShrink(0)`
   显式禁止压缩,防止某些机型把 42 高的按钮压扁。

⚠️ 旧注释(v60b)说 `layoutWeight(1)` 会让"短内容也把卡片顶满、底下留一片空白"。
   那个副作用是真的,但属于观感问题;按钮看不见是功能问题。
   两害相权先保证按钮可用。要消空白的话正确做法是外层配 minHeight:0,
   而不是回到 flexShrink(那条路会让底栏被挤出去)。

编译通过,已装真机。
2026-11-05 19:58:53 +08:00
laoyang 980630644b 底部「编辑」换成「保存」(#77)
用户 2026-11-05:「和网页版一样,没有保存按钮啊,这应该怎么保存呢?
在哪个地方点保存呢?」

之前是"失焦自动保存"——逻辑上能存,但**用户看不见这件事**,
没有按钮就没底,不知道到底存没存。补一颗显式按钮。

① footBar 的「编辑」→「保存」
   原来那颗「编辑」本身就是矛盾的:面板已经能改所有字段
   (标题/备注/截止/负责人/分类/协作人/子任务/附件/留言),
   再给一颗"编辑"反而跳到另一张表单。
   ⇒ 换成「保存」,保存后关窗(与网页端保存后关浮层一致)。

② saveAll():串行存标题 + 备注,然后 onChanged() + onClose()
   ⚠️ 必须**串行**不能并发:`saveField()` 内部会 `reload()`,
      两次并发会让后一次的 reload 覆盖前一次的结果,
      表现为"改完标题,备注却回到旧值"。
   ⚠️ 有 `saving` 忙碌态:避免连点两次发四组请求。

③ 其余字段保持"点了就生效",不按保存
   状态/看板/截止/负责人/分类/协作人/子任务/附件/留言 都是**动作**
   (选中即生效),多一步确认反而烦;只有"打字"类(标题、备注)
   才需要一次确认 —— 这也是网页端的口径。

编译通过,已装真机。
2026-11-05 19:54:30 +08:00
laoyang cbeeb55a82 任务标题改用单行输入框 (#77)
用户 2026-11-05:「现在这个任务标题是好几行的那种输入框,不要用这种输入框,
用一行的输入框就行了。标题没有那么长,现在太高了。」

TextArea(多行,height 84)→ TextInput(单行,height 40)。

① 高度 84 → 40,与「截止」「负责人」那些控件行同高
   标题是一句话,本来不需要多行;84 的高度等于多占两行位置,
   把下面所有内容(状态/看板/截止…)整体往下推。

② 顺带补 onSubmit:单行框里回车是"提交"而不是换行,
   用户打完直接按回车就保存,不用先点别处。
   多行框里回车是换行,所以以前不能加这一条。

⚠️ TextInput 只有一行,高度超了也只是上下留白 ⇒ 直接给 40,
   不是像 TextArea 那样"给多少显示多少"。
⚠️ 单行框超长时不换行、横向滚动 —— 标题本来就没那么长,可接受。

编译通过,已装真机。
2026-11-05 19:52:23 +08:00
laoyang 10cd10e9ff 顶栏去掉重复状态徽标 + 标题输入框与其它统一 (#77)
用户 2026-11-05 两点:
  1.「编辑任务那一行,右上角还有个"进行中",那个也去掉吧,
     因为在状态那里已经看到"进行中"了。」
  2.「台式机采购计划这个任务标题的输入框和其他的都不一样。我感觉没必要,
     字体什么的大小,和下面的备注还有什么的保持一致就行了,没必要特别特殊。」

① 删掉顶栏的状态徽标
   下面「状态」区三个 chip 已经把当前态高亮出来了,顶栏再挂一个只读徽标
   是同一信息画两遍。顶栏现在只剩「编辑任务/属于父任务 + 关闭」。

   ⚠️ `statusWord() / statusTint() / statusWash()` 三个方法**保留** ——
      状态区的 statusChip 还在用。
   ⚠️ 上一版注释写"只留状态徽标 + 关闭",与现实不符了,已同步改掉。

② 标题输入框与备注完全统一
   改前:fontSize(fontTitle=17) + Bold + height(64)
   改后:fontSize(fontSmall=13) + 不加粗 + height(84)
   三项参数与备注(noteDraft 那个 TextArea)**逐项一致**。
   标题的"重要性"由它排在最前 + 标签主题色表达,输入区不必再靠字号放大。

编译通过,已装真机。
2026-11-05 19:50:45 +08:00
laoyang d4b9d08b7e 标题/备注改成常驻输入框 + 内容用墨黑 (#77)
用户 2026-11-05 两点:
  1.「任务标题里面,"台式机采购计划"这样的字样还是改成黑色吧。
     任务标题这四个字用主题色,但是输入框里面的文字用黑色。」
  2.「这个输入框本来就应该是输入框,怎么现在点了一下才变成输入框呢?
     它应该直接就是输入框,只是把文字放到输入框里面就行了。
     这样的话,点一下键盘就弹出来了,包括下面的备注也是一样。」

① 颜色:标签主题色、内容墨黑
   上一版我把 TextArea 的 fontColor 也设成 accent,整段标题变成红的,
   像报错。现在:
     · 「任务标题」四个字 → fieldLb(主题色 + Bold)
     · 框里的文字 → FbColors.ink
   备注同理。

② 常驻输入框(这是关键的结构修正)
   上一版是"只读文字 + 点一下切成 TextArea"的双态(titleEdit / noteEdit),
   所以用户看到的是"点一下才变成输入框"。
   现在**直接就是 TextArea**,值从 titleDraft / noteDraft 来。
   点输入框由系统自己聚焦 + 弹键盘,不需要 requestFocus。

   ⚠️ 必须是**受控**的:`TextArea({ text: this.titleOf() })` 是非受控,
      每次 reload() 重建组件会丢掉用户正在打的字。
   ⚠️ 删掉 titleEdit / noteEdit 两个状态(双态不存在了),
      保存判据改成"值变了没有"。
   ⚠️ 标题**空值回填、不提交**:服务端会把空标题清洗成空串 = 把标题删了。
      用户只是清空重打,不该丢标题。
   ⚠️ 备注**允许清空**:删掉备注是正常操作,所以 note 不像 title 那样回填。

③ 焦点保护(新增 titleFocus / noteFocus)
   `@Monitor(refreshTick)` 会在外部刷新时调 reload(),而 reload 会用
   服务端的值覆盖 titleDraft —— 用户正打字时被覆盖就是"打着打着字没了"。
   ⇒ 有焦点时不覆盖,失焦后再同步(onBlur → save → reload)。

编译通过,已装真机。
2026-11-05 19:48:49 +08:00
laoyang 4cca4c51be 任务面板:左上「编辑任务」+ 点一下即编辑 + 标题最前 + 字号统一 (#77)
用户 2026-11-05 五点:
  「这个框的标题在左上角,是"编辑任务"几个字,颜色用主题颜色。
    你参考一下"编辑行程"的做法。」
  「任务和备注是点进去之后,只要点一下,马上就要变成编辑状态,
    然后弹出输入法,开始输入。不需要点一下之后先变成一个输入框,
    再点一下弹出输入法。直接点进去之后就是输入状态。」
  「任务标题应该放在最前面,放在所有内容之前,也就是状态之前。」
  「状态、分组、看板、任务标题、截止、负责人、备注、协作人、
    子任务、附件、留言这些字的大小要保持统一。」

① 左上角「编辑任务」(主题色)
   与 FbTripNodeSheet 的「编辑行程」完全同一套:
   fontSize(fontBody=15) + Bold + accent。
   只写固定的「编辑任务」,不写任务标题 —— 标题是内容(可改),
   已经下移到 titleRow()。
   「属于「父任务」」跟着放到标题下面第二行(原来是同一行的大字位置)。

② 点一下直接进编辑态并拉起输入法
   原来点一下只是 `xxxEdit = true`,把 TextArea 画出来 ——
   但**画出输入框不会自动获得焦点**,用户还得再点一下才弹输入法,
   正是他说的"变成输入框,再点一下弹出输入法"。
   ⇒ 点击时:
        this.titleEdit = true;
        focusControl.requestFocus(tkpTitleInput);
     并给控件加 `.id(...)` + `.defaultFocus(true)`。
   ⚠️ `focusControl` 是**全局声明的命名空间**(ets/component/common.d.ts),
      **不能 import** —— 写成 `import { focusControl } from @kit.ArkUI`
      会报 "has no exported member"。这个坑踩过一次。
   ⚠️ `defaultFocus(true)` 是兜底:个别机型上 requestFocus 会因
      "节点刚创建"而落空,defaultFocus 能让它自己拿到。
   备注同改(id = tkpNoteInput)。

③ 任务标题提到最前
   build() 的 Scroll 里顺序改为:
      titleRow() → statusSeg() → boardChips() → infoRows() → …
   原来它在 infoRows 内部第一个(等于排在状态/看板之后)。

④ 字号统一
   `fieldLb` 从 fontTiny(11) 改成 **fontSmall(13)**,
   与 `FbSectionTitle` 内部字号完全一致(那个组件是 fontSmall)。
   上一版标签比区块标题小一号,看着不成套。
   现在统计:状态/分组(看板)/协作人/子任务/附件/留言 六个 FbSectionTitle
   全部 fontSmall,任务标题/截止/负责人/备注/分类/循环 六个 fieldLb
   也全部 fontSmall ⇒ 11 处标题字号统一。

编译通过,已装真机。
2026-11-05 19:45:17 +08:00
laoyang 28c6e674ba 任务面板:去掉左上圆圈、标题下移成可编辑的「任务标题」(#77)
用户 2026-11-05:「还是不对,这个弹出框的标题是编辑任务,台式机采购这样的
东西,它是一个标题,也应该可以被修改才对。所以它应该往下放,就是任务标题;
然后台式机采购计划也往下放,也是可以修改的。左上角那个标志,好像一个圆圈
什么的,那个也不要了,去掉。」

① 删掉左上角的 FbCheckCircle("圆圈")
   它是"点一下勾完成"的快捷入口,但下面「状态」区已经有三个明确的 chip,
   入口重复。而且它紧贴标题左侧,视觉上像"标题前面的项目符号",
   反而让人以为标题前面该有个图标。

② 大标题从顶栏下移到「更多信息」区,成为第一项「任务标题」
   原来标题画在 headBar 里,所以它是**只读**的 —— 而标题本来就该能改。
   现在与截止/负责人/备注同形态:标题行 + 控件行,
   点一下变成 TextArea(height 64、字号 fontTitle 17、主题色),
   失焦自动保存(与备注同一套交互,不设保存按钮)。
   ⚠️ 空标题不发请求:服务端会清洗成空串,等于把标题删了。
   ⚠️ `saveField(title, ...)` 走的是既有 PATCH —— 服务端 ApiHandler
      对未提交字段回退原值,所以只传 title 不会把 note/due_at 抹掉。

③ headBar 只剩「状态徽标 + 关闭」,左端放「属于「父任务」」
   父任务那行保留在顶栏不跟着下移:它是上下文不是内容,
   跟标题走反而离"父任务"这个概念更远。
   没有父任务时用 Blank() 占位,保证徽标和关闭仍在右边。

④ 完成态标题仍是 faint + 删除线(不上主题色)
   删除线是"这件事已经过去了"的信号,再上主题色会和待办标题一样抢眼。

编译通过,已装真机。
2026-11-05 19:41:14 +08:00
laoyang 2fefacccd3 任务面板字段改上下两行 + 标题主题色 (#77)
用户 2026-11-05:「还是不对,你还是没有看清楚那个样式的设置:上面的标题和
下面的输入框,其实要分成两行。比如说,"截止"不是直接后面排一个日期,
而是把日期输入框放在"截止"下面;"负责人"也是,"备注"也是,全部都是这样的。
上面"台式机采购计划"这样的标题,应该是主题的颜色。」

前两轮(#77i / #77j)我都在改**颜色和字重**,方向偏了 ——
用户要的是**布局**:标签一行、控件另起一行。

① fieldLb 去掉 width(48)
   上一版把它定成 48vp 固定宽的行内标签,形态是「[截止] 2026-11-07 18:30」,
   与要求的上下两行正好相反。
   现在 width(100%) + textAlign Start,独占一行;
   字号从 fontSmall(13) 降到 fontTiny(11):上下两行之后标签再和控件同号
   会抢视觉重心。

② infoRows 重写为 Column({space:14}),每项 = Column({space:6}){
     标题行 fieldLb(...)
     控件行(height 40 / inputBg / radiusS / 左右 padding 12)
   }
   控件行统一给 height(40):不给的话「设置…」「未指定」这类短文字
   会把框压扁,几行之间的间距就参差不齐。
   负责人那行的头像从 22 调到 24(框内 40 高,22 略嫌小)。
   备注:只读态改 constraintSize({minHeight:40}) + maxLines(3),
   避免长备注把框撑成一大片;编辑态从单行 TextInput 换成
   TextArea(height 76)—— 备注本来就是多行的,单行输入框里
   看不见换行,用户会以为只能写一句。

③ kvRow 同步改成上下两行
   「分类」「循环」若还左右排,夹在截止/负责人/备注中间会显得错位。

④ 面板标题改主题色
   Text(this.titleOf()) 从 FbColors.ink + Medium 改成 this.accent + Bold。
   ⚠️ 完成态仍保留 faint + 删除线:那是"这件事已经过去了"的信号,
      再上主题色会和待办标题一样抢眼,反而分不出轻重。

编译通过,已装真机。
2026-11-05 19:37:08 +08:00
laoyang b42e657bb2 标题颜色/看板/截止/负责人/备注/协作人/留言头像 全部修正 (#77)
用户 2026-11-05:「还是不对,标题的颜色不对。分组看板那个地方没改,截止没改,
负责人没改,备注没改,协作人没改。评论那个地方的头像怎么又没了?」

逐条查下来,四个真问题:

① 上一轮只改了 3 个标题,漏了一大片
   只换了「子任务/附件/留言」,而 **看板、状态、协作人**、
   以及「更多信息」里的**字段标签**全是手写的 fontTiny + faint。
   ⇒ 看板/状态/协作人 补 FbSectionTitle(Bold + accent + 字距 0.6)。
   ⇒ 截止/负责人/备注/分类/循环 这些**行内字段标签**新增 fieldLb() Builder:
     同样取主题色 + Bold + letterSpacing,但宽度 48、无内边距
     (不能直接用 FbSectionTitle:它 width 100% + padding,会把右侧控件顶下去)。
   ⚠️ 这与「编辑表单」的口径不同:那边 `期限` 仍是浅灰小字。
      用户明确要求这个面板里字段标签也用主题色粗体 —— 按用户要求走,
      已在 fieldLb 的注释里写明这个差异,避免以后有人"统一"回去。

② 留言头像丢失的真因
   不是数据问题,是 `FbPersonChip` **不透传 imageUrl**。
   它内部的 `FbAvatar({ name, color, dia, fontSizeVp })` 漏了这个参数,
   而 `FbAvatar` 自己是支持的(注释里还写着"800 人里只有 19 人有头像,
   有图显示图、没图回落色块字")。
   ⇒ 上一轮我换成 FbPersonChip 后,原来手写 FbAvatar 时**能显示的头像反而没了**。
      给 FbPersonChip 补 `@Param imageUrl` 并传给内部 FbAvatar,
      调用点把 `fbImageSrc(c.avatar, token)` 传进去。

③ 状态区原来没有标题
   三个 chip 直接飘在那(上下每个区段都有标题)。补 FbSectionTitle(状态)。

④ 协作人标题改组件
   顺手把「+ 共享」按钮挪到标题下面单独一行(FbSectionTitle 是 100% 宽,
   放同一个 Row 里会把它压成 0 宽)。

编译通过,已装真机。
2026-11-05 19:34:12 +08:00
laoyang 09f7878994 样式改为走规范组件 FbSectionTitle / FbPersonChip (#77)
用户 2026-11-05:「样式还没变,还记得咱们之前规定的样式是怎么做的吗?」

找到了。规定就在 entry/src/main/ets/components/Widgets.ets,
而且 FbSectionTitle 的注释里**留着用户原话**:

  「"今天的日程",既然是日程,标题颜色就应该用日程的主题色。
    …下面的"近期任务"改法跟上面一样,还有"最近纪念日"也不说了,全都是一样的。」

上一轮我做错的地方:**手写 Text 改 fontColor**,没走组件。
而 FbSectionTitle 给的是三样东西:
  · fontWeight: accent 存在时 Bold(不是 Medium)
  · fontColor: accent
  · letterSpacing: 0.6
我只改了颜色 ⇒ 字重和字距还是老样子 ⇒ 用户看着"样式没变"。

改动:
① 子任务 / 附件 / 留言 三个区块标题 → 全部换成 FbSectionTitle
   传 accent: this.accent(模块主题色)+ extra(数量/进度)
② 留言人 → 换成 FbPersonChip
   网页端留言人用的就是 person_chip()(头像 + 名字的胶囊),
   鸿蒙端对应 FbPersonChip。上一轮我手写的"头像 + 纯文字名字"只是长得像,
   不是同一套。换掉后同时删掉外置的那个 FbAvatar(组件内置了,会画两遍)。

🔴 踩到一个组件约束:FbSectionTitle 是 `width(100%)` 的独立组件,
   塞进 `Row` 里会和后面的计数/按钮抢宽度,被压成 0 宽。
   ⇒ 三处都改成**独占一行**;「已完成 2/5」这类附加信息走它自带的 extra;
      按钮(+ 添加附件)另起一行右对齐。

编译通过,已装真机。
2026-11-05 19:29:39 +08:00
laoyang 32a35ca222 留言底部被裁 + 留言支持附件 (#77)
用户 2026-11-05:
  「留言输入框下面还有东西,现在被挡住了,根本看不见,也滚不动了。
   但下面应该还有字什么的,看看是什么。」
  「留言是可以需要添加附件的。」

① 被挡住的是什么
   是 #77 刚加的「回车发送」那行小字。

   根因:`commentSection()` 是 `Scroll` 里**最后一个**子元素,
   而 Scroll 内容末尾只留了 `height(6)`。
   Scroll 本身是 `flexShrink(1)`(内容不够高时按内容收缩),
   最后一行下边距一旦小于"卡片 padding + footBar 高度",
   就会被判定为"已到底" —— 滚不过去、也看不见。
   ⇒ 底部留白从 6 改成 28。

② 留言带附件
   服务端网页端早就有(tasks/comment_add 里的 attach_save_files),
   但 **API 的 comments 端点只收 content**,且空文字直接 422
   ⇒ 端上根本没有传附件的入口。三处一起改:

   · app/ApiHandler.php:comments POST 支持 multipart
       - 有 file 就走附件流程:先 addComment 拿 cid,再 Attachment::save
         (顺序不能反:save 要 owner_id,cid 是插入后才有的)
       - kind 用 task_comment,与网页端 attach_save_files 同一归属类型
         (不同的话同一条留言的附件在两端互相看不见)
       - **允许只发附件不写字**:自动补占位文案「(附件)」,与网页端同口径
   · FbTaskRepo.addTaskCommentWithFile():手拼 multipart
       - 不能复用 fbUploadOne(那个是"一个文件 + 一个字段",
         评论要同时传 content + file)
       - 文件段必须带 Content-Type:服务端按它判"是不是图片"
       - boundary 之间用 \r\n(用 \n 有些解析器会把整段当一个字段,
         表现为"文字上去了、附件不见了")
   · FbTaskPanel:
       - 「+ 添加附件」按钮(DocumentViewPicker,任意文件)
       - 选中的文件名显示成 chip,可点 ✕ 去掉
       - 附件**不立刻上传**,随留言一起发(与「任务附件」那块语义不同:
         那个传完就生效)
       - 发成功后清空待发附件,否则下一条会重复带上

   实测(模拟上传链路):
     只有文字          => cid=18 ✅
     文字 + 附件        => cid=19,附件数 1 ✅(key=files/6b4e...txt)
     只发附件(占位)   => cid=20 ✅
   测试数据已清理。
2026-11-05 19:25:32 +08:00
laoyang ad25792732 选人改「只留搜索框」+ 附件用系统打开 + 留言头像与主题色 (#77)
用户 2026-11-05 五项。

① 选人慢 → 未输入关键词时不渲染任何人
   用户:「实在不行,我们就不把人列出来了,只留搜索框…输入拼音首字母就可以找到这个人」

   慢的根因(不只是节点多):
     · 名册 **800 人**(不是 8 个);
     · 每人一个 FbAvatar,而它内部调 fbImageSrc(avatar, token) ——
       那是**同步**查本地缓存 + 拼 URL,800 次全在打开弹窗那一帧跑完;
     · 再加 800 个 Column/Text 节点参与布局。
   ⇒ 未输入时一个候选都不渲染,只显示"输入名字或拼音首字母找人"。
     只有真打了字才过滤、才渲染(结果通常 1~20 个,瞬间出来)。

   ⚠️ 没改成"只显示前 N 个":800 人里翻前 20 个没用,用户还是得搜。
   ⚠️ 暂存区(已选那几个)不受影响:数量极少且必须一直可见
      (否则用户忘记选了谁)。多选 + 多次搜索的流程因此保持完整:
      搜一个 → 点选进暂存 → 改关键词再搜 → 再点选 → 确定。

② 附件:Word 等非图片文件交给系统应用打开
   用户:「应该打开本机上默认能够打开这个文件的应用,让它把文件打开才对。」

   项目里**已经有** common/FbAttachOpen.ets(下载明文到 cacheDir +
   隐式 Want + action.viewData 交给系统「打开方式」),只是任务面板没调用。
   ⇒ 附件缩略图挂上 onClick:
       · 图片 → 就地全屏预览(新加浮层,ImageFit.Contain 看全图不裁边)
       · 其它 → fbAttachOpen(),系统自己找能打开 .docx 的应用
   ⚠️ 不能用「显式 Want + 包名」:不知道用户装的是 WPS 还是 Office。
   ⚠️ 预览用 Contain 而非 Cover —— 缩略图用 Cover 对,预览裁边就错了。

③ 留言人头像
   服务端 Task::comments() 的 SQL 里**一直有** `p.avatar`,
   端上 parseTaskComment 只取了 name/color ⇒ 头像从没显示过。
   补 RawTaskComment.avatar + FbTaskComment.avatar + 解析,
   UI 换成公共 FbAvatar(有图用图、没图退回首字色块)。

④ 留言标题/发言人用模块主题色加粗
   原来发言人是 muted 灰、常规字重 —— 漏了全站「以模块色标识」的规矩
   (观影紫 / 旅程绿 / 任务赭红)。改成 this.accent + FontWeight.Medium。
   同时把「附件」「留言」「子任务」三个小节标题也从 ink 改成主题色。

   ⚠️ 正文**没有**跟着改主题色:整段上色会掉可读性(色彩需要留白)。
      如果用户要求正文也变色,改一行即可(已在代码注释里标了位置)。

⑤ 编译通过,已装真机。
2026-11-05 19:19:57 +08:00
laoyang 4e2fbb04b7 鸿蒙任务面板:看板平铺、截止负责人备注可改、文案与图标 (#77)
用户 2026-11-05 对鸿蒙端首页任务弹窗提了 12 项,本批完成 9 项
(附件预览/下载单独做)。

① 看板平铺,不再左右滑
   参考 FbTripNodeSheet 的做法:≤6 项 Flex(wrap) 换行平铺,>6 项才横滑。
   当前家庭 4 个看板(默认 + 正在进列/等待反馈/等待结果)⇒ 走平铺。
   ⚠️ 平铺必须用 Flex(wrap),ArkUI 的 Row 不换行,塞不下会横向溢出
      父容器(v76 真机踩过:最后一个 chip 被裁掉且点不到)。
   ⚠️ Row 的 space 在 Flex 里不生效 ⇒ chip 补了 margin 做间距。

② 删掉截止下面重复的「看板」行
   上面 boardChips() 已经把当前看板高亮出来了,再列一行纯属重复,
   而且两处都要同步才不出错。

③ 截止日期可改
   点整行 → FbDateTimePicker(与编辑表单同一个组件,交互一致)。
   改完立刻保存,不设保存按钮 —— 与状态/看板/子任务同一套交互。

④ 负责人可改 + 头像
   点头像/名字 → FbPersonPickSheet 单选。
   头像来源:服务端 Task::find 的 SQL 里**一直有** a.avatar AS assignee_avatar
   (实测 #49 返回 avatars/2026xxx.jpg),只是端上 parseTaskItem 从没解析
   ⇒ 补 RawTask 字段 + FbTaskItem.assigneeAvatar/assigneeColor + 解析。
   没头像时用首字色块兜底(与留言区同一手法)。

⑤ 备注可改
   点一下变输入框,回车或失焦自动保存(等用户去找"保存"按钮
   就会以为没存上)。

⑥ 「共享给谁」→「协作人」(标题与弹层标题都改)

⑦ 「+ 加图片」→「+ 添加附件」,**行为也跟着改**:
   原来 pickAttach 用 PhotoViewPicker + IMAGE_TYPE 只能选相册图片,
   改叫"添加附件"却选不了 PDF/Word,用户会以为功能坏了
   ⇒ 换成 DocumentViewPicker(系统文件选择器),能选任意文件。

⑧ 留言区标题加 💬 图标

⑨ 「回车发送」从 placeholder 挪到下方独立小字
   (塞 placeholder 里一打字就看不见,而且 placeholder 该放"填什么"不是"怎么操作")

⑩ 发送按钮显示不全
   根因:写死 width(56) + textAlign(Center),Text 默认内边距吃掉两侧空间,
   56 减去内边距 + 两个汉字正好压线,字体缩放或不同机型就截掉半个字。
   ⇒ 改 constraintSize({minWidth:60}) + 水平 padding,按文字实际宽度自适应。

⚠️ 另一个必须同步的地方:FbTasksPage 里改状态时会**整条替换** FbTaskItem,
   新加的两个字段漏了就会被抹掉(改一次状态头像就没了)——已补。

编译通过。
2026-11-05 19:12:24 +08:00
laoyang 5fba8c4eda 修导航目的地前面多出问号 (#69)
用户 2026-11-05:「导航的,点进去导航之后,为什么目的地地址前面有个问号」

根因:我上一轮拼的是 `lat,lng?地名` —— 那个 `?` 是照抄 `geo:` URI 的写法,
但这个字符串**不是 URI**,是交给 fbOpenNavigation() 的普通文本。

FbNav.parseGeoOf() 的动作:
  ① 正则抓 `数字,数字` 当坐标
  ② clean = 把坐标从原文剪掉后剩下的文字,用作地名

传 `36.168977,120.375693?青岛北站` 剪掉坐标后剩 `?青岛北站`
⇒ 问号跟着进了高德 dname,目的地标题显示成「?青岛北站」。

改成「地名 + 空格 + lat,lng」:
  `青岛北站 36.168977,120.375693`
剪掉坐标后剩「青岛北站」,正是要显示的名字。

实测(复刻 parseGeoOf 跑):
  旧 lat,lng?地名  → clean=[?青岛北站]   ❌
  新 地名 lat,lng  → clean=[青岛北站]    ✅
  只有地名         → ok=false 走地理编码  ✅

两处都改了:
- FbEventSheet.navTargetOf()(#69 新增的)
- FbDayEventPanel 行程卡片的 onClick(#67 里同一个错误)
2026-11-05 17:22:36 +08:00
laoyang ad9b934760 日程详情卡:补齐行程的出发地/目的地 (#69)
用户 2026-11-05:「点击行程里旅行日历本里的信息时,弹出来的那个框是在首页上
弹出的,还是没有出发地和目的地。」

根因链(三段,缺一段都不显示):

1. FbCalEventFull 没有那四个字段
   #67 只给了 FbCalEvent(列表用),详情卡消费的是 FbCalEventFull。

2. shellOf() 只塞了 location: b.place
   行程节点的 place 是空串 —— 它的位置概念是"从哪到哪",没有单一地点。

3. 最关键:行程条目**永远走 shell,压根不请求详情接口**
   openEvent() 里 `if (!isEditableEvent(e)) { shellOf(e); return; }`,
   行程的 kind 是 trip 不是 custom ⇒ 直接短路。
   而且即使去请求也没意义:Calendar::event($id) 查的是 calendar_events,
   行程节点在 trip_nodes 表,那个 id 查不到东西。
   ⇒ 位置信息**只能**靠 shell 带过去。

改动:

- FbCalEventFull 加 fromPlace/toPlace/fromLng/fromLat/toLng/toLat
- FbCalRepo.toFull() 从接口读(接口给就填,普通日程取到空串)
- 首页 FbHomePage.shellOf()、日程页 FbCalendarPage.shellOf() 两处传递
  (两处是独立实现,只改一处会漏)
- FbEventSheet:
  · navRow 改 navRowRaw(label, place, navTarget) —— 标签不再硬编码「地点」
  · onNavigate 从无参改成 onNavigate(navTarget)
    原来宿主只能读 event.location 猜导航目标,无法区分出发地/目的地
  · 渲染三分支:有 fromPlace 画「出发地」、有 toPlace 画「目的地」、
    有 location 画「地点」(三者互斥:行程条目只有前两个,普通日程只有第三个)
  · 新增 navTargetOf(place, lng, lat):有坐标返回 `lat,lng?地名`
    (同名地点不会搜错城市),没有就只给地名让 FbNav 地理编码
- 首页 navigateEvent / 日程页 navigateDetail 改成接受 navTarget,
  非空直接用,为空退回原来那套推导(底部那颗「导航」按钮传空串)
2026-11-05 16:21:16 +08:00
laoyang f30ea5b1e4 日程行程条目端上同步:字段、列表卡「出发地→目的地」可导航 (#67)
同步服务端 #67 的新字段与三处改动:

数据层
- RawCalEvent / FbCalEvent 加 fromPlace/toPlace/fromLng/fromLat/toLng/toLat
- FbParse 解析赋值;新增 numStr() 把 DECIMAL 可能给的 number 转成字符串
  (`s()` 签名只收 string|null|undefined,坐标字段会报类型错)
- FbCalStore.toModels、DemoData 的 8 条演示数据同步补字段

渲染
- FbDayEventPanel 列表卡片:在时间行**上面**加一行「出发地 → 目的地」,
  只对两端都有值的条目画(普通日程 fromPlace/toPlace 都是空串,不画)。
  目的地用日历本色 + 下划线,点击 fbOpenNavigation:
  有坐标走 `lat,lng?地名`(同名地点不会搜错城市),没有就让 FbNav 地理编码。
  不塞进时间行是因为那里已有「时刻 + 重复徽标 + 地点」,窄屏会挤爆。
- FbDayTimeline 全天块:一行文字「出发地 → 目的地」,替代原来的 place。
  不给点击 —— 26vp 高的块切两个热区点不准,导航入口在详情卡。

图标与 ✓ 的改动在服务端(Trip::leadIcon / 删  ✓ ),端上只消费 title,
所以不需要额外处理。
2026-11-05 16:12:14 +08:00
laoyang c498037ec8 首页标题主题色补完 + 新建去加号 + 全天块上间距 (#60 #61)
用户二次反馈四项,全部是上一轮没做到位的:

1. 标题颜色没变
   上一轮给 FbSectionTitle 加了 @Param accent(默认空串),
   但首页 6 处调用点**一处都没传**。grep 到的 7 个 accent:
   全在 FbEventSheet/FbTaskPanel/FbDayEditSheet/FbMovieDetailSheet/
   FbPersonSheet 上 —— 那些组件本来就自带这个参数,与标题无关。
   空串 ⇒ titleColor() 退回 #8A7A6D 灰 ⇒ 组件改得再对也不变。
   补 5 处:日程/任务/纪念日/观影/联系人各自 fbModuleOf().brand。
   往年的今天不传(混合来源,没有单一模块色)。

2. 条数与"新建"贴死
   extra 后面直接接胶囊,Row 无 space、胶囊无 margin。
   加 margin({left:12})。不用 8:胶囊有实体底色,视觉比纯文字更紧。

3. 去掉「+」前缀
   5 处 addText 去加号;连带 3 处空态提示的「点右上角「+ 新建」」
   同步改掉,否则提示里带着按钮上已不存在的符号。

4. 全天日程问题在上面不在下面
   上一轮改 bottom 6->12,方向反了。
   FbDayEventPanel 给 FbDayTimeline 的 padding 没有 top
   ⇒ 第一个全天色块上边直接压在白卡片顶边上。
   改成 {top:10, bottom:12}。只加在有全天的分支里。
2026-11-05 15:52:40 +08:00