Files
FamilyBook/templates/calendar/display.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

99 lines
5.2 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
/**
* 日程「显示设置」——一页只放**个人显示偏好**(v51 用户 2026-10-15 第 6 条)。
*
* 用户原话:
* 「日程的设置,拉下来之后,先是默认打开时的视图,然后还有显示哪些日历本,
* 下面是那个设置。应该把这些设置都**放在另外一个页面**里面。因为如果日历本特别多的时候,
* 它拉下来会非常长,下面的设置就看不到了,要拉下来才能看到。
* 所以点开这个设置之后啊,仍然和其他的设置页面一样,是几个,是一个列表。
* 第一个就是**显示设置**,把默认是打开看什么视图和显示哪些日历本放在另外一个页面,
* 专门用来设置显示。然后紧接着就是**日历本管理**,再下面是**订阅管理**,
* 再下面是 **Dev(DAV)口令**。」
*
* ⇒ 结构(与旅程/观影/任务模块的"设置下拉 = 一串列表页"完全一致):
* ☰ 下拉里只有四行:显示设置 / 日历本管理 / 订阅管理 / DAV 口令
* · 本模板 = **第一行进去的那页**(默认视图 + 显示哪些日历本);
* · 其余三行各自去既有页面(calendar/calendars 的对应区块)。
*
* ⚠️ 为什么必须单独成页(用户给了理由,值得照抄):日历本可能有几十本,
* 原来把它们**铺在下拉里**,下拉就会拉到屏幕外面去,
* 排在后面的"日历本管理 / 订阅 / 口令"全被挤到看不见的地方 ——
* 用户原话"下面的设置就看不到了,要拉下来才能看到"。
* 挪进独立页面后,下拉永远只有四行(高度恒定),点进去再看长长的一列。
*
* 变量:
* $cals 我可见的日历本(含 perm / color / kind / shared)
* $hiddenCals 我关掉的那些本子 id
* $view / $anchor 回跳用(保持当前视图与锚定日)
*/
$canWrite = \App\Auth::canWrite();
$myCals = (array)($cals ?? []);
$hiddenIds = array_map('intval', (array)($hiddenCals ?? []));
$backView = (string)($view ?? 'month');
$me = (int)\App\Auth::personId();
?>
<div class="page-head">
<h1>显示设置</h1>
<span class="sub">默认看哪个视图、日历上显示哪几本</span>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => $backView])) ?>">返回日程</a>
<?php $setCurrent = 'view'; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
<form method="post" action="<?= e(url('calendar/prefs')) ?>" class="cal-set-page">
<?= csrf_field() ?>
<input type="hidden" name="back_view" value="<?= e($backView) ?>">
<?php /* ⚠️ 这个隐藏标记是**必须**的(从原来的下拉里原样搬过来):
HTML 里"一个都没勾"会让浏览器整个省掉 `show[]` 键,
服务端就分不清「用户想全关掉」与「这次请求没打算改显隐」——
没有它的话,将来任何只改默认视图的提交都会把所有日历本静默藏掉。 */ ?>
<input type="hidden" name="show_present" value="1">
<div class="card card-pad">
<h2 class="cinema-h2">默认打开时看
<span class="hint">打开日程页默认落在哪个视图</span></h2>
<div class="cal-set-views">
<?php foreach (\App\Calendar::VIEWS as $vk => $vw): ?>
<label class="cal-set-view">
<input type="radio" name="view" value="<?= e($vk) ?>"
<?= \App\Calendar::defaultView($me) === $vk ? 'checked' : '' ?>>
<span><?= e($vw) ?>视图</span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="card card-pad" style="margin-top:14px">
<h2 class="cinema-h2">显示哪些日历本
<span class="hint">关掉的本子只是不在日历上画出来,数据都还在</span></h2>
<?php if (!$myCals): ?>
<p class="small muted" style="margin:0">还没有日历本。</p>
<?php else: ?>
<div class="cal-set-books">
<?php foreach ($myCals as $c): $cid = (int)$c['id']; ?>
<label class="cal-set-book">
<input type="checkbox" name="show[]" value="<?= $cid ?>"
<?= in_array($cid, $hiddenIds, true) ? '' : 'checked' ?>>
<span class="color-preview" style="background: <?= e((string)$c['color']) ?>;"></span>
<span class="cal-name"><?= e((string)$c['name']) ?></span>
<?php if ((string)$c['kind'] !== 'custom'): ?>
<span class="node-badge">系统</span>
<?php elseif ((int)$c['shared'] === 1): ?>
<span class="node-badge"><?= ($c['perm'] ?? '') === 'write' ? '共享·可写' : '共享·只读' ?></span>
<?php endif; ?>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
<p class="small muted" style="margin:10px 0 0">
观影、旅行、有截止时间的任务会自动出现在这里,不用另建本子。
</p>
<div class="btn-row" style="margin-top:14px">
<button class="btn" type="submit"<?= $canWrite ? '' : ' disabled' ?>>保存显示设置</button>
<a class="btn btn-ghost" href="<?= e(url('calendar', ['view' => $backView])) ?>">取消</a>
</div>
</div>
</form>
<?php unset($myCals, $hiddenIds, $backView, $me, $canWrite); ?>