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、流水线全绿并已部署。
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
<?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 白名单里),不在本条讨论范围。 */
|
||||
Reference in New Issue
Block a user