Files
FamilyBook/templates/home.php
T
laoyang 4941e9db9c fix(home): 从浮层把任务标成完成时该行不消失(只修了复选框那条路)(#20)
用户第二次提同一条需求。核了线上代码:4 文件 md5 与本地一致、mtime 18:14,
部署没问题;复跑原判据仍 20/0 全绿。判据绿 + 用户能复现 ⇒ 判据有盲区。

盲区:首页把任务标成完成有**两个入口**
  ① 每行前面那颗复选框 —— home.php 末尾的脚本(上一版已修)
  ② 点整行弹浮层 → 面板里「状态」三段按钮 → assets/js/task_panel.js 的 syncRow()
上一版只改了 ①。syncRow() 只改徽章与复选框的 aria 状态,**从不删行**,
所以从面板点「已完成」后行留在原地,只把徽标换成「已完成」。
用户点的就是 ②,而原判据 grep 全文没有 panel / 浮层 / data-fb-entry —— 完全没测这条路。

改法:
- 把删行逻辑抽成 window.fbHomeTaskDrop(row, wasStatus) 放 assets/js/app.js,
  两个入口共用一份。作用域限定 [data-tk-home-host]:任务**看板**页的行也带
  data-task-row,那里勾完成只改状态、**必须保留该行**(看板按状态分栏,删了就没处看)。
- task_panel.js 的 syncRow() 在 d.status === 'done' 时调它;
  wasStatus 必须在改写 data-tk-status **之前**读,否则概述卡不知道该减「在做」还是「待办」。
- home.php 原来那份本地实现改成薄转发,避免两份实现改一处忘一处。
- 顺带修掉一条说谎的注释:原注释写"补偿了一个可撤回的提示",但代码里从来没有
  撤回按钮,只有一句 3.2 秒自动消失的 say()。
- _card_tasks.php 补记"两个入口"这件事。

子任务安全:task_panel_json() 用 panel_of 找**父任务**,回的 status 是父任务的,
所以面板里勾子任务不会把父任务那一行删掉(判据 ⑱ 专门守这条)。

判据:
- 新增 _b38_panel_done_drop.py(21 条,走真实浮层点击):
  修复前 **16 通过 / 5 失败**(⑥⑦⑧⑬⑭)→ 修复后 **21 / 0**
- 注入反证 _b38_falsify.py:把删行那两行用 if(false) 挡掉,锚点断言 count==1,
  立刻红回同样的 5 条、关键条目 ⑥⑧⑬⑭ 命中 4/4,副本自动还原
- 原 _b37_home_task_drop.py(复选框路径)仍 **20 / 0**
- _b37_regress.py **11 / 0**:看板仍改状态且**不删行**、详情页子任务复选框正常、
  空态 display 真的是 none
- home.php 内联 JS 0 块坏;app.js / task_panel.js node --check 全过
2026-10-28 18:29:59 +08:00

401 lines
25 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
/**
* 首页 —— 卡片网格(v62 用户 2026-10-18 第 3 条)。
*
* 用户原话:「今天,我们正式引入卡片的概念,在首页上,概述是卡片,日程是卡片,
* 近期任务是卡片,往年的今天也是卡片,纪念日,要有一个卡片,电影、日程、
* 读书,都要有自己的卡片。……卡片的规格,我们可以定义一下,例如,把宽度
* 定义为 8,高度可以是 2、4、6、8,这样。如果是 2x2,那么就可以一排放 4 个,
* 如果是 2x8,那就是一行放一个,类似于这样的定义。」
*
* ────────────────────────────────────────────────────────────────
* 结构变了,但**内容一字未改**:原来的三块(信息面板 / 日程+任务 / 往年的今天)
* 现在各自是一张卡片,内容块原样搬进 `templates/home/_card_*.php`。
*
* 尺寸口径(与 app.css 的 `.hc-*`、设置页文字完全一致):
* · 一排 **8 格**;宽 2 / 4 / 6 / 8 → 一排 4 张 / 2 张 / 一大一小 / 独占一排;
* · 高 2 / 4 / 6 / 8 行(一行 76px + 10px 间距)。
* · 排版完全由 CSS 网格决定:**卡片顺序 = 数组顺序**,不需要任何浮动/清浮。
*
* ⚠️ 别在模板里自己算"该多宽":宽高由 `$homeCards`(用户设置)给,
* 模板只负责把类名拼对(`hc-w4 hc-h8`)。
*
* 🔴 v71c(用户 2026-10-21)推翻了一条旧约束:「通知消息这个,也是首页的卡片,
* 可以调整顺序和大小。」—— 它原来在本文件里是一块**网格之前**的写死区块
* (v30 第 5 条当时还没有卡片体系,只能用"排在网格前"保证它在「往年的今天」上面)。
* 现在它是一张**正规卡片**(`_card_notif.php`):位置/宽高/开不开都由用户定。
* ⇒ 这条"必须在网格前"的约束**到此为止**,别再按它往网格上加写死区块。
*
* @var array $homeCards HomeCards::layout() 的结果(每项 k/w/h/n)
* @var array $cardData HomeCards::data() 取的那几张卡片的额外数据
*/
?>
<?php /* 用户 2026-10-03 第 3 条:
① 标题下显示**个人 slogan**;
② 右侧那三块统计(与我有关 / 名册总数 / 亲属连线)**去掉**;
③ 「我的资料 / 我的家谱」保留,但从文字按钮换成**图标按钮**;
④ 「名册」按钮去掉 —— 它与顶部导航条里的「名册」重复。
v62 追加第五个图标按钮:「首页卡片」布局设置(与这排图标同一套做法)。 */ ?>
<section class="hero">
<img class="hero-logo" src="<?= e(asset('/assets/icons/icon-192.png')) ?>" alt="">
<div class="hero-main">
<h1><?= e(\App\Auth::homeTitle()) ?></h1>
<?php $heroSlogan = trim((string)($mySlogan ?? '')) !== ''
? (string)$mySlogan
: trim((string)($family['motto'] ?? '')); ?>
<?php if ($heroSlogan !== ''): ?>
<p class="hero-slogan"><?= e($heroSlogan) ?></p>
<?php endif; ?>
</div>
<?php /* ⚠️ 这两个链接必须带上真实的 person id:曾经 layout.php 里写
`$me = Auth::check()` 把模板里的 $me 覆盖成布尔值,链接就变成 id=0
("补充亲属关系"点进去是空白页)。所以这里显式用 $meId。 */ ?>
<div class="hero-actions">
<a class="icon-btn" href="<?= e(url('home/cards')) ?>"
title="首页卡片(摆哪几张、多大)" aria-label="首页卡片">
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" fill="none"
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<rect x="2.4" y="2.4" width="6" height="6" rx="1.2"/>
<rect x="11.6" y="2.4" width="6" height="6" rx="1.2"/>
<rect x="2.4" y="11.6" width="6" height="6" rx="1.2"/>
<rect x="11.6" y="11.6" width="6" height="6" rx="1.2"/>
</svg>
</a>
<a class="icon-btn" href="<?= e(url('people/show', ['id' => (int)$meId])) ?>"
title="我的资料" aria-label="我的资料">
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" fill="none"
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10" cy="7" r="3.4"/><path d="M3.6 17c0-3.3 2.9-5.4 6.4-5.4s6.4 2.1 6.4 5.4"/>
</svg>
</a>
<a class="icon-btn" href="<?= e(url('kin')) ?>"
title="我的家谱" aria-label="我的家谱">
<svg viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" fill="none"
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<rect x="7.4" y="2.2" width="5.2" height="4.2" rx="1"/>
<rect x="2.2" y="13.6" width="5.2" height="4.2" rx="1"/>
<rect x="12.6" y="13.6" width="5.2" height="4.2" rx="1"/>
<path d="M10 6.4v3.4M4.8 13.6v-1.9h10.4v1.9M4.8 11.7h10.4"/>
</svg>
</a>
</div>
</section>
<?php /* ==================== 卡片网格(v62)====================
⚠️ `require` 进来的每个 `_card_*.php` 与这里**同一作用域**(共享 $tasks / $todayEvents
/ $cardData …):这是刻意的 —— 卡片的取数与首页的取数必须同源,
各自再查一遍就会出现"数字和列表对不上"那类老问题。 */ ?>
<section class="section">
<?php if (empty($homeCards)): ?>
<div class="card card-pad empty">
<b>首页卡片都被关掉了</b>
到 <a href="<?= e(url('home/cards')) ?>">首页卡片</a> 里重新勾几张出来。
</div>
<?php else: ?>
<div class="hc-grid" id="homeCards">
<?php foreach ($homeCards as $card): ?>
<?php
$hcKey = (string)($card['k'] ?? '');
if (!\App\HomeCards::exists($hcKey)) {
continue;
}
$hcDef = \App\HomeCards::TYPES[$hcKey];
$hcTpl = __DIR__ . '/home/_card_' . $hcKey . '.php';
if (!is_file($hcTpl)) {
continue; // 注册了但还没写模板:跳过,不要白屏
}
$hcMore = (string)($hcDef['route'] ?? '') !== '' ? url((string)$hcDef['route']) : '';
/* 🔴 v63(用户 2026-10-19 第 1 条):「卡片右上角的按钮,应该跟这个卡片
来自于什么模块的主题色一样才对。」
⇒ 颜色从**卡片登记表**取(`HomeCards::module()`),不再在这里硬编码白名单。
日程卡来自 calendar、任务卡来自 tasks、观影来自 movies…… 一张不漏。 */
$hcMod = \App\HomeCards::module($hcKey);
$hcColor = $hcMod !== '' ? \App\Module::color($hcMod, (int)\App\Auth::familyId()) : '';
?>
<section class="card hc-card hc-w<?= (int)$card['w'] ?> hc-h<?= (int)$card['h'] ?>"
data-hc="<?= e($hcKey) ?>" id="homeCard-<?= e($hcKey) ?>">
<div class="hc-head">
<h2 class="cinema-h2"><?= e((string)$hcDef['name']) ?></h2>
<?php if ($hcMore !== ''): ?>
<a class="btn btn-sm hd-more<?= $hcColor === '' ? ' btn-ghost' : '' ?>"
href="<?= e($hcMore) ?>"<?= $hcColor !== '' ? ' style="--hd-c: ' . e($hcColor) . ';"' : '' ?>><?= e((string)$hcDef['more']) ?></a>
<?php endif; ?>
<?php /* 🔴 v78(用户 2026-10-22):「在全部任务旁边,放一个新建任务的功能,
其实就是把任务中的新建和编辑功能,搬过来,用弹出框来处理。」
⇒ 卡片登记表里登记了 `add` 的,就在「更多」**右边**再多一颗 ——
同一套 `.hd-more` 配色,所以它是那个模块的主题色,不会抢眼。
🔴 v81(用户同日):「首页上的其他卡片也应该有这些基本功能。
比如电影观影,它有一个"新增电影"的功能;如果是纪念日,就有一个
"新增纪念日"。实际上,就是把原来模块里弹的那个框,现在拿到这边来。」
⇒ 属性改成**通用**的 `data-fb-add-open="#<弹框容器 id>"`(v81 的
`assets/js/add_modal.js` 认这个),弹框 id 由登记表 `add_modal` 给出。
⚠️ 任务的弹框仍由老脚本 `task_add.js` + `data-tk-add-open` 管
(v78 已上线,不动它以免回归);所以这里**按登记表决定挂哪个属性**,
不是一刀切换掉 —— 两种写法并存但各自有唯一来源。 */ ?>
<?php
$hcAdd = (string)($hcDef['add'] ?? '');
$hcAddModal = (string)($hcDef['add_modal'] ?? '');
?>
<?php if ($hcAdd !== '' && \App\Auth::canWrite()): ?>
<button type="button" class="btn btn-sm hd-more<?= $hcColor === '' ? ' btn-ghost' : '' ?>"
<?php if ($hcAddModal !== ''): ?>
data-fb-add-open="#<?= e($hcAddModal) ?>" aria-haspopup="dialog" aria-expanded="false"
<?php else: ?>
data-tk-add-open
<?php endif; ?>
<?= $hcColor !== '' ? ' style="--hd-c: ' . e($hcColor) . ';"' : '' ?>><?= e($hcAdd) ?></button>
<?php endif; ?>
</div>
<?php require $hcTpl; ?>
</section>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php /* ==================== 首页任务复选框的表单与脚本(v24 第 25 批 用户第 6 条)====================
「在任何地方点复选框,这个任务就完成了」—— 首页也算"任何地方"。
与任务看板同一套做法:**表单全页一份**,行上只放 data-tk-done。
⚠️ 改完**不摘掉这一行**:用户点完正想看自己刚完成什么,立刻消失会让人怀疑
"是不是点到删除了"。刷新时由服务端决定它还在不在。
⚠️ 这一段是**页面级**的(不属于某张卡片):任务卡片被关掉后它也跟着没用,
但留着无害 —— 别为了"省几行"把它挪进卡片模板里,那样卡片一换模板就丢功能。 */ ?>
<?php if (!empty($tasks) && \App\Auth::canWrite()): ?>
<form method="post" action="<?= e(url('tasks/status')) ?>" id="taskDoneForm" class="hidden-form">
<?= csrf_field() ?>
<input type="hidden" name="id" id="doneId">
<input type="hidden" name="status" id="doneStatus">
<input type="hidden" name="back" id="doneBack" value="home">
</form>
<script>
(function () {
var form = document.getElementById('taskDoneForm');
if (!form) { return; }
var staticWord = {todo: '待办', doing: '进行中', done: '已完成'};
function say(msg, ok) {
var host = document.querySelector('.flash');
if (!host) {
host = document.createElement('div');
host.className = 'flash';
var main = document.querySelector('main') || document.body;
main.insertBefore(host, main.firstChild);
}
var el = document.createElement('div');
el.className = 'flash-item' + (ok ? '' : ' err');
el.setAttribute('role', 'status');
el.textContent = msg;
host.insertBefore(el, host.firstChild);
setTimeout(function () { if (el.parentNode) { el.parentNode.removeChild(el); } }, 3200);
}
Array.prototype.forEach.call(document.querySelectorAll('.tk-check[data-tk-done]'), function (b) {
b.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var now = b.getAttribute('data-tk-status') || 'todo';
var next = (now === 'done') ? 'todo' : 'done';
/* 🔴 工单 #20:先把"改之前的状态"存下来 —— 后面删行时要用它决定
「概述」卡里该减「件在做」还是「件待办」。
⚠️ 不能在删行时再读 `data-tk-status`:那时它已经被下面改成 `d.status` 了。 */
var was = now;
document.getElementById('doneId').value = b.getAttribute('data-tk-done');
document.getElementById('doneStatus').value = next;
document.getElementById('doneBack').value = b.getAttribute('data-tk-back') || 'home';
if (!window.fetch || !window.FormData) { form.submit(); return; }
b.disabled = true;
fetch(form.getAttribute('action'), {
method: 'POST', body: new FormData(form), credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
}).then(function (r) { return r.json().catch(function () { return null; }); })
.then(function (d) {
b.disabled = false;
if (!d || !d.ok) { form.submit(); return; }
b.setAttribute('data-tk-status', d.status);
b.classList.toggle('is-done', d.status === 'done');
b.setAttribute('aria-pressed', d.status === 'done' ? 'true' : 'false');
var row = b.closest('[data-task-row]');
var badge = row ? row.querySelector('.kb-st') : null;
if (badge) {
badge.className = 'kb-st is-' + d.status + ' is-static';
badge.textContent = staticWord[d.status] || d.word;
}
/* 🔴🔴 工单 #20(用户 2026-10-28):「首页,近期任务,如果勾选了完成,
应该刷新一下这个列表,把完成的删掉。」
⇒ 在这里把这一行**从列表里摘掉**,并同步概述卡的计数。
⚠️ 判据是**服务端返回的 d.status**,不是我点之前算出的 next:
`tasks/status` 是幂等接口(传 done 再来一次还是 done),
拿 d.status 判断才不会"用户手快连点两下,第二下又把行删了"。
⚠️ 首页这份列表只收 doing + todo(index.php:839),done 本来就不在其中
⇒ 手动刷新时它确实会消失,所以"删行"与刷新后的结果**一致**,
不是前端自己造了个假象。
⚠️ 当年这里是刻意**不删**的(原注释:怕用户以为"点到删除了")——
用户现在明确要它消失,所以改掉。
🔴 补偿方式是**一句说明去哪了**(下面 say() 那段),**不是"撤回按钮"**:
先前这里的注释写着"补偿了一个可撤回的提示",但代码里从来没有撤回按钮,
只有一句 3.2 秒自动消失的 `say()`。
注释描述不存在的功能,比没有注释更糟 —— 下一个人会去找它、找不到、然后再写一个。
(撤回的正路:卡片右上角「全部」→ 任务看板,那里 done 的还在,能改回来。) */
if (d.status === 'done' && row) {
dropTaskRow(row, was);
/* 🔴 工单 #20:行被摘掉了,必须**明说**它去哪了 ——
当年"不删行"的理由就是"怕用户以为点到删除了"(见上面原注释)。
删了行就要把这条路补上:告诉他已从近期任务移除,
而卡片右上角本来就有「全部」能找回($hcMore → tasks)。
⚠️ 不能只沿用服务端那句 `已标成「已完成」` —— 那会让人以为行还在,
下一眼发现没了反而更慌。 */
say((d.msg || '已标成「已完成」。') + '(已从近期任务移出,去「全部」里能看到)', true);
} else {
say(d.msg || '已更新。', true);
}
}).catch(function () { b.disabled = false; form.submit(); });
});
});
/* ============================================================
把一条刚完成的任务行从首页列表里摘掉(工单 #20)
============================================================
🔴🔴 实现在 `assets/js/app.js` 的 `window.fbHomeTaskDrop()` —— 这里只是转发。
**不要**把那段逻辑抄回来:同一个动作在首页有两个入口
(① 这颗复选框 ② 点整行弹浮层、在面板里点「已完成」,
脚本是 task_panel.js),抄两份必然只改一份。
第一版就是抄在本地,于是"从复选框勾会删行、从面板点不删",
用户复现了后者,而我的判据只测前者 ⇒ 20/0 全绿也照样是假绿。
⚠️ 保留这个薄封装是因为下面两处调用都读 `was`,就地传更清楚;
同时也让 home.php 里"删行"这件事还有一处可 grep 的落点。 */
function dropTaskRow(row, wasStatus) {
if (typeof window.fbHomeTaskDrop !== 'function') { return; }
window.fbHomeTaskDrop(row, wasStatus);
}
})();
</script>
<?php endif; ?>
<?php /* ==================== 「新建任务」弹框(v78 用户 2026-10-22)====================
用户原话:「在全部任务旁边,放一个新建任务的功能,其实就是把任务中的新建和编辑功能,
搬过来,用弹出框来处理。」
⚠️ 与任务页(`templates/tasks/index.php`)**共用同一份** `tasks/_add_modal.php` ——
两处各写一张表单,字段必然有一天不同步(`_edit_form.php` 头部记着同一条教训)。
⚠️ `$tkAdd` 只在「近期任务」卡片真的摆在首页、且当前账号可写时才由路由准备
(见 index.php 的 home 分支):卡片被关掉就没有入口,也就不该白跑三条查询。
⚠️ 新建完回**首页**(`back=home`),卡片上立刻能看到刚建的那条 ——
用户要的是"不离开首页就把事办了"。 */ ?>
<?php if (!empty($tkAdd)): ?>
<?php
$tkAddBack = 'home';
$tkAddBoards = (array)($tkAdd['boards'] ?? []);
$tkAddCats = (array)($tkAdd['categories'] ?? []);
$tkAddPeople = (array)($tkAdd['people'] ?? []);
require __DIR__ . '/tasks/_add_modal.php';
unset($tkAddBack, $tkAddBoards, $tkAddCats, $tkAddPeople, $tkAdd);
?>
<?php endif; ?>
<?php /* ==================== 「新增电影」「新建纪念日」弹框(v81 用户 2026-10-22)============
用户原话:「首页上的其他卡片也应该有这些基本功能。比如电影观影,它有一个"新增电影"
的功能;如果是纪念日,就有一个"新增纪念日"。实际上,就是把原来模块里弹的那个框,
现在拿到这边来。目的就是为了在首页上就能快速完成一定工作,而不用进到模块里面。」
⚠️ 与模块页**共用同一份模板**(`movies/form.php` / `days/form.php`),不另写表单:
字段、校验、服务端契约、附件上传全部同一套,永远不会不同步。
⚠️ 三件事必须一起给,否则这张弹框"长出来了但用不了":
① `$fbAddModalAttr = true` —— 让模板挂 `data-fb-add-modal`(通用开关认它);
② 模板需要的**数据**(名册 / 影院 / 分类)由路由按"卡片在不在"准备,见 index.php;
③ 表单里的 `back=home`(模板按 `$fbAddModalAttr` 自动输出)—— 提交完回首页。
⚠️ 只在对应卡片**真的摆在首页**且账号可写时才渲染:卡片被关掉就没有入口,
不该白跑 People::roster / Cinema::all 这些查询。 */ ?>
<?php if (!empty($homeAddModals) && \App\Auth::canWrite()): ?>
<?php foreach ((array)$homeAddModals as $hcAddKey => $hcAddData): ?>
<?php
$fbAddModalAttr = true; // 通用开关属性(见上面第 ① 条)
/* 每个片段自己兜底变量:模板片段不许偷宿主的变量(本项目重复踩过的坑)。 */
$m = null; $errors = [];
if ($hcAddKey === 'movies') {
$people = (array)($hcAddData['people'] ?? []);
$cinemas = (array)($hcAddData['cinemas'] ?? []);
$mvModalOnly = true; // 不输出 page-head / 设置菜单
$mvModalForce = true; // 强制弹框壳(新增态本来就是,写明更稳)
$mvOpen = false; // 初始关闭,点卡片按钮才开
require __DIR__ . '/movies/form.php';
unset($mvModalOnly, $mvModalForce, $mvOpen, $people, $cinemas, $m, $errors);
} elseif ($hcAddKey === 'days') {
$anModalOnly = true; $anOpen = false; $anModal = true;
$anEmbed = false; $anShell = true;
/* ⚠️ 这三个必须逐个显式给:`days/form.php` 自己会 `$kindsFull ?? 查库` 兜底,
但**兜底会多跑一次查询**,而且与模块页那份不一定同源。
这里直接给它容器已经查好的那份(路由里按"卡片在不在"准备)。 */
$editing = null;
$people = (array)($hcAddData['people'] ?? []);
$kindsFull = (array)($hcAddData['kindsFull'] ?? []);
$kinds = array_column($kindsFull, 'name');
require __DIR__ . '/days/form.php';
unset($anModalOnly, $anOpen, $anModal, $anEmbed, $anShell,
$editing, $people, $kindsFull, $kinds);
} elseif ($hcAddKey === 'agenda') {
/* 🔴 v82(用户 2026-10-23):「日程,没有新建日程按钮……」
⇒ 首页「今天的日程」卡片右上角一颗「新建日程」,点开的是**与日程页同一张**
弹框(`templates/calendar/_add_modal.php`,v82 从 calendar/index.php 抽出来的)。
⚠️ `$calCtx['back'] = 'home'` 是这个分支**唯一**与日程页不同的地方:
提交完回首页(`calendar/event_add` 里认这个白名单值),
用户不用离开首页就能看见刚建的那条。
⚠️ `ym`/`anchor` 用"今天"——首页卡片本来就只显示今天,
新建的日程默认落在今天才符合直觉(日程页那边才用页面锚点)。 */
$calCanNew = true; // 进到这里已经过了 Auth::canWrite()
$calWritable = (array)($hcAddData['writable'] ?? []);
$calCtx = [
'ym' => date('Y-m'),
'view' => 'day', // 首页日程卡就是"日视图那样"
'anchor' => date('Y-m-d'),
'new_at' => '',
'back' => 'home',
];
require __DIR__ . '/calendar/_add_modal.php';
/* ⚠️ 🔴 v82 再修:这段 unset 清单**必须与片段真实赋过的名字一致**。
第一版抄的是模块页的老变量名(`$writable/$ym/$view/$anchor/$newAt`)——
那是片段改用 `$cal*` 前缀**之前**的清单,抄过来有两个问题:
① 那五个名字**片段已经不再赋值**,unset 它们等于**误杀宿主** ——
首页后面还有别的卡片在用(尤其 `$writable`);
② 片段真正留下的 `$cal*` 反而没清干净。
⇒ 现在按片段的真实输出逐个清:`$calCanNew/$calWritable/$calCtx`
三个入参 + 片段内部那 7 个 `$cal*` + `_event_fields.php` 那 8 个。
⚠️ 加新变量时**先回片段末尾那段清单对一遍**再往这里抄。 */
unset($calCanNew, $calWritable, $calCtx,
$calYm, $calView, $calAnchor, $calNewAt, $calBack, $calThisYm, $calBooks,
$firstCal, $hasPersonal, $defDay, $defHM, $defEndH, $evFields,
$evAllDay, $evMvStyle,
/* ⚠️ 片段自己会设 `$fbAddModalAttr`(按 `$calBack` 判定);
外层循环每轮也要设它,不清掉会让第二轮拿到上一轮的值。 */
$fbAddModalAttr);
} elseif ($hcAddKey === 'contacts') {
/* 🔴 v82(用户 2026-10-23):「名册,也没有新增成员这个按钮。」
⇒ 首页「联系人」卡片右上角一颗「新增成员」,点开的是**与名册页同一张**
表单(`templates/people/form.php` 新加的弹框形态)。
⚠️ 这套开关(`$pfModalOnly` / `$pfModal` / `$pfOpen`)是 v82 才加到
people/form.php 的,之前那页只能整页打开 —— 所以这里**必须**逐个给全,
漏一个就会退回"整页表单塞进卡片里"那种坏形态。
⚠️ `$person = null` + `$isEdit = false` 是新增态的判据(模板第一行按
`$person['id']` 算 $isEdit),不要靠"不传"来默认。 */
$pfModalOnly = true;
$pfModal = true;
$pfOpen = false;
$person = null;
$errors = [];
$canEditAll = true;
$isSelf = false;
$jobRows = [[]]; // 工作经历先给一行空白(与 people/new 一致)
$myNote = '';
$myTags = [];
$tagChoices = (array)($hcAddData['tagChoices'] ?? []);
$myNick = '';
$contacts = [];
require __DIR__ . '/people/form.php';
unset($pfModalOnly, $pfModal, $pfOpen, $person, $errors, $canEditAll, $isSelf,
$jobRows, $myNote, $myTags, $tagChoices, $myNick, $contacts);
}
unset($fbAddModalAttr);
?>
<?php endforeach; ?>
<?php endif; ?>