Files
FamilyBook/templates/calendar/_settings_menu.php
T
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

69 lines
4.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 日程模块的「设置」下拉(v24 用户 2026-10-03 第 4、5 条;🔴 v51 重构)。
*
* 起源用户原话:「日程模块,左侧的日历本显示与否,去掉,让日历充满整个宽度。
* 在右侧,也加一个设置按钮,设置中,可以设置哪些日历本显示,
* 调整日历的颜色,导入日历等功能,都放在这个设置下。」
* 「在日程的设置中,可以设置默认显示的视图模式,月视图还是周视图还是日视图。」
*
* 🔴🔴 v51(用户 2026-10-15 第 6 条)——**下拉从"长表单"改成"纯列表"**:
* 用户原话:「日程的设置,拉下来之后,先是默认打开时的视图,然后还有显示哪些日历本,
* 下面是那个设置。应该把这些设置都**放在另外一个页面**里面。因为如果日历本
* 特别多的时候,它拉下来会非常长,下面的设置就看不到了,要拉下来才能看到。
* 所以点开这个设置之后啊,仍然和其他的设置页面一样,是几个,是一个列表。
* 第一个就是**显示设置**……然后紧接着就是**日历本管理**,再下面是**订阅管理**,
* 再下面是 **Dev(DAV)口令**。」
*
* 为什么必须这么改(用户给的理由,别丢):日历本可能有几十本,
* 原来把「默认视图 + 每一本一个勾选框」**铺在下拉里** ⇒ 下拉被撑得极长,
* 排在后面的「日历本管理 / 订阅 / 口令」被挤到屏幕外面。
* ⇒ 下拉只留**四行固定高度的条目**,点进各自的页面再看长内容。
* 这也让日程与本项目其它模块(观影/旅程/任务)的设置**手感一致**:
* 三处都是"☰ → 一串列表"。
*
* 四行(顺序是用户点名的):
* ① 显示设置 → `calendar/display`(默认视图 + 显示哪些本子)★ 本批新增的独立页
* ② 日历本管理 → `calendar/calendars`(改名/换色/共享/导入)
* ③ 订阅管理 → `calendar/calendars#caldav`(只读订阅进来的外部日历)
* ④ DAV 口令 → `settings/me#dav`(日历 + 通讯录共用那一把)
*
* 用法(require 前 set 好):
* $setCurrent 当前页 key('view'|'books'|'dav')用来高亮;
* '' 也行(比如从旅程/观影页挂过来时没有当前项)
*/
$setCurrent = (string)($setCurrent ?? '');
$setOn = static function (string $key) use ($setCurrent): string {
return $setCurrent === $key ? ' class="on"' : '';
};
$setIcon = static function (string $name): string {
$paths = [
'view' => '<rect x="2" y="3.2" width="12" height="10.4" rx="1.6"/><path d="M2 6.4h12M5.4 1.6v3.2M10.6 1.6v3.2"/>',
'books' => '<path d="M2.6 3.2h4v9.6h-4zM9.4 3.2h4v9.6h-4z"/>',
'sub' => '<path d="M8 2.4v11.2M2.4 8h11.2" />',
'dav' => '<circle cx="8" cy="8" r="5.6"/><path d="M2.4 8h11.2M8 2.4c1.8 2 2.6 3.7 2.6 5.6S9.8 12 8 13.6C6.2 11.6 5.4 9.9 5.4 8S6.2 4.4 8 2.4z"/>',
];
$d = $paths[$name] ?? '';
if ($d === '') {
return '';
}
return '<svg class="mi" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"'
. ' fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"'
. ' stroke-linejoin="round">' . $d . '</svg>';
};
?>
<details class="movie-set menu cal-set">
<summary class="btn btn-icon" title="日程设置" aria-label="日程设置"><svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/></svg></summary>
<div class="menu-list cal-set-list">
<?php /* ⚠️ 这里**只放列表**(不做任何表单)—— 见文件头那段「为什么必须这么改」。
表单全在各自的目标页上;下拉绝不随"日历本数量"变长。 */ ?>
<a href="<?= e(url('calendar/display')) ?>"<?= $setOn('view') ?>><?= $setIcon('view') ?><span>显示设置</span></a>
<a href="<?= e(url('calendar/calendars')) ?>"<?= $setOn('books') ?>><?= $setIcon('books') ?><span>日历本管理</span></a>
<a href="<?= e(url('calendar/calendars')) ?>#caldav"<?= $setOn('sub') ?>><?= $setIcon('sub') ?><span>订阅管理</span></a>
<?php /* v24 第 22 批:口令改成通用 DAV 口令(日历 + 通讯录共用)。落到能**生成口令**的
那一页(个人设置 → DAV 同步),而不是只显示地址的日历本页。 */ ?>
<a href="<?= e(url('settings/me', [], 'dav')) ?>"<?= $setOn('dav') ?>><?= $setIcon('dav') ?><span>DAV 口令</span></a>
</div>
</details>
<?php
unset($setCurrent, $setOn, $setIcon);