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:
2026-10-24 04:25:02 +08:00
parent fd90e33d94
commit 4b58fec796
7 changed files with 502 additions and 185 deletions
+19
View File
@@ -73,6 +73,15 @@ final class HomeCards
'name' => '今天的日程', 'desc' => '按日视图那样排的今天', 'name' => '今天的日程', 'desc' => '按日视图那样排的今天',
'w' => 4, 'h' => 8, 'route' => 'calendar', 'more' => '更多日程', 'n' => 0, 'w' => 4, 'h' => 8, 'route' => 'calendar', 'more' => '更多日程', 'n' => 0,
'mod' => 'calendar', 'mod' => 'calendar',
/* 🔴 v82(用户 2026-10-23):「日程,没有新建日程按钮……」
⚠️ 实测确认:他说的是**这张卡片**上没按钮 —— 日程模块页右上角
那颗 `#calAddBtn` 一直在、也可见(`l=1042, w=90`,点得到)。
⇒ 这里补上 `add`,卡片右上角就多一颗「新建日程」。
⚠️ `add_modal` 的 id 必须与 `templates/calendar/_add_modal.php` 里的
容器 id **逐字相同**(`calAddCard`)——写错的后果是"点了没反应",
是本项目最难查的一类 bug(v81 真事故:`anAddCard` vs `anNewCard`)。 */
'add' => '新建日程',
'add_modal' => 'calAddCard',
], ],
'tasks' => [ 'tasks' => [
'name' => '近期任务', 'desc' => '进行中与待办(含看板与状态)', 'name' => '近期任务', 'desc' => '进行中与待办(含看板与状态)',
@@ -136,6 +145,16 @@ final class HomeCards
'name' => '联系人', 'desc' => '名册里的人,可以选显示几个', 'name' => '联系人', 'desc' => '名册里的人,可以选显示几个',
'w' => 4, 'h' => 4, 'route' => 'people', 'more' => '名册', 'n' => 8, 'w' => 4, 'h' => 4, 'route' => 'people', 'more' => '名册', 'n' => 8,
'mod' => 'people', 'mod' => 'people',
/* 🔴 v82(用户 2026-10-23):「名册,也没有新增成员这个按钮。」
⚠️ 实测确认:名册模块页那颗「添加成员」**一直在**
(`<a class="btn btn-sm" href="people/new">`,可见可点)。
用户说的是**这张联系人卡片**上没有按钮。
⇒ 补 `add`,卡片右上角多一颗「新增成员」,点开的是**与名册页同一张**
表单(`templates/people/form.php`,加了弹框形态)。
⚠️ `add_modal` 必须与 `people/form.php` 里那个容器 id 逐字相同
(`pfAddCard`)。写错 = 点了没反应(v81 真事故:anAddCard vs anNewCard)。 */
'add' => '新增成员',
'add_modal' => 'pfAddCard',
], ],
]; ];
+16 -9
View File
@@ -4258,12 +4258,13 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b
z-index: 4; /* 在日程块(z-index 2)之上,要看得见 */ z-index: 4; /* 在日程块(z-index 2)之上,要看得见 */
pointer-events: none; pointer-events: none;
} }
/* 左端那个小圆点:让"这条线是活的、代表此刻"更明显(也与日历 App 的通行做法一致) */ /* 🔴 v82(用户 2026-10-23):「在日程模块中,那个红线左侧是不是还有一个红点?
.tg-now::before { 如果有,去掉。」—— 确实有,就是这条 `::before`。
content: ""; position: absolute; left: -4px; top: -5px; ⚠️ 首页日程那条红线 v44 已经删过一次红点(`.ag-now-dot`),用户当时说
width: 8px; height: 8px; border-radius: 50%; background: #E0453B; 「红点显示也不全,也不太好看。直接把红点去掉就行了,直接一根红线就可以了。」
box-shadow: 0 0 0 2px rgba(255, 255, 255, .9); ⇒ 两处口径统一:**只有一根线,没有任何端点装饰**。
} ⚠️ 这条规则整条删掉,别只把 `content` 清空 —— 留个空规则在这儿,
将来一定有人照着它把圆点"补回来"。 */
/* 过午夜(或跨天翻页)时线要能"走出去",不要卡在格子边界上闪 */ /* 过午夜(或跨天翻页)时线要能"走出去",不要卡在格子边界上闪 */
.tg-col { overflow: hidden; } .tg-col { overflow: hidden; }
@@ -4772,9 +4773,15 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b
🔴 v44(用户第 3 条):左端那颗小圆点**删掉**了 —— 🔴 v44(用户第 3 条):左端那颗小圆点**删掉**了 ——
用户:「这个红点显示也不全,也不太好看。直接把红点去掉就行了,直接一根红线就可以了。」 用户:「这个红点显示也不全,也不太好看。直接把红点去掉就行了,直接一根红线就可以了。」
⇒ 直接一根 2px 红线;`.ag-now-dot` 这条规则随之删除(不再有那个元素)。 ⇒ 直接一根 2px 红线;`.ag-now-dot` 这条规则随之删除(不再有那个元素)。
🔴 v80:左端改吃 `--ag-gutter`(与车道同一起点),右边留 2px 与色块对齐 —— 🔴🔴 v82(用户 2026-10-23):「首页上的日程,红色线不对,变短了左侧缺了一块。」
原先是 `left: 46px; right: 0`,比车道多出 4px,红线尾巴会探出色块右沿。 */ 真根因 = **双重缩进**:v80 把色块挪进 `.ag-lanes`(这一层自己已经
.ag-now { position: absolute; left: var(--ag-gutter, 50px); right: 2px; height: 0; border-top: 2px solid var(--red); z-index: 2; } `left: var(--ag-gutter)` 让出了刻度),红线在它**内部**却又写了一次
`left: var(--ag-gutter, 50px)` ⇒ 实际左沿 = 刻度宽 + 刻度宽 = 100px,
比车道左沿多出整整一份刻度宽(实测差 50.00px,正是 `--ag-gutter` 的值)。
⇒ 红线在车道内部,`left` 就必须是 `0`(相对车道),不能再吃 gutter。
⚠️ 别再"顺手写回去":`.ag-ev` 的百分比也在这层里算,只要给它们加过 gutter,
就是同一个 bug 再长一遍。外来元素要判断"我在谁里面"再决定 left。 */
.ag-now { position: absolute; left: 0; right: 0; height: 0; border-top: 2px solid var(--red); z-index: 2; }
/* 日程块 */ /* 日程块 */
/* 🔴 v30 第 2 条:「首页上的今天的日程,用色块表示,和日程保持一致。」 /* 🔴 v30 第 2 条:「首页上的今天的日程,用色块表示,和日程保持一致。」
⇒ 从原来的"白底卡片 + 左边 3px 色线"改成**与日历页 `.tg-block` 同款的实心色块**: ⇒ 从原来的"白底卡片 + 左边 3px 色线"改成**与日历页 `.tg-block` 同款的实心色块**:
+36
View File
@@ -965,6 +965,32 @@ switch ($r) {
'kindsFull' => Anniversary::kindsFull($familyId), 'kindsFull' => Anniversary::kindsFull($familyId),
'people' => Person::roster($familyId, $me, 'member', ''), 'people' => Person::roster($familyId, $me, 'member', ''),
]; ];
} elseif ($hcKey === 'agenda' && !isset($homeAddModals['agenda'])) {
/* 🔴 v82(用户 2026-10-23):「日程,没有新建日程按钮」——
首页「今天的日程」卡片要能就地新建日程。
⚠️ 这里给的 `writable` 必须与 `calendar/index.php` 算的**同一口径**:
`Calendar::mine()` 取我可见的本 → 再按 `Calendar::canWrite()` 过一道。
少走 canWrite 的话,只读共享本会出现在下拉里,选了就被服务端拒。
⚠️ 顺手 `ensurePersonalBook()`:与日程页同一条幂等补本 ——
老用户库里的"我自己的日程本"是 `seedDefaults()` 之后才有的,
不补的话首页这张弹框的默认本可能落到**观影本**上,
用户直接点"加上"就建出一条观影记录(v24 第 21 批真事故)。 */
Calendar::ensurePersonalBook($familyId, $me);
$agCals = Calendar::mine($familyId, $me);
$homeAddModals['agenda'] = [
'writable' => array_values(array_filter(
$agCals,
static fn($c) => Calendar::canWrite($c, $me)
)),
];
} elseif ($hcKey === 'contacts' && !isset($homeAddModals['contacts'])) {
/* 🔴 v82(用户 2026-10-23):「名册,也没有新增成员这个按钮」——
首页「联系人」卡片要能就地新增成员。
⚠️ 只给标签候选(表单要用),**不给名册**:新增态不需要选人,
白跑一次 roster 查询(1000+ 人)没有意义。 */
$homeAddModals['contacts'] = [
'tagChoices' => PersonTag::choices($familyId, $me),
];
} }
} }
} }
@@ -5727,6 +5753,16 @@ $want = array_keys($want);
flash('err', '有附件没有保存:' . implode(';', $warns)); flash('err', '有附件没有保存:' . implode(';', $warns));
} }
flash('ok', '日程已加上。'); flash('ok', '日程已加上。');
/* 🔴 v82(用户 2026-10-23):「首页上的日程,……首页卡片上要有新建按钮」——
从**首页**那张弹框提交的,回首页(卡片上立刻能看到刚建的那条)。
⚠️ 默认仍回日程模块 —— 从日程页新建时,用户接着要看的是日历本身上
刚加的那一格,跳去首页反而看不到。
⚠️ 白名单取值(只认 'home'),绝不把 POST 字符串直接当跳转地址
(开放重定向)—— 与 v81 的 `days/create`、`movies/create` 同一条约定。
⚠️ 放在 AJAX 分支**之后**:AJAX 那条路自己早 exit 了,走不到这里。 */
if ((string)($_POST['back'] ?? '') === 'home') {
redirect('home');
}
redirect('calendar', ['ym' => substr($startsAt, 0, 7)]); redirect('calendar', ['ym' => substr($startsAt, 0, 7)]);
break; break;
+284
View File
@@ -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 白名单里),不在本条讨论范围。 */
+32 -174
View File
@@ -148,182 +148,40 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
只把它的容器从"页面上的一格"换成"浮层里的一块"。 只把它的容器从"页面上的一格"换成"浮层里的一块"。
`calendar_id` 那个 select 也保留(第 7 条要求"选择日历本,带上颜色", `calendar_id` 那个 select 也保留(第 7 条要求"选择日历本,带上颜色",
见下面给每个 option 加的色块)。 */ ?> 见下面给每个 option 加的色块)。 */ ?>
<?php if ($canWrite && $writable): ?> <?php /* 🔴 v82(用户 2026-10-23):这一大段弹框**抽成公共片段**了 ——
<div class="cal-modal" id="calAddCard" role="dialog" aria-modal="true" 首页日程卡片要"在首页就能新建日程",两处必须用同一张表单。
aria-label="新建日程" hidden> 片段:`templates/calendar/_add_modal.php`(表单契约一字未改)。
<div class="cal-modal-panel"> ⚠️ 抽片段后**必须**同步改部署脚本的静态断言:那批断言原来按
<div class="cal-modal-head"> `calendar/index.php` 找 `id="calPicker"` / `id="cbDot"` /
<b>新建日程</b> `class="cs-dot" style="background:` / `<span class="mv-lb">日历本</span>`
<button type="button" class="cal-modal-x" id="calAddCancel" / `$evMvStyle = true` —— 现在这些串在新片段里。
aria-label="关闭">✕</button> 不改的话:正向断言当场中止部署,负向断言恒真没牙齿(v78 原话)。 */
</div> ?>
<form method="post" action="<?= e(url('calendar/event_add')) ?>" <?php
enctype="multipart/form-data" id="calAddForm"> $calCanNew = $canWrite && (bool)$writable;
<?= csrf_field() ?> $calWritable = $writable;
<input type="hidden" name="ym" value="<?= e($ym) ?>"> $calCtx = [
<input type="hidden" name="back_view" value="<?= e($view) ?>"> 'ym' => $ym, 'view' => $view, 'anchor' => $anchor,
<input type="hidden" name="back_anchor" value="<?= e($anchor) ?>"> 'new_at' => $newAt,
<div class="cal-modal-body"> // 模块页只管自己:'' = 提交完回日程模块(不写 'home')
<?php /* ⚠️ v44:`.mv-grid` 在这里就打开 —— 下面的「日历本」行 'back' => '',
与「日期范围」字段块都要走观影式排版(标签右对齐 + 冒号)。 ];
原来 `.mv-grid` 是包在字段块那一小段里的,日课本在它**外面**, require __DIR__ . '/_add_modal.php';
于是"日历本:下拉"落不到这套排版上(正是用户报的"不在一行")。 */ ?> /* 🔴🔴 v82:**只清掉三个"调用参数"**,别的什么都不许 unset。
<div class="mv-grid"> 为什么:`require` 与宿主**同一作用域**,片段里赋的每个变量都会留下来。
<?php /* 🔴🔴 v44(用户 2026-10-12 第 1 条):「新建日程,放在哪本,改成日历本后面加冒号。 我第一版顺手把 `$ym/$view/$anchor/$newAt/$writable` 也 unset 了 ——
现在这句话和日历本下拉框还有前面的颜色,仍然不在一行, 而这五个正是**模块页自己后文还要用的**(页头标题、翻页链接、周/日网格、
我建议把颜色放在下拉框里面,就跟电影的影院选择一样, `.tg-now` 的 top 计算全依赖它们)。unset 掉不会报错,只会让后半页
让日历本这三个字和下拉框在一条线上。」 静默变成空值:页头显示"1970 年"、翻页链接丢参数、时间网格画不出来。
这类"清了别人的变量"比"片段拿不到变量"更难查 —— 页面能开,只是全错。
三件事: ⇒ 判据:unset 之前先 grep 一下后文还用不用它(本项目吃过"片段覆盖宿主变量"的亏)。
① 标签改「日历本」(用户点名要这三个字),冒号由观影式排版的
`.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 条:**默认必须选"我自己的普通日历本"**。 🔴 v82 再修(同日):片段内部现在已经**改用 `$cal*` 前缀**,不再覆盖
原来直接取 `$writable[0]` —— 而顺序是按 id,观影 / 旅行两本 宿主的 `$anchor/$newAt/$writable/$view/$ym`(见该片段头部那段说明)。
通常排在最前面。于是用户打开"新建日程"、直接打字、点"加上", 所以这里连"片段残留物"都不必再清 —— 真正要清的只有本模板自己传进去的
建出来的其实是一条**观影记录**(或旅程)。 三个 `$cal*` 入参。 */
这不是"显示不对",是**建错东西**,而且事后很难发现 unset($calCanNew, $calWritable, $calCtx);
—— 用户以为日历里多了条日程,其实观影列表里多了一条记录。
所以:优先把自己建的 kind='custom' 本子排在前面当默认;
没有普通本时才退到第一个可写的本子。 */
$firstCal = null;
foreach ($writable as $c) {
if ((string)($c['kind'] ?? '') === 'custom') { $firstCal = $c; break; }
}
if ($firstCal === null) { $firstCal = $writable ? $writable[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 ($writable 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 ($writable as $c): $ro = \App\Calendar::isReadonlyKind((string)$c['kind']);
$isDef = $firstCal && (int)$c['id'] === (int)$firstCal['id']; ?>
<option value="<?= (int)$c['id'] ?>"
data-color="<?= e((string)$c['color']) ?>"
<?= $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 = $newAt !== '' ? substr($newAt, 0, 10)
: (($ym === $thisYm ? date('Y-m-d') : $ym . '-01'));
$defHM = $newAt !== '' && strlen($newAt) > 10
? substr($newAt, 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">取消</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<?php if ($view === 'week' || $view === 'day'): ?> <?php if ($view === 'week' || $view === 'day'): ?>
<?php /* ===== 周 / 天:按小时铺的时间网格(全天日程单独挂顶上) ===== */ ?> <?php /* ===== 周 / 天:按小时铺的时间网格(全天日程单独挂顶上) ===== */ ?>
+60
View File
@@ -286,6 +286,66 @@
require __DIR__ . '/days/form.php'; require __DIR__ . '/days/form.php';
unset($anModalOnly, $anOpen, $anModal, $anEmbed, $anShell, unset($anModalOnly, $anOpen, $anModal, $anEmbed, $anShell,
$editing, $people, $kindsFull, $kinds); $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); unset($fbAddModalAttr);
?> ?>
+53
View File
@@ -49,8 +49,47 @@ $ctHints = [
'address' => '住址在「住址」一栏;这里放单位地址、老家地址等', 'address' => '住址在「住址」一栏;这里放单位地址、老家地址等',
'url' => '主页、博客、店铺链接……', 'url' => '主页、博客、店铺链接……',
]; ];
/* ============================================================================
🔴 v82(用户 2026-10-23):「名册,也没有新增成员这个按钮。」
⚠️ 实测确认:名册模块页右上角那颗「添加成员」**一直在**(`<a class="btn btn-sm"
href="people/new">`,可见可点)。用户说的是**首页「联系人」卡片**上没有按钮。
⇒ 这张表单要能像 `movies/form.php` 那样被**别的页面内嵌成弹框**。
---------------------------------------------------------------------------
两个开关(与观影表单同一套命名,别自创一套):
· `$pfModalOnly` 不输出 page-head(本模板被别的页面内嵌时用);
· `$pfModal` 输出弹框壳(`.cal-modal` + 遮罩)。默认:新增态就是弹框。
首页内嵌时由 `templates/home.php` 传 `$fbAddModalAttr = true`,
本模板据此挂 `data-fb-add-modal` / `data-fb-add-close`(通用开关认这两个属性)。
⚠️ **表单契约一个字没改**:字段名 / enctype / action(people/create)全照旧。
============================================================================ */
$pfModalOnly = !empty($pfModalOnly);
$pfModal = isset($pfModal) ? (bool)$pfModal : !$isEdit;
$pfOpen = isset($pfOpen) ? (bool)$pfOpen : ($pfModal && !$pfModalOnly);
$fbAdd = !empty($fbAddModalAttr);
?> ?>
<?php /* ---- 弹框壳(只在 $pfModal 时开)-------------------------------------
⚠️ 首屏默认 `hidden`:首页那张弹框点按钮才开(`data-fb-add-open="#pfAddCard"`)。
不要写成"默认打开"——首页会一进来就糊一层遮罩。
⚠️ `role="dialog" aria-modal="true"` 与站内其它浮层一致(观影/日程/任务)。 */ ?>
<?php if ($pfModal): ?>
<div class="cal-modal" id="pfAddCard" role="dialog" aria-modal="true"
aria-label="<?= $isEdit ? '编辑成员' : '添加成员' ?>"
<?= $pfOpen ? '' : 'hidden' ?><?= $fbAdd ? ' data-fb-add-modal' : '' ?>>
<div class="cal-modal-panel pf-panel">
<div class="cal-modal-head">
<b><?= $isEdit ? ($isSelf ? '我的资料' : '编辑成员') : '添加成员' ?></b>
<button type="button" class="cal-modal-x" id="pfAddX"
aria-label="关闭"<?= $fbAdd ? ' data-fb-add-close' : '' ?>>✕</button>
</div>
<?php endif; ?>
<?php /* ⚠️ v82:弹框形态下这个 page-head 必须消失(它就是"整页"的标志物)——
否则表单会连着页头一起塞进浮层,看起来像"整页被塞进一个框里"。
判据用 `$pfModalOnly`(内嵌方给的),不要用 `$pfModal`
—— 编辑态整页时 `$pfModal` 可能是 true 而它**不是**内嵌。 */ ?>
<?php if (!$pfModalOnly): ?>
<div class="page-head"> <div class="page-head">
<h1><?= $isEdit ? ($isSelf ? '我的资料' : '编辑成员') : '添加成员' ?></h1> <h1><?= $isEdit ? ($isSelf ? '我的资料' : '编辑成员') : '添加成员' ?></h1>
<span class="sub"><?= $isEdit ? e($person['name']) : '填姓名就行,其他都可以以后再补' ?></span> <span class="sub"><?= $isEdit ? e($person['name']) : '填姓名就行,其他都可以以后再补' ?></span>
@@ -61,6 +100,7 @@ $ctHints = [
<a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回名册</a> <a class="btn btn-ghost btn-sm" href="<?= e(url('people')) ?>">返回名册</a>
</div> </div>
</div> </div>
<?php endif; ?>
<form method="post" enctype="multipart/form-data" <form method="post" enctype="multipart/form-data"
action="<?= e(url($isEdit ? 'people/update' : 'people/create')) ?>"> action="<?= e(url($isEdit ? 'people/update' : 'people/create')) ?>">
@@ -579,13 +619,26 @@ $ctHints = [
? ($isSelf ? url('settings/me') : url('people/show', ['id' => (int)$person['id']])) ? ($isSelf ? url('settings/me') : url('people/show', ['id' => (int)$person['id']]))
: url('people'); : url('people');
?> ?>
<?php if ($pfModal): ?>
<?php /* ⚠️ 弹框形态下「取消」必须**只关弹框**(`data-fb-add-close`),
不能跳去名册 —— 跳走就把用户刚填的一整张表扔了,
而且首页那张弹框本来就是"填一点就走"的用法。 */ ?>
<button class="btn btn-ghost" type="button"
<?= $fbAdd ? 'data-fb-add-close' : '' ?>
onclick="document.getElementById('pfAddCard').hidden = true;">取消</button>
<?php else: ?>
<a class="btn btn-ghost" href="<?= e($cancelUrl) ?>">取消</a> <a class="btn btn-ghost" href="<?= e($cancelUrl) ?>">取消</a>
<?php endif; ?>
<?php if ($isSelf): ?> <?php if ($isSelf): ?>
<a class="btn btn-ghost" href="<?= e(url('people/show', ['id' => (int)$person['id']])) ?>">看我的详情页</a> <a class="btn btn-ghost" href="<?= e(url('people/show', ['id' => (int)$person['id']])) ?>">看我的详情页</a>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
</form> </form>
<?php if ($pfModal): ?>
</div><?php /* /.cal-modal-panel */ ?>
</div><?php /* /.cal-modal */ ?>
<?php endif; ?>
<script> <script>
(function () { (function () {