Commit Graph
18 Commits
Author SHA1 Message Date
laoyang 4b58fec796 v82: 首页卡片补「新建日程 / 新增成员」+ 两处红线
用户原话:「日程,没有新建日程按钮,名册,也没有新增成员这个按钮。
另外,首页上的日程,红色线不对,变短了左侧缺了一块。在日程模块中,
那个红线左侧是不是还有一个红点?如果有,去掉。」

首页卡片就地操作(不用进模块):
- HomeCards.php 的 agenda/contacts 补 add + add_modal 元数据
- templates/calendar/_add_modal.php:从 calendar/index.php 抽出的
  「新建日程」弹框片段(首页与日程页共用;片段自读变量一律加 cal 前缀,
  避免 require 共享作用域覆盖宿主变量)
- templates/people/form.php:加弹框形态(pfModalOnly / pfModal),
  弹框形态下「取消」走 data-fb-add-close
- index.php:首页 agenda/contacts 两个 $homeAddModals 分支;
  calendar/event_add 支持 back=home 白名单跳转(不采信任意 POST 字符串)

红线(左端缺一块 / 多余红点):
- .ag-now 原来写 left: var(--ag-gutter,50px),但 v80 起它已经在
  .ag-lines 内部(那层自己已有同样缩进)⇒ 左沿跑到 100px、比车道多
  出一整份刻度宽。改成 left: 0
- .tg-now::before 整条删除(日程模块页那个 8px 红点)

涉及共用件(HomeCards/index.php),已跑全量:e2e 1572/0、
v82 正向 20/0、v82 反向 15/0、流水线全绿并已部署。
2026-10-24 04:25:02 +08:00
laoyang b45cf671d1 v71c:首页「通知消息」改成正规卡片,样式直接复用通知页
用户 2026-10-21:
「先把web端的首页修一下,通知消息这个,也是首页的卡片,可以调整顺序和大小。
 另外,这个样式也不好看,我看个人头像点击后,通知消息那个页面的样式就不错,
 直接复用就可以了。」

① 卡片化(HomeCards 注册表 + 默认布局 + 取数)
   · TYPES 新增 'notif'(w4/h4,route=notifications,more=全部通知)
   · DEFAULT_LAYOUT 钉在 brief 之后,与 days 并一行(宽 4)——
     若插在 brief 与 agenda 之间,agenda/tasks 会被挤成一上一下(实测 C13 顶边 407 vs 751)
   · data() 加 case 'notif' → 新增 notifData():最新 10 条 + 未读数,
     与原来的取数口径一字不差;关掉卡片就不查这两条 SQL
   · templates/home.php 删掉网格前那块写死区块(v30 第 5 条「必须在往年的今天前面」
     这条约束作废 —— 那时还没有卡片体系);index.php 不再单独传 notifs/notifUnread

② 样式复用 templates/notifications/index.php
   · 结构与通知页完全一致:头像在前 → 正文 → kind 图标在右(带 ntf-kind-{kind})
     (旧首页那块是"图标在前、头像第二",正是用户说不好看的那一版)
   · 新增 .hc-ntf 作用域只补"卡片比整页小"的三条差异:正文允许折两行(宽 2 时
     单行省略会把正文全吃掉)、时间挪到正文下一行、未读数做小注脚
   · 不带每条右侧「删除」按钮(4×4 的卡里塞两个按钮会把正文挤成两三个字),
     删除在通知页做,卡片右上角有「全部通知」

③ 顺带修掉 v63 A1 抓住的一处:'notif' 的 mod 给 'home' 而不是空串
   (通知不在 Module::LIST,但它是首页自己的卡片;空串会让「全部通知」成为
   一排同色模块按钮里唯一白底 ghost 的那个)
   ⇒ TYPES docstring 立规矩:route 非空就必须有 mod,认不出归哪个模块就归 home

验证(familybook_push.sh 第 6 轮全绿,PUSH_RC=0,2h11m)
   v62 首页卡片 56/0 · v62 反向验证 14/0(新增 M8–M12 五条注入)
   b30 通知 16/0 · b30 反向验证 18/0
   v63 40/0 · v63 反向验证 51/0 · e2e 1523/0 · 部署静态断言空跑 RC=0

同批带上上一轮 v70/v71 的日历修复(已单独验过,本轮流水线一并回归):
   · v70 日历导入后条目不显示:+N 条从纯文本 span 改成真按钮,点开是当天全部
     条目浮层;AJAX 新建后就地插入与服务端同形(回 entry,不让前端自己拼)
   · v71 ICS 导入顺序:主事件 → 覆盖行 → EXDATE 由 Calendar::importIcs() 统一保证
2026-10-21 13:22:37 +08:00
laoyang 4fbac180e1 v51:旅程编辑保存后刷新 / 名册编辑页可改标签 / 手机号归一 / 反复点击带入时间+默认1h+分栏 / 日程设置拆分
用户 2026-10-15 六条。

① 旅程详情「编辑」保存后页面不刷新(改了题图/标题,页头纹丝不动)
   **两个 bug 串在一起**:
   A. 只换 `#nodeListHost`(行程列表),而标题/题图/时间城市都在那容器**外面**(服务端直出);
   B. 🔴 `form.id === 'tripForm'` **永远为假** —— HTML 表单把**同名具名控件**挂成自己的属性,
      `tripForm` 里有 `<input name="id">` ⇒ `form.id` 返回的是**那个 input**
      (`String(form.id)` = `[object HTMLInputElement]`)。同类还有 form.name/action/method。
   ⇒ 修法:`form.getAttribute('id')` 判型;tripForm(编辑旅程本体)→ **整页刷新**,
      nodeForm(行程增删改)→ 仍只换行程列表(那是 v41 用户明确要的"不整页跳")。

② 名册编辑页不能选标签("应该能编辑这个人的所有信息才对")
   `templates/people/form.php` 里压根没有标签 UI ⇒ 新增 `#ptBlock`:
   候选勾选(PersonTag::cloud)+ 自由输入(`name="tags"`,唯一提交源,勾选框只是快捷方式)。
   ⚠️ `PersonTag::setFor(家庭, **我**, 他, …)` —— owner 永远是"我",不影响全家的标签。
   ⚠️ 守卫按**字段存在与否**分流(array_key_exists),不是"值是否为空"——否则改自己时
      表单没这个字段,按"空值=清空"会把人家的标签抹掉。
   ⚠️ 校验失败的两条重渲染路径也要带上 `myTags`/`tagChoices`(取 `$_POST['tags']`)。

③ 导入的手机号带空格 → 编辑时被判"格式不对"("统一处理一下")
   真根因:主手机号正则 `^\+?[0-9\-]{6,20}$` **空格一律判错**,而补充电话那条收 `[0-9\- ]`
   ⇒ 同一个号码填主字段报错、填补充字段通过。
   ⇒ ① `Person::normPhone()` 去空格/短横/括号/**全角空格 U+3000**;
      ② 校验改成「先归一再判纯数字」——⚠️ 只把正则放宽到含 `\s` **不够**:
         PCRE 的 `\s`(无 /u/UCP)**不认全角空格**,通讯录导出里真有;
      ③ create/update/replaceContacts **入库前归一**(原来 hash 内部归一但**密文存原样**);
      ④ `Schema::ensure` 加**一次性迁移**(哨兵 `phone_normalized_v51`)把已入库的
         带分隔符号码解密→归一→写回(手机号是加密存的,SQL 里没法 LIKE/REPLACE)。

④ 周/日视图"只有第一次带入的时间是对的"
   真根因(真机给 `__fbRemount` 装 getter/setter 才抓到):起止字段藏在弹框里 ⇒
   boot 时 `upgradeRange()` 因"两端不可见"提前 return ⇒ 那一刻**只有 mountAntd** 给它赋值
   (会"重读原生值+重绘 antd");而**一打开弹框** `fbUpgradeRange()` 补跑 ⇒
   在尾部把它**重新赋成"只对齐原生下拉、不碰 antd"**的一版 ⇒ 此后 antd 控件永远停在第一次的值。
   ⇒ `upgradeRange()` 不再降级覆盖更强的实现,改成"把对齐下拉串在它前面"(`__fbRangeSync` 防自套)。

⑤ 默认结束 = 开始 + 1 小时;新增日程要按分栏错开(不再叠盖)
   · `setStart()` 里顺带设 ends_at = +1h(⚠️ 跨午夜顺延到次日;⚠️ 不捆进 fbSetDT,
     那个接口被"编辑已有日程"复用,强改会弄丢用户填的时长)。
   · 真根因:`putTimed()` 只把自己那条**整宽**铺上去,不知道同列已有日程
     (服务端整页渲染有分栏,但那是整页重绘时才算的)⇒ 前端 `relayoutColumn(col)`
     **照抄服务端 `conflictLanes`** 重排整列;模板加 `data-s`/`data-e`(**声明的起止分钟**,
     不能从 top/height 反推 —— 那是"实际显示结束",不是真实结束分钟)。

⑥ 日程设置下拉太长(日历本一多就把下面的设置挤出屏幕)→ 拆成"四行列表 + 显示设置独立页"
   新建 `templates/calendar/display.php` + 路由 `calendar/display`(默认视图 + 显示哪些本子);
   `_settings_menu.php` 从"内联表单"改成**纯列表四行**(显示设置/日历本管理/订阅管理/DAV 口令);
   ⚠️ `.cal-set-books` 原本 `max-height:42vh`("塞在下拉里"的妥协)⇒ 用 `.cal-set-page`
      作用域覆盖,否则整页里再套一层内滚 = 要滚两次。
   保存仍走既有 `calendar/prefs`(一个字段没改)。

验证:`_fb_v51_ui.py`(25 条真机)+ `_b51_falsify.py`(**12 注入 / 25 断言全抓**);
e2e 1511/0;`_fb_batch_e.py` 与 `familybook_deploy.sh` 里"设置菜单含 show_present"等
旧断言随 #6 搬到 display 页(意图一字不改);全流水线 PUSH_RC=0、部署 0 FAIL(含"第51批形态");
线上 10 文件 md5 逐字节一致。

⚠️ 排查纪律(已写进 pitfalls.md):
 · `form.id` 被同名控件遮蔽 —— 判型一律用 `getAttribute('id')`;
 · 反向验证的注入可能"打偏"(改的那处不是探针实际走的分支)——
   本轮 12 个注入有 2 个一开始没变红,都是注入打偏、不是断言弱。
2026-10-15 10:37:07 +08:00
laoyang 6ee5662854 v48:日历本共享面板提速(×日历本数 → 唯一一份)/ 点日历带入开始时间真修 / 去十字光标
用户 2026-10-14 三条。

① 日历本共享弹框「特别慢」—— 真根因 = 选人组件 ×日历本数
   用户原话:「设置日历本共享的时候,弹出框特别慢,好像又是 bitwarden 的问题,
              需要再彻查一下。」
   线上门禁诊断页实测(真实登录态):
       cals=12, roster=800, pp_components=12, html_kb=5876.5,
       json_attr_kb=1586.8, noscript_kb=4137.8, inputs=9657, labels=9593
   服务端只花 ~70ms ⇒ **慢在浏览器侧**。本地夹具造出同规模复现:
       首屏 4086ms / 整页 9.3MB / noscript 文本 6.7MB。
   真根因:calendars.php **每本日历**都内联一整套 _person_picker,每个组件把
   **全部 800 人**化成 1 份 data-pp-people JSON + 1 份 <noscript> 兜底 ⇒
   12 本 = 12 份 ⇒ 近万个表单控件,浏览器/密码管理器逐个扫描。
   ⚠️ **不是 Bitwarden 独有**(用户怀疑它)——任何遍历 input / 读整页 HTML 的扩展都会中招。
   修法:与账目页 #ledPanels / 名册 #rosterMenus 同一套(同类问题第 3 次)——
   行上只留 [data-cal-share-open] 按钮,真表单全页唯一一份 #calSharePanel。
   线上复验:html_kb 5876.5 → **526.6**、pp_components 12 → **1**、noscript 6.7MB → 0.34MB。

② 点日历带入开始时间失效 —— 真根因是 window.fbSetDT 被提前 return 吞掉
   用户原话:「之前这个功能是有的,后来我们改了时间控件之后,这个功能就没有了。」
   两层真根因(都在 assets/js/app.js):
     · fbSetDT 定义在 `if (!hasMonth) { … return; }` **之后**(IIFE 尾部)——
       日程页首屏没有 input[type=month] ⇒ 直接 return ⇒ 尾部赋值**永不执行**
       ⇒ setStart() 里 typeof window.fbSetDT === 'function' 为假。(与 v42 修
       fbBootAntd 是同一家族:对外接口写在提前 return 之后 ⇒ 静默失效。)
     · bindParts() 初始化是"以三个下拉为准"(rebuildDays(true); sync();),而
       __fbSyncParts 这个**赋值**写在它们之后 ⇒ 点击带入的 dt-date 被旧下拉盖回。
       真浏览器调用栈实测:set date=2026-10-10(setStart)紧跟 set 2026-10-14(sync←bindParts)。
     · 第三层:group.__fbRemount 是 v35 的空壳,且 fbSetDT 查找被 data-semi-range-done
       门住 —— 日程「结束」默认隐藏 ⇒ upgradeRange 早退、永不设该标记 ⇒
       **可见的 antd 控件永不重绘**(隐藏字段对了、用户看到的还是旧值)。
   修法:① fbSetDT 提到所有 return 之前;② bindParts 初始化改成**以权威字段 date
   为准**(box.__fbSyncParts(); sync();,且赋值顺序提前);③ antd RangePicker/单点
   控件改**受控**(value+状态)、__fbRemount 接成"重读原生值+重绘"、查找不再要求
   data-semi-range-done。真浏览器实测:**可见控件**显示 2026-10-10 09:00 / 周视图 15:00。

③ 去掉十字光标
   用户:「现在鼠标放在视图上时,变成了一个十字。我感觉这个没必要。」
   删 app.css 的 cursor:crosshair 规则;`.rt-writable` 类名**保留**(它是"能不能写"
   的 DOM 标记位,_fb_batch_e.py 断言依赖它)。点击建日程的能力**一字不动**。

验证(新增 3 个套件 + 1 个反向验证):
 · _fb_v48_cal.py(12 条,含"**可见的** antd 控件显示值"两条)
 · _fb_v48_picker.py(9 条,造 12 本 + 800 人规模,量首屏耗时/整页体积/picker 数)
 · _fb_v48_share_e2e.py(4 条,共享真落库 + 权限正确)
 · _b48_falsify.py(**6 个注入 / 13 条断言全抓**,含"antd 改回非受控"、
   "不接 __fbRemount"、"重现第二份选人组件")
 · 全量 e2e 1510/0、vCard 40/0、几何 428/0、v38/v39/v40/v47 反向验证全绿
 · 全流水线 **PUSH_RC=0**;线上四文件 md5 与本地逐字节一致;
   线上门禁诊断复验三条修复全部 YES,临时诊断页已删并复验 404

顺带修掉的**工具链自身缺陷**(详见 pitfalls.md):
 · familybook_deploy.sh 有 8 处 `_code_only … | grep -q`,在 `set -o pipefail` 下
   会因 SIGPIPE(141) **假红**(本轮缩短了 calendars.php 上半段就触发)。
   新增 `_code_has <file> <str>`(先落盘再 grep,不走管道)全部换掉。
 · v40 那条"数据丢失级"静态守卫盯的是老字面量 `rebuildDays(true)` —— 改成
   接受两种安全形式,行为层最终判据仍在 _fb_b40_movie.py(全绿)。
 · 更新 4 个反向验证脚本里因本次重构而失配的锚点(b38/b39/b40 + 新增 b48)。
2026-10-14 14:30:37 +08:00
laoyang 075ac6bbeb v44 第 44 批:界面 5 条 + 移动端 API v1 全量
界面(用户 2026-10-12 第 1~5 条)
1. 新建日程「日历本」并入 .cs 自绘下拉,颜色画进候选项,三字与下拉同行。
2. 新建日程「开始/结束」改「日期范围:」,删旧日期控件;
   重复相关三行仅选重复时出现;删「(含第一次)」与「这段时间算忙碌」。
3. 条目浮层加「地点」、去掉「状态」行;首页红线按分钟定位(原只取整小时);
   去掉左侧红点只留红线。
4. 任务页删掉状态说明提示(STATUS_HINT 三值清空)。
5. 新建任务:标题在前树框在后;截止时间改最新日历控件;负责人默认自己;
   删冗余提示;「上面要做什么」→「任务名称」;清理括号说明。

移动端 API(用户第 6 条:导航模块除外,其余全量)
- 新增 Bearer 令牌鉴权(api_tokens 表,Schema v32),与登录口令 / DAV 口令三套分立。
- Api.php 内核(鉴权/取参/信封/分页/分发 + _method 覆写)+
  ApiHandler.php 17 条资源 + ApiToken.php + api.php 入口。
- 路由:router.php / .htaccess / index.php 三处 /api/ 兜底(部署脚本自动追加线上规则)。
- 个人设置新增「App 接口令牌」卡;docs/API.md 接口文档;
  docs/API-QUESTION-LIST.md 自主决策与待确认缺口清单。

验证:本地 e2e 1510/0、v43 真机 33/0、v43 反向 9/0、
v44 真机 41/0、v44 API 63/0、v44 反向 19/0;全流水线 PUSH_RC=0。
2026-10-12 15:31:03 +08:00
laoyang 8c651b43eb v43 第 43 批:六条反馈(界面 5 + 条目详情浮层)
用户 2026-10-11 反馈六条,逐条定位真根因后修复:

① 纪念日「日期」多出一个控件 —— 只保留最新的那个
   两层根因:(a)原生 `input[type=date]` 当初没包在 `.dt-native` 里,
   v42 那条"AntD 挂上后收起原生框"的选择器根本不命中;(b)更要命的是
   `.dt-native` 基类写着 `display: contents` —— 该元素**没有盒子**,
   给它 position/width 完全无效 ⇒ 必须把 display 显式改回 block 才收得起来。

② 编辑纪念日把底层列表页弄没 —— 「修改」原来是整页跳 `days/edit`
   (套 layout = 一张只有弹框的空白页)。改为 `?embed=1` 只吐弹框那一块,
   列表页 JS 取回来替换 `#anNewCard` 并打开 —— 与「新建」同一观感。
   内嵌形态标 `$anModalOnly` 以免打 standalone 标记(关闭时不再跳页)。

③ 拖图裁剪的层被弹框盖住 —— `.crop-overlay` 是 z-index:100 而 `.cal-modal`
   是 200 ⇒ 裁剪层被整块盖住。提到 260(与 v41 的 .cn-modal 同一处理)。

④ 行程弹框里"参与的人"弹不出选人框 —— person_picker.js 只绑定一次,
   而弹框表单是 AJAX 片段 ⇒ 里面的 `.pp` 从未被 init。三件事一起:
   暴露 `window.fbInitPickers()`(幂等)+ layout 暴露 `window.fbLoadPicker()`
   (脚本是按需加载的)+ 塞完片段后补调初始化。

⑤ 日程新建排版/控件/文案 —— 改走观影式 `.mv-grid`(标签对齐 + 冒号);
   起止补上 `$dtPlugin='antd'`(试点正式推广到日程);标签「时间范围」→「开始 / 结束」;
   删三句提示(结束留空 / 多选提醒 / 全天)。

⑥ 条目详情浮层(首页日程 / 首页近期任务 / 日程页条目)——
   点条目不再直接跳页,弹浮层显示详情;「详细信息」跳该来源详情页;
   「编辑」就地换成该来源的编辑表单(四处 `?embed=1` 片段)。
   新增 `templates/_entry_popup.php` + `assets/js/entry_popup.js` +
   `fb_entry_json()`(三个入口共用一个编码函数)。

验证:php -l 157 文件全过;div 深度 33 组合全配平;片段体检 20 片段无越界;
本地 e2e 1510/0;v43 真机 33/0;v43 反向验证 8/0;v38 36/0;v42 33/0;
b31/b32 反向验证 8/0 与 5/0;部署断言本地空跑 RC=0;完整流水线 PUSH_RC=0。
2026-10-12 00:24:20 +08:00
laoyang d83c94e127 v37 第 37 批:日期控件换成「年/月/日 + 时/分」五个原生下拉 + 五处界面重做
用户原话(6 条,一次给全):
  ①「观影详情页面。观影图片下面有一个块…要加照片请点上方「编辑」…这个块不要,
     直接把这个块删除。」
  ②「编辑页面的时间…选日期的这个控件非常非常不好用。就是如果我把年和月那个地方
     拉下来,我选完年和月之后,它并不会回到让我选日的那个界面,它还是不动的…
     要么换一个控件,要么在那个地方加一个确定按钮…现在开始和结束时间分在了两行…
     能放得下的话就应该放一排…不要让它默认显示在两行上…"散场时间可不填。"这句话去掉。」
  ③「首页上今天的日程…分了几列,列之间的空隙非常非常的大…日程里面的那两个色块
     就是紧挨着的。所以…应该让它们紧挨着。」
  ④「现在怎么部署的时间这么长?…你修改了哪些页面,你就只测试哪些页面就可以了。」
  ⑤「日程模块的新建日程…"加上"和"取消"两个按钮在右上角,离得特别远,根本看不到…
     每个输入框前面的标题都太小了…都应该加粗、变大。」
  ⑥「1.新建任务的截止时间,也和观影一致。2.新建任务时竟然没有添加附件…
     3.新建任务也弹出一个框,新建完之后刷新看板页面。4.观影新增也弹框输入所有信息。」

② 日期控件:**真根因是"这个控件给不了"**
  `<input type="date">` 的日历弹层由**浏览器自己画**,我们碰不到 DOM、也控制不了流程;
  Chrome 在"点顶部 `2026年 10月` 切到年月视图"之后,要靠用户**再点一次日期或按 Esc**
  才回得到日期视图,各家还不一致 ⇒ 用户遇到的"选完年月就卡住、选不了日"。
  修:`_datetime.php` 重写成 **年 / 月 / 日 三个原生 <select> + 时 / 分**,
  选完即生效、无需"确定";秒默认去掉(用户"只记录到时和分")。
  ⚠️ 提交契约**一字未改**:`*_date` 仍是真字段(改 `type=hidden`)+ `*_hour/_minute`。
  ⚠️ `required` 落在隐藏控件上会让 Chrome 报 "not focusable"(点保存什么都不发生)
     ⇒ 改用 `data-dt-required` 声明、JS 自查(12 个调用点全部受影响)。
  起止两端:`.dt-range.dt-cols` + `flex-wrap` → **宽屏一行、窄屏自动换行**
  (不用 JS 量宽度:字体/动画/缩放时量不准,flex-wrap 是浏览器布局的一部分)。
  🔴 顺带挖出真 bug:旅程「修正」表单的 `.dt-range` 只有 **22px 宽**
     —— 它长在 `.node-ops`(一排小按钮的容器)里,展开的内容只有"修正"两个字那么宽。
     修:`.rev-panel{position:absolute}` 纯 CSS 浮出来(不动 DOM,避免牵动节点列表排版)。

③ 首页冲突色块"空隙特别大":与日历页逐字对比出来的
  日历页 `left: calc(2px + L%)` / `width: calc(W% - 4px)` → 只留 4px、紧挨;
  首页却写死 `calc(46px + L%)` / `calc(W% - 50px)` —— 每块左右各扣 23/25px,
  两列时那 46px **没法均分**、全挤进了中间的缝(实测 ≈50px)。
  修:`left: calc(46px + L% + 2px)` / `width: calc(W% - 4px)` → 缝恒为 4px。

⑤ 日程弹框"按钮看不到":**两个**根因叠加
  ① `calendar/index.php` 里**多写了一个 `</div>`** —— `.cal-modal-panel` 被提前闭合,
     底栏 `<div class="cal-modal-foot">` 成了 `.cal-modal` 的直接子元素、被挤到面板外;
  ② `.cal-modal-panel` 是 flex 列,但**它下面的 `<form>` 不是** →
     `.cal-modal-body{flex:1 1 auto}` 的上下文只到 form 为止,body 压不下去也不滚。
  另有 `.cal-modal-panel` 的 max-height 没算上 `.cal-modal` 自己的 padding(差 4px)。
  三处都修;标签只对**弹框里**的 `.field > label` 加粗放大(14px/600/深色)。

⑥ 任务新建:内联卡片 → 弹框;**新增附件**(`attach[]`,服务端零改动能收);
  `back=tasks` → 新建完回**看板**刷新。观影新增:列表页内嵌同一份 form.php 弹框。

① 详情页删掉那句提示块。④ 流水线改**增量**:`run_suite_if <标签>` + 从 git 改动
  推模块;**一碰共用件就退回全量**(app.js/app.css/layout/index.php/_datetime.php…),
  认不出来也全量(保守:少跑一段发坏东西的代价远大于多跑十分钟)。

🔴🔴 本轮最值得记的教训:**DOM 嵌套错误比语法错误难查得多**
  在 movies/form.php 与 calendar/index.php 各写坏一次嵌套(多/少 `</div>`、多 `</form>`),
  两处**都通过 php -l、页面也打得开**,症状却是"按钮跑到面板外"与
  "编辑保存静默失效"(保存键落到 form 外面)。⇒ 模板里 `<?php if ?>` 与 `</div>`
  必须**按分支配对**,改完用真机渲染 dump 一次 div 深度;光靠 php -l 发现不了。

验证(全部真机 + 反向验证):
  · _fb_b34_dtrange.py **61/0**(四个页面:年/月/日/时/分 都可见可点 +
    选完三个下拉**立刻**写回 *_date + 改月重建"日"列 + 起止默认同一行 + 无 JS 报错);
  · _fb_b34_dtrange_falsify **7/0**(三处注入各自必须变红);
  · _fb_b37_ui.py **27/0**(详情页无提示块 / 首页冲突缝 ≤8px / 弹框按钮在面板内且在
    视口内 / 标签加粗 / 任务弹框+附件+回看板 / 观影列表页弹框);
  · _fb_b37_falsify **5/0**;
  · _fb_b35_movie_media.py **16/0**、_fb_geom_interact **109/0**、e2e **1503/0**、
    b30_ui2 16/0(含首页冲突分列)、_b30_import 26/0、b18/b21/b23/…全绿、
    DAV 三套全绿、部署静态断言空跑通过;
  · 线上 app.js / app.css / movies/form.php / calendar/index.php 的 SHA256 与本地一致。
  · 顺带修掉 _b30_import 的一个**时序竞争**:它用外部上游(联想词服务)的二次探测
    当对照,浏览器取词在前、探测在后,上游抖一下会误报成"回归"(已改成两次确认)。
2026-10-10 00:26:42 +08:00
laoyang 2a10df2557 v30 后半(5 条):导入框选中反馈+单选 / 首页日程改色块 / 近期任务主题色 / 时间冲突错开 / 通知消息模块
用户 2026-10-08 第二批 5 条。

1. 导入日程选中文件后没有任何反应 + 不允许多选
   `_stage_drop.php` 现在有两种模式:暂存型(附件,拖进来即刻上传)与导入型
   (`$sdStage=false`,写回 input.files、随表单提交、PHP 读完即弃)。
   暂存型框靠 queueDrop 画缩略图,而**导入型框选中文件后什么都不显示** ——
   框里那句「点击或拖动进行上传」从头到尾不变,用户看不出"到底选上没有"。
   改:app.js 新增 bindFileEcho(),只给**非暂存框**补 change 监听,
   选中后框里的话换成文件名 + 图标变 ✓ + `.upbox.is-picked` 稳定态;
   取消选择则还原原始文案(不是空字符串)。单选由 `$sdMultiple=false` 保证。

2. 首页「今天的日程」用色块表示,和日程保持一致
   原来那套是"白底卡片 + 左边一条 3px 色线",色只占一条线,看不出是哪个日历本。
   改:与日历页 `.tg-block` 同款实心色块(底色=日历本色,白字,圆角,无阴影),
   left/width/top/height 全部由内联给出(冲突分列 + 真实时长)。
   ⚠️ `.ag-ev` 的 CSS 里去掉 `right` —— 绝对定位下 left+right+width 三者同时存在是
      过约束,内联 width 会失效(冲突就不会错开了)。

3. 首页「近期任务」名称与圆点走任务模块主题色
   原来用 `var(--brand)` —— 那是**当前页面**(首页)的主题色,不是任务模块的;
   用户改过模块配色时两者不一致(首页砖红、任务别的色),正是他看到的"不一致"。
   改:两处都走 `--tk-c`(= Module::color('tasks')),与右上角「全部任务」按钮同源。
   逾期仍用红色(状态语义,不能被主题色盖掉)。

4. 日程时间冲突错开(算法借鉴 SyncCalendar)
   用户指定去读 SyncCalendar 的算法。落地自
   `entry/src/main/ets/common/TimelineUtil.ets` 的 build()/groupsOf():
   ① 划分冲突组(与组内最晚结束仍重叠就并入);② 组内贪心分列(区间图着色),
   列数 = **每组各自的最大同时重叠数**(sync 原注释点名的反例:传递性重叠但
   B/C 不重叠 → 只该 2 列)。落成 `App\Calendar::conflictLanes()`(PHP,判据一字未改)。
   ⚠️ 两处比原算法更细,都有单测钉住:
     · 列数按**组**算 —— 否则一个孤立日程会被别处的三方冲突连累成 1/3 宽;
     · "缺结束时间"不能一律按 30 分钟参与冲突(会与后面所有日程假冲突),
       改为优先拿**同列下一条的起点**当它的视觉结束 → 因此必须**先分列再算高度**。
   首页与日历页共用同一套(用户第 2 条要求"和日程保持一致")。

5. 通知消息模块(Schema v30 → v31)
   不在导航显示,入口在头像下拉里、**排在个人设置之前**,名字「通知消息」;
   有人在观影/旅程加我时发通知;通知出现在首页、**往年今天之前**。
   新增:app/Notification.php + notifications 表(唯一键
   (person_id,target_type,target_id) —— 参与人是整组替换,没有它改三次标题就收三条)+
   ?r=notifications{,/del,/clear} + templates/notifications/index.php + 头像下拉未读角标 +
   app/helpers.php 的 fb_time_ago()。
   ⚠️ 三条设计判断:不进 Module::LIST(进去就会被顶栏渲染、被"模块显示与排序"当开关);
      只在 person_id !== actor_id 时发(不给自己发,"通知"≠流水账);
      移出参与人**撤销未读**、**保留已读**(未读是没送达的提醒,已读是看过的历史)。

验证(全部 0 失败):e2e 1478/0(+14 条新断言)、升级 153/0、vCard 40 项、城市 32/0、
几何 428/0 + 移动 680/0 + 交互 92/0、b24/b27/b28/b29 真机全绿;
冲突分列算法单测 12/0(含 SyncCalendar 点名的反例)、
新探针 _b30_ui2 16/0 + _b30_notify 16/0 + _b30_import 26/0、
反向验证 18/0(五条各自改回旧行为 → 断言全红)、
部署脚本新增 9 条守卫 + 守卫证伪 11/0。线上 11 文件 SHA256 一致,Schema v31 已升级。
2026-10-08 17:41:30 +08:00
laoyang 41572b77e7 第 21 批收尾:按需补「我的日程」本(老库也能拿到)
线上实测发现第一版没覆盖到的情况:把 'custom' => 「我的日程」加进
`Calendar::DEFAULT_KINDS` **是不够的** —— `seedDefaults()` 只在
**安装**与**结构升级**两条路径上被调用,而线上这套库早就升完级了,
于是老用户永远拿不到那一本。线上实测:owner=1 可写的普通本 0 本
(唯一那本 custom「工作」是用户自己建的)。

后果不只是"少一本":新建日程的"放在哪本"里全是派生本、默认还选中排最前的
**观影** → 用户直接打字点「加上」,建出来的是一条**观影记录**(建错东西,
事后极难发现:日历里看着是多了条日程,观影列表里莫名多一条)。

修法:
- 新增 `Calendar::ensurePersonalBook()` —— 按需补,幂等判据是
  `owner_id + kind='custom'`(**刻意不看名字**,否则用户把自己的本子改名之后
  会又冒出一本「我的日程」)。
- 由 `calendar` 路由在打开日历时顺手调一次(一次请求只多一条 SELECT)。
- 模板里再兜一道:默认选中的不是可写普通本时显式提示
  "你还没有自己的日程本,上面选的是一本派生本(加进去会变成观影/旅程记录)"。

验证:e2e **1064/0**(新增 5 条:删本 → 打开日历自己补回来 → 名字对 → 幂等 →
改名后也不会又冒一本);线上以 Web 用户(www)身份实测 before=1 after=1
(正确幂等,不动用户自己那本);真机探针 75/0 · 桌面几何 428/0 · 部署自检 0 失败。

⚠️ 测试侧顺带记一个坑:夹具删"有日程的本子"时 `calendar_events.calendar_id`
外键是 **RESTRICT**,而用**多语句** `mysql -e "SET FOREIGN_KEY_CHECKS=0; DELETE...;"`
时一旦有一句被挡下,**整个 -e 静默失败**(返回码 0、无 stderr),
症状是夹具自检报"还有 2 本"、后面断言一路红,单独手跑同样语句却是通的。
改成**一条一条发并检查返回码**后就好了。
2026-10-06 13:44:24 +08:00
laoyang f0437e6c93 v24 第 21 批:无刷新删除与新建日程 / 实际行程不回退 / 行程按时间排 / 统一上传框 / 时间范围控件修复(用户 8 条)
用户 8 条原话要点:
  ①「名册页面,删除一个联系人后,能否不刷新页面,删除成功后,直接把这个人,
     从当前页面的 dom 中删除,页面保持不动。」
  ②「观影模块,海报和票根以及照片都带进来了,但是没有缩放,把页面撑破了,
     应该缩放成缩略图。」
  ③「个人设置页面,天气设置那里,把:只影响你自己首页上的那一格 / 填个城市名就行
     (如「青岛」)… 这些提示,都删除。把城市单独放一行,精度维度放在一行。」
  ④「旅程模块,实际行程编辑后,点击保存,竟然又变成计划行程了,这个不合理,
     实际行程永远是实际行程,不会再变回计划行程了。」
  ⑤「旅程模块,旅程列表,行程列表,都是按照行程的开始时间排序的,最新的在最上面,
     而不是按照编辑修改时间。」
  ⑥「旅程的详情页面,右上角的按钮,设置在最右侧,和其他模块保持统一。」
  ⑦「所有上传图片等附件的地方,都不再放按钮,而是放一个框,点击这个框实现点击上传
     按钮的功能,拖动图片进这个框里,实现拖动多个上传功能。框里面写一句话:
     点击或拖动进行上传,其他的功能和现在保持一致。」
  ⑧「日历中新建日程,无论是点击按钮还是点击日历中间,都是采用弹框的形式进行添加,
     这样不影响当前日历的显示,添加完成后,动态修改当前视图,不要刷新。添加日历日程,
     开始时间和结束时间那里,用我们统一的时间范围选择器重做,现在功能无法使用。
     选择日历本,带上颜色。」

🔴 五个真根因(全都不像 bug):

 ① 🔴 **④「实际行程变回计划」= 表单字段同名合并**(本批最要紧的一条)
    暂存上传的令牌字段原叫 `stage[]`,与行程表单里那个隐藏的 `stage`(plan/actual)
    **同名**。PHP 解析表单时同名会**合并进同一个键**:
        `stage=actual` + `stage[]=tok` → `$_POST['stage'] = ['actual','tok']`
    (`[]` 只是"往同一个键追加"的记号,**不构成另一个键名**。)
    于是路由里 `(string)($_POST['stage'] ?? 'plan')` 得到 `"Array"` →
    `in_array('Array', ['plan','actual'])` 为假 → **静默回落成 'plan'**。
    ⚠️ 只有"拖过图片的行程"会犯(没拖就没有 `stage[]`)→ 症状是"时好时坏"。
    修法:令牌字段改名成 `stage_token[]`(改的是**字段名本身**,不是"是数组就取第一个"
    绕过去 —— 同名合并是 PHP 的表单语义,绕过去只会把坑留给下一次加字段)。
    **另加一道语义保险**:编辑路径**一律沿用库里那条自己的 stage**,不读表单 ——
    编辑表单里根本没有改阶段的控件(改阶段走详情页那几个显式动作)。
    这样即便将来又冒出某个字段名冲突,也不可能把「实际」悄悄变回「计划」。

 ② 🔴 **⑧「时间范围选择器无法使用」= 三道门叠在一起**
    ① `_event_fields.php` 的**结束端默认带 `hidden`**(要点「+ 添加结束时间」才出现);
    ② `app.js` 的 `upgradeRange()` 有「**两端都得可见**才升级」的判断 → 永远不成立
       (实测 `data-semi-range-done` 恒为 `null`);
    ③ CSS `html.js .dt-range:not(.is-semi-range) .dt-input { visibility: hidden }` →
       未升级时原生控件**也是不可见的** ⇒ 屏幕上一片空白、点哪儿都没反应。
    修法:两端**始终可见**("没有结束"由清空后半段表达,不再靠开关);
    并把旧的「+ 添加结束时间 / 去掉结束时间」开关整套删掉。
    ⚠️ 顺带挖出:`upgradeRange()` 里 `group.insertBefore(host, a)` 在"两端各自被
    `.field` 包着"时 `a` 不是 `group` 的子节点 → 直接抛
    `Failed to execute 'insertBefore' on 'Node'`。这个异常**以前从没触发过**
    (被 ② 的提前 return 挡住了)—— 一旦放出来就炸。修成插在"端点最外层包装"之前。
    ⚠️ 观察器加了 `attributes: true, attributeFilter: ['hidden']`(并把 childList
    分支的 `return` 去掉):弹框/折叠面板/tab 里"稍后才露出来"的日期控件从此都能自动升级。

 ③ 🔴 **⑧「新建日程默认建出观影记录」= 产品缺口,不只是显示问题**
    `Calendar::DEFAULT_KINDS` 原来只有 6 本**派生**本(观影/旅行/农历/节假日/传统节日/
    节气),**一本可写的普通本都没有**。于是"放在哪本"里全是派生本、默认选中排最前的
    **观影** → 用户直接打字点「加上」,建出来的是一条**观影记录**
    (日历里看着是多了条日程,观影列表里莫名多一条)。这是"建错东西",事后极难发现。
    → ① 默认本补成「我的日程」(kind='custom'),模板里默认选中也改成**优先 custom 本**;
      ② 选择器里把**纯派生只读本**禁用并标「(自动生成)」
         (直接不列会让人以为本子丢了;列为可选又会白选一次再吃错误)。
    ⚠️ 顺带发现 `Calendar::READONLY_KINDS` 把 `movie`/`trip` 也算进去了,而
    `event_add` 里**紧跟着**就有两段专门处理它们的分支(「已加进观影」/「已加进旅程」)
    —— 那两段**永远走不到**(校验先一步把请求打回去了)。判据要按**能不能写**分,
    不是按"是不是派生"。

 ④ 🔴 **②「观影编辑页图片撑破」= `.mv-slot` 这个类在样式表里根本不存在**
    第 21 批加模板时只写了结构没写样式 → `<img>` 按**原始像素**渲染:600×900 的海报
    在 236px 宽的格子里撑出 942px 高,还把整页横向顶破(实测 docW 1297 > vw 1280)。
    → 补 `.mv-slot` 规格:固定 108×162 缩略图 + `object-fit: cover`。
    ⚠️ **高度必须写死** —— 不写死就又会由原图比例决定,等于没修。

 ⑤ **⑤行程列表排序 = `category, position` 主导**
    `position` 默认同一个数(旧迁移给的多是 0/100),于是顺序被 position 挤乱。
    实测线上 #222 渲染成「返回青岛(10-5) → 出发去菏泽(9-30) → 去姥姥家(10-2)」。
    → 改成 `ORDER BY (starts_at IS NULL), starts_at, id`。
    ⚠️ 方向是**正序**(早的在前,已跟用户确认);旅程列表仍是 DESC(最新在最上)。
       两者方向不同是**刻意的**:一个是"挑哪趟旅行",一个是"读这趟旅行的行程"。
    ⚠️ 详情页**按阶段分三组**渲染(计划/实际/未成行),排序只需在**组内**有序。

本批新增能力:
- **统一上传框**(⑦):`templates/_stage_drop.php` 现在**同时**负责
  「sr-only 的真文件框 + 看得见的上传框 + 暂存清单」,调用方只 require 一行。
  框是 `role="button" tabindex="0"`,JS 转发 `input.click()`(键盘 Enter/Space 也通),
  拖文件进框走既有的暂存上传(拖一个立刻传、可续拖、失败标红、未传完拦住保存)。
  ⚠️ 框用 `<div>` 不是 `<button>`:里面要放暂存清单(含「撤掉这张」按钮),
     按钮里嵌按钮是非法 HTML(第 21 批刚在 `<form>` 嵌套上栽过同一个跟头)。
  ⚠️ 文件框用 `.sr-only` 不是 `display:none`:后者不可聚焦,带 `required` 时
     Chrome 会报 "An invalid form control is not focusable" —— 点了什么都不发生。
  落了 11 处:行程照片 / 观影海报·票根·合影 / 资源 / 任务 / 日程附件 / 书籍 /
  联系人导入 / ics 导入 / 观影补传 / 资源补传。
  ⚠️ 清单与提示行移到框**外面**(放框内会变成"一个大框套五张图"),
     所以 app.js 改从 `.upfield` 容器里找它们。
- **无刷新删除(①)与无刷新新建日程(⑧)**:路由按 `X-Requested-With`
  (或 `Accept: application/json`)区分调用者 —— 带它回 JSON、不带照旧 302
  (无 JS 的兜底**不变**,两边的反向都写了断言)。
  · 名册删除:成功 → 摘卡片 + 就地改「共 N 人」与分组人数 + 派发 `input` 让
    filter.js 自己重算(**不抄它的判据**);失败 → 把原因说出来(不刷页);
    拿不到 JSON → **退回原生提交**(绝不静默失败)。
  · 日历新建:JSON 带齐前端插入当前视图要用的字段(时间/颜色/链接/是否全天/cal)。
    月视图**按时间插入**(不能 append —— 服务端只渲染前 3 条、按时间序取,
    append 会让"新加的早间日程"掉进「+N 条」里看不见,刷新后又跑到第一位);
    周/日视图按"24h = 100%"算 top/height 插块。
  · 观影 / 旅行本是**可写派生本**:AJAX 时不再跳详情页(跳走就离开了日历),
    改在提示行给一个「去看详情 →」链接。
- **⑧日历本带颜色**:原生 `<option>` 画不出色块(浏览器不允许在 option 里放元素),
  所以做成「色块 + 下拉」并排,色块跟着当前选中的本子变色。
- **③天气卡片**:三句提示全删;城市单独一行、纬度/经度同一行(`.loc-row`)。
  删的只是说明文字,三个输入框 + 保存 + 状态回显都保留。
- **⑥设置按钮贴最右**:原来顺序是「… → 设置 ☰ → 删除 🗑」,设置被删除键挤到**倒数第二**;
  改成删除在前、设置恒为最后一个(与观影/日程/任务/资源同一个手势)。

验证(本地全套):
- e2e **1059/0**(本批新增约 40 条断言)· 结构升级 149/0 · 桌面几何 428/0
- **真机探针 75/0**(`_fb_b21_ui.py`):AJAX 删除真的没导航 / 卡片真被摘掉 /
  滚动位置保持;缩略图几何(108×162,不再溢出);上传框(框在、原生框不可见、
  点框真打开文件选择框、连拖两张都上去且清单可见);日历弹框(打开时网格一动不动、
  范围控件真的挂上且可见、能弹面板、色块跟着换、无刷新提交后当前视图里立刻出现、
  周视图也能插时间块、全程无 JS 报错)。
- **反向验证 12 条**(`_fb_b21_falsify.py`):逐条把修复改回旧行为看断言是否变红 ——
  令牌字段改回 stage[] / 编辑读表单 stage / 排序改回 position / 设置按钮挪走 /
  框不转发点击 / 文件框不再 sr-only / 删缩略图规格 / 删除不回 JSON /
  结束端改回 hidden / 日历本默认改回第一个 / 天气提示加回 / 弹框改回内联。
- 部署脚本静态断言段本地空跑 **307/0**。
2026-10-06 12:08:40 +08:00
laoyang c3d00ad66a v24 第 20 批:拖动即时上传 / 观影图片带出 / 行程保存修复 / 日程红线与删除范围与多提醒 / 悬停统一(用户 9 条)
用户 9 条原话要点:
  ①「观影模块,编辑观影,已经保存的海报和票根、合影,都没带进来。」
  ②「个人设置页面,"只影响你自己看到的样子"这个提示删除,不要了。我怎么称呼家人,
     这个卡片,不要了。我的资料这个卡片,不要了。」
  ③「旅程模块,编辑行程,点击保存没有反应。」
  ④「所有上传图片视频和附件的模块,都允许多次拖动添加文件上传,拖动一个进去,
     就开始上传,还可以继续拖动,继续上传,最后统一保存即可。」
  ⑤「添加行程,线路名、承运方,都不要了。车辆,只在出行方式选择自驾时才出现,
     其他情况下默认不出现。出发地和到达地,应该在同一行。」
  ⑥「日程模块,周视图和日视图,应该在视图中间显示一条红线,用来表示当前进行的时间…
     用一个不断从上到下的红线,来表示当前时间在视图上的刻度和位置。」
  ⑦「对于重复的日程,当点击删除的时候,应该提示是删除这一次,还是全部都删除,
     还是删除这一次及以后的。」
  ⑧「日程的提醒,是可以多选的,就是说,一个日程,我可以选择让它 5 分钟提醒,
     10 分钟提醒等等。」
  ⑨「很多按钮的悬停效果非常奇怪,感觉纯粹是换了一个背景色…应该调整所有按钮的悬停效果,
     保持统一,但是不要单纯的换一个不协调的背景色。」

🔴 三个真根因(都是"看着不像 bug"的地方):
  1. **③ 点保存没反应 = `<form>` 嵌套**(HTML 非法):`trips/node_form.php` 的照片缩略图上
     套了 `<form action=photo_del>`,而整块在**外层行程表单内部**。浏览器解析到内层
     `</form>` 时**把外层表单提前关掉** → 「保存」按钮**掉出表单**(真机实测
     `button.form === null`)。源码里完全看不出,**只有真浏览器量 `btn.form` 才现形**;
     且**只有这段行程已经有照片时才触发** —— 所以表现为"有时候好、有时候坏"。
     → 按钮留在原位(布局不动),用 HTML5 `form="id"` 关联到**表单外**的独立表单。
  2. **⑤ 车辆藏不住(第 17 批就写过、一直没生效)**:`.field` 自己声明 `display:flex`,
     作者样式压过浏览器默认的 `[hidden]{display:none}` → `hidden` 形同虚设。
     **本项目第 4 次栽同一个坑**(.att-item / .pv / .pp-item 各一次)→ 补 `.field[hidden]`。
  3. **④ 拖动即时上传的卡片"看不见"**:模板给 `.stage-list` 加了 `hidden`(没人上传时
     不留空边框),JS 建卡片时忘了 `list.hidden = false` → 卡片在 DOM 里但整条清单
     `display:none`、尺寸 0×0。**HTTP 层断言全绿**(HTML 里确实有卡片),
     **只有真机量尺寸才发现** —— 由新写的真机探针抓到。

本批新增能力:
- **拖动即时上传**(第 4 条):新增 `upload_stage.php` + `app/StagedUpload.php` +
  `templates/_stage_drop.php`。拖进来 → **立刻** POST 到暂存目录 → 回 `stoken` →
  放进隐藏字段 `stage[]` → 保存时在 `attach_save_files()` 里**认领**(移文件 + 建附件行)。
  `media.php` 加 `?stage=` 预览(拖完立刻看得见缩略图)。
  ⚠️ 三个必须踩准:**串行队列**(并发会让缩略图顺序乱跳)、**提交守卫**
  (还有没传完就 preventDefault,否则"拖了但没保存上"必然发生)、**卡片清单要 un-hide**。
  ⚠️ 海报/票根走**独立字段名** `stage_poster`/`stage_ticket`(各自单槽位、要按比例重裁)。
  ⚠️ 落点**就近取**(`input.closest('.field')`):一个表单里多个上传区不能串到第一个去。
- **观影编辑带出已存图片**(第 1 条):编辑页显示海报 / 票根 / 合影 + 各自的删除入口;
  新增 `movies/media_del`(只删不换也能删,槽位走白名单);`movies/update` 收编辑页
  新选的照片时**合并**而不是替换(否则一编辑就把已有合影冲掉)。
- **行程表单**(第 5 条):去掉线路名 / 承运方(**库里的值保留**:UPDATE 时不参与,
  与 seat_info/gate 同一套);出发地/到达地包进 `.loc-row` 真同一行;车辆按自驾显隐。
- **当前时间红线**(第 6 条):位置在服务端算(与 `.tg-block` 同一套"24h = 100%"坐标),
  前端每分钟自己挪(对齐到下一分钟整再起定时器),跨午夜 reload;只画在**今天那一列**。
- **重复日程删除三档**(第 7 条):与「编辑」那三档同名同序(全部 / 这一次及以后 /
  只这一次)。「只删这一次」→ `skipOnce`(**绝不删整条系列**);「这一次及以后」→
  老系列截 UNTIL,**必须清掉 COUNT**(两者并存时 COUNT 优先,不清就截不断)。
- **提醒多选**(第 8 条):同列存逗号串(`5,15,60`);Schema 把它从 **INT 改 VARCHAR**
  (INT 存逗号串会被隐式转换,多档会静默只剩第一档);ICS 导出吐**多个 VALARM**。
- **悬停统一**(第 9 条):原来 14 种互不相干的 hover 底色 → 统一成**两套手法**:
  有底色 `filter: brightness(.92)` 同色系加深 + 轻抬升;描边白底 `--hov-veil` 中性薄雾
  + 边框微显。放在 CSS **最末**用一条兜底收口(新按钮自动继承),并把十几条旧规则
  清成"交给统一段"。`person_picker.js` 同时改为按需加载。

验证(本地全套):
- e2e **1004/0**(+21 条本批断言)· 升级 149/0 · 桌面几何 428/0 ·
  **真机探针 25/0**(连续拖两次 / 红线位置与颜色与几何 / 车辆显隐 / 出发到达同行)。
- **反向验证 5/5**:①删除按钮改回嵌套 form ②卡片清单不 un-hide ③观影编辑不渲染海报
  ④车辆隐藏失效 ⑤「只删这一次」做成删全部 —— 五种坏法全部被抓住,还原后复绿 1004/0。
  过程中还**抓到两条弱断言**(只查 DOM 有无、不查可见性/是否真渲染),已一并加强。
- 部署脚本静态断言段本地空跑 **305 条全过**。
2026-10-06 03:14:18 +08:00
laoyang 7138314dc7 v24 第 15 批:活动 / 旅程两级语义(术语 · 派生时间城市 · 表单三项 · 题图可点)
用户原话四条,逐条落地:

1. 「旅程模块,按钮是记一次活动,而不是记一趟旅程。旅程是活动里面的概念。」
   → 顶层一律改称**活动**:按钮「记一次活动」、页标题「活动」、`N 次` / `N 段旅程`;
     活动详情三组改叫「计划的旅程 / 走过的旅程 / 没成行的」;
     节点表单「这段旅程叫什么」「阶段」;flash 文案 17 处同步。
     (导航条仍叫「旅程」——用户就是这么称呼这个模块,列表页加副标题说清两级关系。)

2. 「活动不需要开始和结束时间,是根据活动中的旅程,最早的旅程开始时间,和最晚的
     旅程结束时间,计算出来的。目的地也不需要…去的城市,也是根据旅程,得到的。」
   → 活动**不再存**这三样,全部**读时算出来**:
     · Trip::derived()      最早开始 / 最晚结束 / 城市去重
     · Trip::cities()       按最早出现时间排(先后顺序本身是信息,不是字母序)
     · Trip::attachDerived() 列表页批量补(避免 N+1)
     · Trip::activitySpan()  没有旅程时说「还没有旅程」,不用「日期待补」
     create/update 不再写 events 的起止与地点(老值保留不动,页面不读)。
     列表排序/搜索、首页「最近去了」改走子查询。

3. 「题图那里,没有办法上传图片,点击哪里都没反应。」——**真 bug**
   「点图 = 开文件框」那句 JS 只写在 people/form.php 的内页脚本里(头像专用);
   第 14 批加的旅程题图与图书封面照抄了同一套 DOM 却没有对应的 JS,
   于是图块有 cursor:pointer 与 hover 高亮(看着能点),点下去毫无反应。
   → 做成声明式通用绑定:图上写 data-pick-input="#fileInputId",
     app.js 统一接管(一个页面多个也各绑各的),并删掉旧的那份专用实现。
     三处(头像 / 题图 / 图书封面)全挂上。

4. 「添加活动的页面,顺序是这样的:添加标题,上传并裁剪题图,然后一段简介,就可以了。」
   → 主区就这三项,顺序照原话;时间/城市不做输入项(只留一句说明它们从哪来,
     不说清用户会满页找"怎么没地方填时间")。
     备注 / 公开 / 同行的人收进「更多设置」折叠区——功能一个没丢。

顺带修掉两处会炸的实现问题:
· ORDER BY 引用聚合别名(first_at)在 MariaDB/MySQL 报 1247,且只在运行时炸、
  php -l 看不出 → 改成 ORDER BY MIN(starts_at),否则整个旅程模块 Fatal。
· e2e 里"详情页显示了派生时间"原本写成整页查找,下面每段旅程自己也渲染日期,
  于是把派生值塞成 null 也照样绿(借光假绿)→ 判据收进页头那一段。

验证:e2e 845/0(+15 条本批断言)· 升级 134/0 · 桌面几何 428/0 · 移动几何 688/0 ·
交互 92/0 · 题图真机点击 4/0 · 读书窄屏 15/0 · 家谱 34/0 + 10/0 + 反向 3/3 ·
观影按钮 60/0 + 反向 3/3 · 本批反向验证 4/4(题图绑定抽掉→红、派生值不挂→红、
城市退回读 location→红、时间字段加回表单→红)· 部署静态断言新增两组(含"活动不许
再写起止时间/目的地"与"ORDER BY 不许引用聚合别名"的反退化防线)。
部署后服务器自检 19/19,五个关键文件 md5 与线上逐一核对一致。
2026-10-05 10:58:17 +08:00
laoyang ecf121772f v24 第 12 批:5 条(删除按钮位置/日期控件三修/日历跳转/节气节日/财务图)
用户 2026-10-05 的 5 条。第 11 批交付后用户追问"确定这些需求都做完了?"—— 没有,
这 5 条是进一步要求。实现过程中查出 4 个真 bug。

#1 详情页删除按钮紧贴「开通账号」
   上一版放在页面最下方独立区块(danger-zone),用户要「就放在开通账号按钮下面,
   紧挨着,长度大小和开通账号一样,颜色改成红色」。→ 搬进「登录账号」卡片内
   (.del-inline + 细分隔线 + .btn-block-lg)。
   线上几何实测:同一张卡片、左边缘对齐 255==255、宽度一致 990==990、红色 rgb(192,83,63)。

#2 日期控件三修(两个真 bug + 一次闪烁)
   · 灰底:Semi **外层** .semi-datepicker-input 自带 rgba(46,50,56,.05)、高 32px;
     内层 wrapper 白底 40px → 灰的露在外面。实测「灰底元素」= 1 个 → 修后 0 个。
   · 不垂直居中:外层 32px < 内层 40px,实测 gapTop=1 / **gapBottom=17**(文字贴顶)
     → 修后 gapTop=1 / gapBottom=1。
   · 闪烁:实测首 **200ms** 原生输入框可见(要等 3.4MB React 解析)。
     用 no-js/js 二态:<html class="no-js"> + head 内联脚本立刻换 js +
     CSS `html.js .dt-input:not(.is-semi) .dt-native { visibility:hidden }`
     (配同高骨架 ::after 防布局跳动)。实测「出现过原生可见」= False。

#3 日历跳转保留视图 + 下拉日历
   原来写死 view=day(在月视图点跳转会被踢进日视图)。→ 表单带当前 $view(月还带 ym);
   选择器从"居中弹窗"改成**按钮下方下拉** .cal-jump-drop。
   实测:月视图跳 8/15 仍在月视图且是 8 月;周视图跳 8/15 → 落 8/10 那一周;
   日视图跳 8/15 → 显示 8/15。

#4 传统节日 + 二十四节气两个系统本(Schema v18)
   Holiday::SOLAR_TERMS(24 项 C 常数近似公式 + 例外表)+ traditionalRange
   (农历推导;除夕用"次日是否正月初一"反推,写死腊月廿九会有一半年份错)。
   实测 2026:节气 24 条全对(清明 4/5、冬至 12/22);春节 2/17、除夕 2/16、中秋 9/25。
   ⚠️ ENUM 必须显式 ALTER(对已存在的表不生效)——同一坑第二次踩,VERSION 17→18。

#5 财务(一个真 bug + 一个假起点 + 一个真 bug)
   · chart_line 读 `cents` 而 timeline() 返回 `net` → 键名不匹配,**总资产图每点读成 0**
     (正是用户说的"图上是 0")。修:兼容 cents ?? net ?? asset。
     实测 timeline net=800000(8000 元),线上 polyline 有真实起伏。
   · 去掉 timeline 人为插的"前一天=0"假起点(用户:"总资产不应该是 0,
     应该是所有账目创建时的金额的总数")。
   · ppSetSelected 只用 document.querySelector 按名字找,而财务页有**两个** person_id
     组件(「记一笔」+ 修改弹框)→ 永远灌到第一个。加 root 参数、调用方传 panel。

验证:e2e 786/0、交互 92/0、升级 129/0(+3 条 v18 断言)、batch_e 91/0、batch10 46/0、
batch9 38/0、picker_unify 28/0、picker_lazy 11/0、crop 17/0、formbloat 9/0、semi 27/0、
mobile 688/0、drag 5/0、geom 5/5;第 12 批专项:财务 7/0、日历 19/0。
部署静态断言新增 5 组(含反向验证:注入回退 → FAIL=1)。
线上自检 19/19;线上真实账号验收 **30/0**(含真实浏览器几何量删除按钮位置)。
2026-10-04 19:43:00 +08:00
laoyang 5d80b9480b v24 第 11 批:7 大条 20 子项(首页/名册/观影/裁剪/日程/财务)
用户 2026-10-04 第 11 批,7 大条:
- #1 首页:未来七天过滤掉「农历」(每日一条会淹掉真日程);右侧日历本标签
  从灰字(.dash-tag)改成**实心色块**(底色=日历色、白字、text-shadow 兜底浅色)。
- #2 名册:分组改**居中模态框**(用户要"像选观影人一样")。修掉"点第二次失效"
  ——根因是旧实现用**捕获阶段**监听 document click 做"点外面就关",与触发按钮
  的 click 打架;模态把关闭收敛到遮罩/✕/取消。卡片「编辑」后加「删除」
  (→ people/destroy);详情页底部删除改成 btn-danger + btn-block-lg 大红按钮 + 两步确认。
- #3 名册详情页重排:.detail(260px+1fr) → .pcard(头像左上 + 右侧姓名/标签)
  + .detail-stack 全宽往下。旧布局头像卡比右栏矮,头像下留一大片空白。
- #4 观影:影院下拉换**自定义组件**(原生 <option> 画不了色块)——按钮+列表、
  每行色点,值仍写隐藏 input[name=cinema_id];「管理影院」→「新建影院」弹框
  (带 back=movies/new,存完回本页并 ?cinema=<id> 自动选中)。
  修「开始/结束时间标签跑到控件下面」:upgradeRange 原来 insertBefore(host, group.firstChild)
  把控件插到了 label 之上 —— 改成插在第一个原生控件之前。
- #5 裁剪改 **Cropper.js**(assets/vendor/cropper/,按需加载走 fb-cropper meta):
  弹框 overflow:hidden + 固定画布(**无滚动条**)、**去掉缩放条**改滚轮 wheelZoomRatio、
  viewMode:1 + autoCropArea 保证框小于画布、8 个触点全露。
  结果仍写 *_crop = 归一化 x,y,w,h(提交契约不变)。
- #6 日程:内置节假日原来每天只标**一天**(国庆只出一条)→ FIXED 改成
  [名字, 放假天数] 按区间铺;月视图从周日改**周一**起(表头同步);
  跳转日期从"长条+原生框"改成**单个 btn-icon**,点开弹 Semi 日期选择器。
- #7 财务:单条曲线无盘点时只回一个点(图看着像全 0)→ 改成"从创建日(0)画到今天";
  图下条目从"每账目一张小图卡"改成**一张 chart_multi 多线图 + 颜色图例**
  (Ledger::LEGEND_COLORS 按 id 取模,保证颜色稳定);账目页卡顿同源治理
  (行上只留按钮、表单全页一份 #ledPanels);修改按钮改弹框并回填归属人。

## 顺带修的真 bug(本批最要紧的三条)
1. **观影表单提交静默失效**:「新建影院」弹框被误嵌在观影主表单 <form> 内。
   HTML 不允许 form 嵌套 → 浏览器拆开内层,弹框里 required 的"影院名称"变成
   主表单必填项、又因 hidden 无法聚焦 → 点「记下这场电影」什么都不发生
   (Chrome: invalid form control with name='name' is not focusable)。
   → 弹框移到主表单之后。部署脚本加防线:按顺序配平 form 开闭判断是否嵌套。
2. **裁剪取消没清空文件框**(换 Cropper 时丢)→ close(cancelled),4 个取消入口
   传 true 清空;否则"点了取消"仍会把没裁的原图传上去。
3. **裁剪预览退化成原图**(同批丢)→ 用 canvas 按 Cropper 的原图像素矩形重画,
   toDataURL 写回预览,恢复"所见即所得"。

## 连带修复:选人组件懒渲染(线上实测的必要性)
线上真实数据下账目页 6939 KB / 16788 个 input、观影表单 1194 个 input ——
根因是 _person_picker.php 首屏就把**全部候选人**渲成 <label>+<input>。
改成:首屏只留「不选」+ data-pp-people(JSON),**打开弹框时**才 buildList();
无 JS 兜底走 <noscript>(HTML5 规定启用脚本时其内容按纯文本解析,不产生节点)。
新增 window.ppSetSelected / decorateItems(动态项要补算首字母)。
实测账目页 6939 KB → 544 KB。新增 _fb_picker_lazy_check.py(11/0 + 反向 2 红)。

验证:e2e 786/0、交互 90/0、半集成 27/0、batch_e 91/0、batch10 46/0、batch9 38/0、
crop 17/0(+反向)、picker_unify 28/0、picker_lazy 11/0(+反向)、formbloat 9/0、
upgrade 126/0、mobile 688/0、drag 5/0、geom 5/5。
线上自检 19/19;线上真实账号验收 7/0(含"主表单无隐藏必填""线上 Cropper 接管+取消清空")。
2026-10-04 17:42:21 +08:00
laoyang b2ca4f336e v24 第 10 批:名册真性能(根因是每卡内联表单)等九条
- #6 名册线上真实数据(1184 人)卡顿:根因是**每张卡片内联一整份分组弹层**——
  全页 2369 个 form / 27234 个 input / 22496 个 radio / 98100 DOM 节点,
  密码管理器(Bitwarden 等)逐个扫描 form 把主线程占死(用户报的就是这个冲突)。
  改为卡片只带 data 属性 + 全页**唯一一份**共享浮层 #rosterMenus。
  实测:form 2369→3、input 27234→25、DOM 98100→17659、解压 7694→2306 KB、
  长任务 1/52ms → 0。新增哨兵 _fb_formbloat_check.py(含反向验证)。
- #5 日视图翻页失效:day_columns() 无条件吸到周一,天视图只有 1 列 → 永远显示那周周一。
  改为仅 span>1 时吸附(周视图行为不变)。
- #2 财务「欠款」类别(负债);并修一个真 bug:负债填负数会让净资产**反向变大**。
  写入口 normalizeAmount 归一化 + summary/timeline 侧 abs 防御 + Schema v17 纠正老数据。
- #3 账目行「盘点」「修改」两个并列按钮(各自就地弹面板);图移到账目上方,
  总金额图跨整行 + 每个账目一条线 + 类别筛选条;charts() 改为覆盖全部账目。
- #4 日程新增「农历」「节假日」两本不可删除只读日历(app/Holiday.php):
  两级自动更新 —— 内置规则(公历固定 + 清明节气 + 农历节日)保证断网也有;
  官方 JSON 镜像自动抓取补齐调休。抓取**不在渲染路径联网**(响应后 fastcgi_finish_request
  + 频率闸门),缓存落 storage/cache。仅派生本不可写日程。
- #6 日程页设置按钮前加「跳到某一天」(选中即跳)。
- #7 起止范围改用一个 Semi dateTimeRange 控件(观影/旅程/节点/日程共 4 处);
  新增 window.fbSetDT / fbUpgradeRange;修范围重挂(React 非受控组件须 unmount 再 createRoot)
  与范围里 required 未摘除导致表单无法提交两个真实问题。

验证:e2e 786/0、batch10 44/0、batch9 38/0、batch_e 91/0、upgrade 126/0、picker 28/0、
semi_integrate 27/0、formbloat 5/0(+反向 3 红)、mobile 688/0、interact 97/0、drag 5/0、geom 5/5;
线上自检 19/19 + 线上真实账号巡检 28/0(含 Firefox 实测)。
2026-10-04 10:46:50 +08:00
laoyang e0562b8627 v24 第 5 轮:十条清单(天气填城市 / 输入即筛选 / 看板列=看板 / 账本盘点折线图 / 纪念日分类)
用户 2026-10-03 的十条,逐条落点:

1. 天气改为**个人设置里填城市**(每人一份):新增 app/Geo.php,内置 197 个
   地级市坐标(离线、逐条核对,生成脚本 _tooling/_fb_geo_gen.py)。填坐标仍
   作为内置表没有时的出口;老的家庭级坐标保留为兜底,升级当天天气不会凭空消失。
2. 所有"找个人"的地方**输入即筛选**:新增 assets/js/filter.js(纯前端显隐,
   不发请求、回车不刷新)+ app/PersonQuery.php 把服务端判据收敛成一份
   (名册 / 挑人 / 高级搜索共用,避免三处实现漂移)。
3. 名册列表:有昵称就不重复显示本名、行变矮、补齐手机/阴历阳历生日(带年份)/
   邮箱/工作单位;「清除」与「搜索」同款同尺寸。
4/5. 日程:撤掉左侧日历本栏改满宽,功能(显隐/颜色/导入/默认视图)全收进
   右侧 ☰;**色块高度改为按起止时长**(旧实现一律 30 分钟,三小时的会跟
   十五分钟的同高);点日历空白处即建日程,开始时间=点的那一格。
6/10. 资源:搜索提到最上;按"最近改动/最新新增"排序,默认只显示最新 10 条并
   给「显示全部」出口。
7. 任务:看板的**列=用户自定义的看板**(不再是写死的待办/进行中),
   拖动/左右键=换看板(改 board_id,不动 status)。
8. 财务:概览在上账目在下、记一笔改右侧按钮、账本设置(共享/改名/新建/删除)
   收进 ☰、默认账本名「xxx的账本」、"挂在谁名下"默认自己且用标准选人框;
   **新增盘点**(新增 ledger_entry_snapshots 只增不改的历史表,旧值不被清空)
   + 上面画金额变化折线图(app/chart.php 服务端画 SVG,零依赖)。
9. 纪念日:手动添加的与名册生日派生的**分两块**、手动的在上;右侧设置可增删
   分类(家庭级,删分类不强改已用的历史)。

本轮另外修掉/加固的四类问题(都由新增的静态或运行时守卫兜住):
- index.php 少写 2 个 use 导入(Pinyin / UserSetting)→ 名册与个人设置
  返回 200 但正文是 Fatal;顺带修了一处早已存在的 Attachment 未导入。
  → 新增 _fb_import_check.py(1f 段)。
- 一次不精确替换弄丢了 app/Calendar.php 开头的 `<?php`,整份源码被当 HTML
  吐给浏览器,而 `php -l` 报"无语法错误"(纯文本输出确实合法)。
  → 新增 _fb_php_open_check.py(1e 段)。
- 模板片段 unset 了宿主模板的变量(账本片段 unset($canWrite))→ 正文里一片
  Undefined variable 而 HTTP 仍 200。同 _datetime.php 早年 $t 的坑。
  → 新增 _fb_fragment_scope_check.py(1g 段)+ _fb_batch_e.py 的 26 页无告警巡检。
- 窄屏下页头动作区不换行 → 周视图横向溢出且按钮被挤成三种高度(用户提过三次
  "按钮要同高")→ .page-head .actions 加 flex-wrap。

验证战绩(全绿):
  e2e 773/0 · 第 5 轮专项 91/0 · 结构升级 126/0 · 几何 428/0 · 移动端 684/0 ·
  交互 97/0 · 家谱 26/0 · 按钮 60/0;三轮反向验证各 3/3 抓到,还原后复绿。
线上:schema v14、指纹一致(9df6c9fccc6796db)、snapshots 表已建、197 城、
  8 个关键文件 md5 与本地逐字节一致、app/ 403 无源码泄漏、错误日志无新增。
2026-10-03 15:01:38 +08:00
laoyang 8f80f3e835 v24 第 2 轮:19 条清单收尾(附件预览/CalDAV/任务分类看板/全局选人/家谱/纪念日/财务)
用户 2026-10-02 提的 19 条需求,本轮完成剩下的 8 条 + CalDAV 服务端:

- ③ 附件:图片/视频给缩略图,PDF/Word/Excel 给**同尺寸**图标;可预览的在本页
     弹层里看(不再新开标签页),渲染不了的点了直接下载;预览层右上角恒有下载按钮。
- ⑦b CalDAV:把「我的日历本」按 RFC 4791 只读子集分享出去(OPTIONS/PROPFIND/
     REPORT/GET,写方法一律 403)。用**专用口令**(库里只存 sha256),不是登录口令。
- ⑨⑩ 任务:分类与看板都由用户自建;已完成的任务从看板消失,改在「已完成」页
     按分类查看/搜索;设置入口在「新建任务」旁的 ☰。
- ⑪ 全局人员选择器:一个片段铺到观影/旅程/资源/日历共享/任务共享/财务共享;
     修掉了老组件「名字和拼音都搜不动」的 bug —— .att-item 自己声明了 display:flex,
     压过了浏览器 [hidden]{display:none},过滤根本没生效。
- ⑫ 家谱:头像改方形不再裁切;姻亲不在图上的卡片给「⇄ 姻亲」入口,
     以 TA 为中心重画 + 「返回我的家谱」。
- ⑬ 纪念日:阴历/阳历都能设且按对应历法重复;名册里每个人的生日默认可见
     (实时派生,不复制一份);全部作为全天日程进日历。
- ⑮ 财务:账本可共享,双方都能记账、都能看全部账目,资产−负债=净资产;
     金额一律以「分」存,不用浮点。

验证:端到端 724 项通过 / 0 失败;几何 368、交互 97、移动端 516、家谱 24、
升级 123 全绿;SQL 列名静态校验 0 处可疑;部署 11 条线上探针全过。
2026-10-03 08:18:05 +08:00
laoyang 71a7ec35a0 v14: 观影/家谱/日程/旅程/资源/任务六模块统一,首页改 dashboard
用户 14 条需求一次交付(1-13 功能 + 14 工作方式)。

观影(1-4)
- 返回文案全模块统一为「返回观影」(不再有「返回列表」「返回观影记录」三种叫法)
- 观影人一律用彩色人物标签 person_chip:底色由头像主色算出来(上传时存库),
  瀑布流卡片也按人显示;五处观影模板全部改用它
- 人员显示统一走 person_name()(shown → 我给他起的称呼 → 姓名)
- 影院管理页与年度报告页的上下两块用 .card + .card 统一给间距(原来紧贴成一整块)

家谱(5)
- 树根从「我」换成**辈分最高的祖先**:库画的是"树根的祖先链+后代+树根的兄弟姐妹",
  根=我 时伯父/堂兄/嫂子/侄子一个都不画(12 人夹具只画出 8 人)
- focusMap 按**代数**圈定三代(不再按步数),normalizeLinks 把子女对称挂到父母双方
  —— 修掉"母亲登录看不到孩子"
- 新增「看全部 / 回到我」两个按钮,放在画布外(画布整块被库接管了手势)

统一事件原语(6-9)
- 旅程:TripLog 全量迁入(3 趟旅程 / 21 段行程 / 72 张照片 / 5 个人)。
  复用 events(kind='trip') + trip_details + trip_nodes(计划/执行双轨保留)
- 日程:每人可建日历本(自定义色)+ 共享只读/可写;观影与旅行是每人默认两本,
  内容是**从各模块实时派生**的(不复制,所以改了观影日期日历立刻跟着变);
  没有具体时分 = 全天;月视图参照群晖 Calendar
- 资源:挂在某人名下的附属信息(标题+正文+多附件),可共享给多人并设只读/读写,
  默认折叠、可搜标题与正文
- 任务:看板三列 + 两级子任务 + 评论@人 + 共享 + 循环(完成时按原截止日推下一期)
- 统一附件表:四个模块的附件共用一张,下载地址不落库、一律由 media.php 现算

模块化(10-11)
- App\Module 是模块的唯一来源(加一行 = 导航条/主题色/个人设置三处自动出现)
- 个人设置里可勾选要显示哪些模块、拖动排序(关掉的模块**不会**被兜底补回来)
- 每个模块一套主题色(家庭级),只覆盖 --brand 三兄弟,不碰纸墨底色

其他(12-13)
- 新增成员表单可选所属分组(编辑表单没有:分组是"我和他的关系",只归我自己)
- 首页改 dashboard:接下来的日子 / 未来七天 / 手头在做 / 往年今日。
  家人卡片从首页撤走(看人去名册)

验证(全部通过)
- e2e 542 条(新增 4 模块端到端 60+ 条)
- 桌面几何 368 / 移动端几何 520 / 家谱 12 / 交互 97 / 老库升级 123
- SQL 列名静态校验 0 处可疑(对着真实表结构)
- 反向验证 18 条新注入全部抓住

顺带修掉的既有问题
- 老库升级回填 SQL 按实际存在的列动态拼(缺列不再 1054 炸断升级)
- 全新安装时 Schema::ensure 跑在"人还没建"之前 → 补建日历本三处齐备
- 窄屏导航横向溢出(模块从 4 个加到 8 个后顶栏撑破视口)
2026-10-02 09:28:07 +08:00