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

285 lines
21 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
/**
* 日程 · 「新建日程」弹框(v82 从 templates/calendar/index.php 抽出来的公共片段)。
*
* 用户原话(v82,2026-10-23):「日程,没有新建日程按钮……」
* 实测确认:用户说的是**首页日程卡片**上没有按钮(日程模块页的 `#calAddBtn` 一直在)。
* 要把「新建日程」搬上首页,最省事也最不会不同步的做法是:
* **把日程模块页那个弹框原样搬过来共用**,与 v81 的观影/纪念日同一套思路。
*
* 用户 2026-10-22 原话(v81 的动机,同样适用这里):
* 「就是把原来模块里弹的那个框,现在拿到这边来。目的就是为了在首页上就能
* 快速完成一定工作,而不用进到模块里面。」
*
* ─────────────────────────────────────────────────────────────────────────
* ⚠️⚠️ 抽公共片段的三条铁律(本项目踩过的,不是理论):
*
* ① **表单契约一个字不能改**:字段名 / CSRF / action(`calendar/event_add`)
* 全部照旧 —— 这个片段被两处 require,改一处就是两处一起变。
*
* ② 🔴🔴 **抽完之后立刻回部署脚本扫静态断言**:
* `_tooling/familybook_deploy.sh` 里有一批断言是拿**老文件名**去 grep 的
* (`id="calPicker"` / `id="cbDot"` / `class="cs-dot" style="background:`
* / `<span class="mv-lb">日历本</span>` / `$evMvStyle = true`)。
* 片段抽走后这些串**只在这个新文件里**,而断言还在查 `calendar/index.php`
* ⇒ 正向断言当场中止部署、负向断言恒真没牙齿(v78 原话:"部署当场中止,
* 看着像断言写错,其实是目标文件漂移")。
* ⇒ v82 已把这 5 条改成 `_cal_add_src` = 两个文件拼起来一起查。
*
* ③ **变量由调用方给全**,片段自己不许偷宿主的变量(本项目重复踩过)。
* 本片段需要的变量见文件末尾那段 `@var` 说明(含兜底逻辑)。
*
* ─────────────────────────────────────────────────────────────────────────
* @var bool $calCanNew 能不能新建(`Auth::canWrite() && $writable` 非空);片段内自己再兜一次
* @var array $calWritable 可写的日历本清单(含 id/name/color/kind)
* @var array $calCtx 上下文:ym / view / anchor / new_at / back / thisYm
* `back` 只认 'home' 或 ''('' = 回日程模块),服务端白名单取值
*/
/* ---------------------------------------------------------------- 兜底 */
/* 🔴🔴 v82 修正(2026-10-23):片段**自己读进来的变量一律加 `cal` 前缀**。
---------------------------------------------------------------------------
第一版是照模块页的老变量名写的(`$ym / $view / $anchor / $newAt / $writable`),
想着"沿用模块页的变量名最省事" —— 那是**整页**时代的写法,抽成片段就错了:
· require 与宿主**共享一个作用域** ⇒ 片段里那句
`$anchor = (string)($calCtx['anchor'] ?? date('Y-m-d'));`
直接改写了**模块页自己的 `$anchor`**;
· 今天两者恰好同值(模块页就是把它自己的值传进 `$calCtx` 的),
所以看不出问题 —— 但只要有一天首页传的 anchor 与模块页不同
(首页固定"今天",模块页是页面锚点),模块页后半页就会静默错位:
页头日期、翻页链接、时间网格的 `.tg-now` top 全跟着变。
· `$newAt` / `$writable` 同理,其中 `$writable` 还被首页 unset 过,
等于在首页里**把宿主的可写本清单干掉**(首页后面别的卡片要用)。
这 5 个名字(anchor / newAt / writable / view / ym)都在
`_tooling/_fb_fragment_scope_check.py` 的"宿主高频名"表里,是那个检查器
2026-10-03 就定下的规则(由来见它文件头:`unset($canWrite)` 把账本页炸成
一片 Warning,`php -l` 与 HTTP 200 全绿,只是页面排版坏了)。
⇒ 现在片段只碰 `$cal*`,宿主那套名字一个都不写。
⚠️ 别为了"读起来熟悉"改回去 —— 片段与宿主同名是这类 bug 的唯一入口。 */
$calCanNew = (bool)($calCanNew ?? false);
$calWritable = (array)($calWritable ?? []);
if (!$calCanNew || !$calWritable) {
return; /* 没得写就整块不输出(与模块页那个 if 等价) */
}
$calCtx = (array)($calCtx ?? []);
$calYm = (string)($calCtx['ym'] ?? date('Y-m'));
$calView = (string)($calCtx['view'] ?? 'month');
$calAnchor = (string)($calCtx['anchor'] ?? date('Y-m-d'));
$calNewAt = (string)($calCtx['new_at'] ?? '');
$calBack = (string)($calCtx['back'] ?? '');
$calThisYm = date('Y-m');
$calBooks = $calWritable; /* 片段内统一用这个短名遍历可写的本子 */
/* 🔴 v82:通用弹框开关(`assets/js/add_modal.js`)认的是 `data-fb-add-modal`。
⚠️ 必须**按来源**决定挂不挂:
· 首页内嵌(`$calBack === 'home'`)→ 挂上,由 `add_modal.js` 管开合;
· 日程模块页 → **不挂**,那页有自己的一套内联 JS(`calAddBtn` 的 onclick
+ 遮罩/✕/Esc 全在 `calendar/index.php` 底部),挂上会**绑两遍**:
点一次开两次、关两次(`add_modal.js` 的注释里写着同一个坑)。
⇒ 判据用 `$calBack`(首页那条路才会传 'home'),不要用"页面里有没有别的弹框"。 */
$fbAddModalAttr = ($calBack === 'home');
?>
<div class="cal-modal" id="calAddCard" role="dialog" aria-modal="true"
aria-label="新建日程" hidden<?= $fbAddModalAttr ? ' data-fb-add-modal' : '' ?>>
<div class="cal-modal-panel">
<div class="cal-modal-head">
<b>新建日程</b>
<button type="button" class="cal-modal-x" id="calAddCancel"
aria-label="关闭"<?= $fbAddModalAttr ? ' data-fb-add-close' : '' ?>>✕</button>
</div>
<form method="post" action="<?= e(url('calendar/event_add')) ?>"
enctype="multipart/form-data" id="calAddForm">
<?= csrf_field() ?>
<input type="hidden" name="ym" value="<?= e($calYm) ?>">
<input type="hidden" name="back_view" value="<?= e($calView) ?>">
<input type="hidden" name="back_anchor" value="<?= e($calAnchor) ?>">
<?php /* 🔴 v82:从**首页**那张弹框提交时带 `back=home`,无 JS 兜底回首页
(卡片上立刻能看到刚建的那条)—— 与 v81 观影/纪念日同一套约定。
⚠️ 服务端**白名单取值**(只认 'home'),绝不把 POST 字符串直接当跳转地址
(开放重定向)。见 index.php 的 `calendar/event_add`。
⚠️ 模块页那张不输出这个字段(`$calBack` 为 ''),行为与以前完全一致。 */ ?>
<?php if ($calBack === 'home'): ?>
<input type="hidden" name="back" value="home">
<?php endif; ?>
<div class="cal-modal-body">
<?php /* ⚠️ v44:`.mv-grid` 在这里就打开 —— 下面的「日历本」行
与「日期范围」字段块都要走观影式排版(标签右对齐 + 冒号)。
原来 `.mv-grid` 是包在字段块那一小段里的,日课本在它**外面**,
于是"日历本:下拉"落不到这套排版上(正是用户报的"不在一行")。 */ ?>
<div class="mv-grid">
<?php /* 🔴🔴 v44(用户 2026-10-12 第 1 条):「新建日程,放在哪本,改成日历本后面加冒号。
现在这句话和日历本下拉框还有前面的颜色,仍然不在一行,
我建议把颜色放在下拉框里面,就跟电影的影院选择一样,
让日历本这三个字和下拉框在一条线上。」
三件事:
① 标签改「日历本」(用户点名要这三个字),冒号由观影式排版的
`.mv-row > .mv-lb::after` 给(与观影/任务一致);
② **颜色不再是下拉框外的一个色块**,而是像"影院选择"那样
**画进下拉里**(每行左边一个该本子的色点)。
原生 `<option>` 画不出元素 ⇒ 换成**自定义下拉**
(复用全站的 `.cs` 组件:按钮 + 弹出列表 + 搜索),
真值仍写进隐藏的 `<input name="calendar_id">`(表单契约不变)。
③ 因为换成了一个自绘的下拉,三者天然在同一行 ——
不再有"标签一行、色块+下拉下一行"的错位。
⚠️ 无 JS 兜底:`.cs` 里**仍然保留**一个原生 `<select name="calendar_id">`
(视觉收起、但它是真的提交字段)—— JS 没跑时它就是那个下拉,
与 `_datetime.php` 的"隐藏 date 当值载体"是同一套思路
(v43 那条"没有盒子的元素定位无效"的教训:这里用 `position:absolute;
width:1px` 而非 `display:none`,保留可聚焦、可提交)。
⚠️ 组件 id 保持 `cinemaPicker/csBtn/csMenu/csDot/csText` **不合适**
(那是观影的编号)—— 用 `calPicker/cb*` 一套独立的,
免得将来两处互相 getElementById 抢。
⚠️ e2e / 断言里按 `#ce-cal` 定位的写法**必须继续成立**:
原生 select 的 id 保持 `ce-cal`(只是被视觉收起)。 */ ?>
<?php /* 🔴 v24 第 21 批 第 7 条:**默认必须选"我自己的普通日历本"**。
原来直接取 `$calBooks[0]` —— 而顺序是按 id,观影 / 旅行两本
通常排在最前面。于是用户打开"新建日程"、直接打字、点"加上",
建出来的其实是一条**观影记录**(或旅程)。
这不是"显示不对",是**建错东西**,而且事后很难发现
—— 用户以为日历里多了条日程,其实观影列表里多了一条记录。
所以:优先把自己建的 kind='custom' 本子排在前面当默认;
没有普通本时才退到第一个可写的本子。 */
$firstCal = null;
foreach ($calBooks as $c) {
if ((string)($c['kind'] ?? '') === 'custom') { $firstCal = $c; break; }
}
if ($firstCal === null) { $firstCal = $calBooks ? $calBooks[0] : null; }
/* ⚠️ 模板里**再兜一道**"快照":路由只会在**页面级**补一次本子,
而这张表单是页面渲染时定格的 —— 万一补本失败(DB 异常等),
这里至少还能看出"没有可写的普通本",不至于把"选中了观影本"
当成正常状态。 */
$hasPersonal = $firstCal !== null
&& (string)($firstCal['kind'] ?? '') === 'custom'; ?>
<div class="mv-row">
<span class="mv-lb">日历本</span>
<div class="cs cal-book-cs" id="calPicker" data-open="0">
<button type="button" class="cs-btn" id="cbBtn" aria-haspopup="listbox" aria-expanded="false">
<span class="cs-dot" id="cbDot"
style="background: <?= e((string)($firstCal['color'] ?? '#4A7C82')) ?>;"></span>
<span class="cs-text" id="cbText"><?= e((string)($firstCal['name'] ?? '未选择')) ?></span>
<span class="cs-caret" aria-hidden="true">▾</span>
</button>
<div class="cs-menu" id="cbMenu" hidden role="listbox">
<div class="cs-list" id="cbList">
<?php foreach ($calBooks as $c): $ro = \App\Calendar::isReadonlyKind((string)$c['kind']);
$isDef = $firstCal && (int)$c['id'] === (int)$firstCal['id']; ?>
<?php /* v24 第 21 批 第 7 条:纯派生的本子(农历/节假日/传统节日/节气)
列出来但**禁用** —— 直接不列的话,用户会以为"我的本子丢了";
列为可选项又会让他白选一次再吃一个错误提示。
禁用 + 一句 title 说明,是这个取舍里最不费解的一种。
观影 / 旅行**不禁用**:它们可写(加一条 = 建一条观影/旅程)。 */ ?>
<button type="button" class="cs-item<?= $ro ? ' is-disabled' : '' ?>"
role="option"<?= $ro ? ' disabled' : '' ?>
data-id="<?= (int)$c['id'] ?>"
data-color="<?= e((string)$c['color']) ?>"
data-text="<?= e((string)$c['name']) ?>"
<?= $ro ? 'title="系统自动生成,不能直接加日程"' : '' ?>>
<span class="cs-dot" style="background: <?= e((string)$c['color']) ?>;"></span>
<span><?= e((string)$c['name']) ?><?= $ro ? '(自动生成)' : '' ?></span>
</button>
<?php endforeach; ?>
</div>
<?php /* 一个都没匹配上时露出来(平时 hidden,不占高度) */ ?>
<div class="cs-empty" id="cbEmpty" hidden>没有匹配的日历本</div>
</div>
<?php /* ⚠️⚠️ 这是**真的提交字段**(不是"只给 JS 用"的影子):
自定义下拉只是它的外观;JS 没跑时它就是那个原生下拉。
视觉收起用 position:absolute + 1px(**不用 display:none**)——
后者会让表单校验时不可聚焦(v37 那次的坑)。
桌面无 JS 时它贴在按钮下方 1px 处、可点,仍能选。 */ ?>
<select class="select cal-book-native" id="ce-cal" name="calendar_id" aria-label="日历本">
<?php foreach ($calBooks as $c): $ro = \App\Calendar::isReadonlyKind((string)$c['kind']);
$isDef = $firstCal && (int)$c['id'] === (int)$firstCal['id']; ?>
<?php /* 🔴 v82:把 `kind` 也输出到 DOM(`data-kind`)。
为什么非要加:默认本挑错的后果是"建出一条观影记录",而
观影本 / 旅行本**都是可写的**(`$ro` 为 false),
光凭"不是禁用项 / 没有『(自动生成)』后缀"根本分辨不出它。
探针原本只能退化成"只要不是自动生成就算对" —— 那是**假绿**:
把默认本改成 `$calBooks[0]`(往往就是观影本)照样通过。
(2026-10-23 反向验证第 10 条当场抓到,见 _fb_b21_ui.py)
⇒ 有了 `data-kind`,判据就能写成"选中项的 kind 必须是 custom"。
⚠️ 别为了省一层缩进把它放进 `data-*` 之外的地方:
JS / 探针都按 `option.getAttribute('data-kind')` 读。 */ ?>
<option value="<?= (int)$c['id'] ?>"
data-color="<?= e((string)$c['color']) ?>"
data-kind="<?= e((string)($c['kind'] ?? '')) ?>"
<?= $isDef ? ' selected' : '' ?>
<?= $ro ? 'disabled title="系统自动生成,不能直接加日程"' : '' ?>><?= e((string)$c['name']) ?><?= $ro ? '(自动生成)' : '' ?></option>
<?php endforeach; ?>
</select>
<?php /* 兼容旧断言/旧 JS:外面那个独立色块曾经叫 ce-cal-dot。
现在颜色已经画进下拉里、不再需要它 —— 保留一个**隐藏**的同名元素,
免得旧探针 document.getElementById('ce-cal-dot').style 直接抛错。 */ ?>
<span class="cal-book-dot" id="ce-cal-dot" hidden
style="background: <?= e((string)($firstCal['color'] ?? '#4A7C82')) ?>;"></span>
</div>
</div>
<?php if (!$hasPersonal): ?>
<span class="tip" style="display:block;margin-top:6px">
⚠️ 你还没有**自己的日程本**,上面选的是一本"派生本"
(加进去会变成观影 / 旅程记录)。去
<a href="<?= e(url('calendar/calendars')) ?>">日历本</a> 建一本吧。
</span>
<?php endif; ?>
<?php /* v24 第 6 条:字段按 iCalendar / CalDAV 的 VEVENT 定(见 _event_fields.php)。
新建时「全天」默认**不勾**(用户原话)。开始时间默认:
带了 new_at 就用它(点日历的那一格),否则今天 09:00。
🔴 v43(用户 2026-10-11 第 5 条):「新建日程页面的排版还是不对,
也是一样的:把标题放在前面,后面加冒号,再跟上输入框。」
⇒ 用观影/行程那套 `.mv-grid`(标签 62px 右对齐 + CSS ::after 冒号 +
控件占满剩余宽度)包住字段块。
`$evMvStyle = true` 让字段块输出 `.mv-row` / `.mv-lb`
而不是 `.field` / `<label>`;编辑页不传,保持原两列。 */ ?>
<?php
// 点的位置优先;没点就"月视图当月 → 该月 1 号 / 本月 → 今天"的 09:00
$defDay = $calNewAt !== '' ? substr($calNewAt, 0, 10)
: (($calYm === $calThisYm ? date('Y-m-d') : $calYm . '-01'));
$defHM = $calNewAt !== '' && strlen($calNewAt) > 10
? substr($calNewAt, 11, 5) : '09:00';
$defEndH = sprintf('%02d', (min(23, (int)substr($defHM, 0, 2) + 1)));
$evFields = ['starts_at' => $defDay . ' ' . $defHM . ':00',
'ends_at' => $defDay . ' ' . $defEndH . ':' . substr($defHM, 3, 2) . ':00'];
$evAllDay = false;
/* ⚠️ v44:`.mv-grid` 容器**已经在上面日课本那一行之前打开了**
(日课本自己也要走观影式排版)—— 这里只 require 字段块,
不要再包一层,否则会出现嵌套的第二个 `.mv-grid`。 */
$evMvStyle = true; // 观影式排版(用户第 5 条)
require __DIR__ . '/_event_fields.php';
unset($evMvStyle);
?>
</div><?php /* /mv-grid */ ?>
</div><?php /* /cal-modal-body */ ?>
<div class="cal-modal-foot">
<span class="cal-modal-tip" id="calAddTip"></span>
<?php /* 🔴 v37 用户第 5 条:「"加上"和"取消"两个按钮在右上角,离得特别远,
根本看不到。应该把它们放在下方…」
真根因(真机量出来的,不是猜):
原来这里**多写了一个 `</div>`** —— `.cal-modal-panel` 被提前闭合、
跟着又被多关一层,于是 `<div class="cal-modal-foot">` 跑到了
**面板外面**(DOM 上它是 `.cal-modal` 的直接子元素)。
面板 `max-height` 一卡,底栏就被挤到面板下方 → 用户看不到。
另有 CSS 一层:`.cal-modal-panel > form` 原先不是 flex 列,
`.cal-modal-body{flex:1 1 auto}` 因此不起作用(两处都已修)。
⚠️ 提交键用 `form="calAddForm"` **显式关联**:多一层/少一层 DOM 的
意外都不会再让它失效(它本来就在 form 内部,但显式写更稳)。 */ ?>
<button class="btn btn-sm" type="submit" form="calAddForm" id="calAddSubmit">加上</button>
<button class="btn btn-ghost btn-sm" type="button" id="calAddCancel2"<?= $fbAddModalAttr ? ' data-fb-add-close' : '' ?>>取消</button>
</div>
</form>
</div>
</div>
<?php
/* 片段结束。调用方 `require` 完之后可以 unset 这些(模块页就是那么做的):
$firstCal / $hasPersonal / $defDay / $defHM / $defEndH / $evFields /
$evAllDay / $evMvStyle / $calBooks / $calYm / $calView / $calAnchor / $calNewAt
⚠️ 不 unset 也不报错,只是会把名字留在宿主的符号表里。
好在它们现在**全部带 `cal` 前缀**,与宿主模板(首页 / 日程页)的高频名
(`$view` / `$writable` / `$anchor` …)不会撞 —— 这正是加前缀的目的。
⚠️ `$evFields / $evAllDay / $evMvStyle` 是 `_event_fields.php` 的入参,
那几个名字由那个片段自己约定(它的 ALLOW 白名单里),不在本条讨论范围。 */