Files
FamilyBook/templates/calendar/event.php
T
laoyang f1b09a2f41 v93: 首页日历本下拉能拉了 / 日程浮层只留编辑删除 / 重复日程三档 / Markdown 标题四级阶梯补齐
用户两条:
① 「新建日程,日历本是拉不下来的,是空的,好像只有一个"工作"什么的」
② 「点日程后的详情跟原来的弹框几乎一模一样,这个详情就没有意义了」
   ⇒ 只显示「编辑」「删除」;重复日程的删除/编辑要有三档
   (当前 / 从现在到以后 / 全部)

【① 根因】`_add_modal.php` 被日程页与首页共用,HTML 一样,但自绘下拉的整套 JS
只内联在 `templates/calendar/index.php` 底部 ⇒ 首页那份没有交互,点了菜单不展开;
而按钮上是服务端渲染的默认本("工作"),所以看着"只有一个工作"。
修法:抽 `assets/js/cs_picker.js`(全站 defer、按 `[data-cs-picker]` 初始化、
`data-cs-ready` 幂等)。不扫所有 `.cs` —— 观影表单的影院下拉有自己的内联绑定,
两边都绑会"点一次开两次"。

【②】浮层自定义日程不再显示「详情」(派生条目仍只有真链接「详情」);
删除/编辑三档措辞按用户原话;`occ` 为空时只给「删除全部」,绝不让空串触发整条删除。
编辑三档的入口在 `fb_entry_json()` 把 `occ` 拼进 edit 地址(`?d=`)——
不带就只有"修改全部"一档。

【顺带修掉 v85 没做完的一半】全量套件在 v61 A9b 报红,查下去不是本轮改坏的:
CSS 只把 `##`(h4)调到 17px,`###`(h5)留在 15.5px(正文 15px,只差 0.5px)、
`####`(h6)留在 14.5px(比正文还小),而那段注释自己写的就是"三级都要调大"。
阶梯重排 h3/h4/h5/h6 = 22/20/18/17,四级两两不同号。
更值得记的是四条判据全部只盯 h4(v61 A9b、v85 B2/B2b 前身、静态断言、线上 A4),
而 h4 恰好是唯一修好的那一级 ⇒ 各加"每一级/最矮的一级"判据,并用
「只把 h6 打回 14.5px ⇒ A9b 仍绿、只有 A9b2 变红」证明新判据有独立价值。

【工具链】`_b61_falsify.py` 的 main_all 改成每个 case 起独立子进程 ——
原先同一进程连跑 10 遍探针,Playwright 的临时 profile 建/删会在同一轮里累计,
撞沙箱批量删除钩子被 SIGTERM(症状:输出戛然而止、退出码 1、一条 FAIL 都没有)。

验证:v93 正向 28/0 · v93 反向 3 处注入各自被抓 · v61 34/0 · v61 反向 9/9 ·
v85 md_heading 12/0 · v85 静态反向 8/0 · 全量流水线通过并已部署。
2026-10-25 14:52:43 +08:00

228 lines
13 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
/**
* 一条自定义日程:查看 / 改 / 删。
*
* ⚠️ 从月视图点**观影 / 旅程 / 任务**那些条会直接跳到对应模块(它们的"编辑页"在那里),
* 这里只处理 calendar_events 里真正属于某本日历的自定义日程。
*
* 🔴 v43(用户 2026-10-11 第 6 条):本模板多了一种用法 —— **`?embed=1` 只吐编辑表单**。
* 条目详情浮层里点「编辑」时用它(浮层把正文整块换成这张表单)。
* ⇒ `$evEmbed = true` 时:不输出页头、不输出只读 dl、不输出删除表单、
* 只保留那张编辑表单(外加已有附件)。
* ⚠️ 与 `days/edit` / `trips/node_edit` / `movies/edit` 的 `?embed=1` 同一套约定。
*
* 🔴 v49(用户 2026-10-14 第 2 条):再加一种 —— **`?embed=detail` 只吐只读详情块**。
* 浮层里点「详情」时用它(面板变宽 + 正文换成完整信息,**不跳页**)。
* ⇒ `$evDetail = true` 时:不输出页头、不输出编辑表单/删除表单,
* 只保留那段 `<dl class="kv">`(含附件)。**与可写与否无关** ——
* 只读日历的日程也要能看详情(它是一个"查看"动作,不是"编辑")。
*/
$canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
$evEmbed = !empty($evEmbed);
$evDetail = !empty($evDetail);
$ym = substr((string)$ev['starts_at'], 0, 7);
?>
<?php if (!$evEmbed): ?>
<div class="page-head">
<h1><?= e((string)$ev['title']) ?></h1>
<span class="sub"><?= e((string)($ev['starts_at'] ?? '')) ?></span>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $ym])) ?>">返回日程</a>
</div>
</div>
<?php endif; ?>
<div class="<?= $evEmbed ? '' : 'card card-pad' ?><?= $evDetail ? ' ev-detail-block' : '' ?>">
<?php /* 只读详情块:两种情形都要它 ——
(a) `?embed=detail`(浮层点「详情」);
(b) 整页且我**不可写**(原行为:只读日历能看到信息但不能改)。 */ ?>
<?php if ($evDetail || !$canWrite): ?>
<dl class="kv">
<div class="kv-row"><dt>开始</dt>
<dd><?= e(empty($ev['all_day']) ? (string)$ev['starts_at'] : substr((string)$ev['starts_at'], 0, 10)) ?></dd>
</div>
<?php if (!empty($ev['ends_at'])): ?>
<div class="kv-row"><dt>结束</dt>
<dd><?= e(empty($ev['all_day']) ? (string)$ev['ends_at'] : substr((string)$ev['ends_at'], 0, 10)) ?></dd>
</div>
<?php endif; ?>
<?php if (!empty($ev['all_day'])): ?>
<div class="kv-row"><dt></dt><dd>全天</dd></div>
<?php endif; ?>
<?php if (!empty($ev['location'])): ?>
<div class="kv-row"><dt>地点</dt><dd><?= e((string)$ev['location']) ?></dd></div>
<?php endif; ?>
<?php /* v24 第 20 批 第 8 条:提醒可以**多档**,所以要列全("提前 5 分钟、提前 1 小时")。
取的是同一个逗号串,Calendar::reminderWords() 负责翻成人话。 */ ?>
<?php $rvWords = \App\Calendar::reminderWords($ev['reminder_minutes'] ?? null); ?>
<?php if ($rvWords !== ''): ?>
<div class="kv-row"><dt>提醒</dt><dd><?= e($rvWords) ?></dd></div>
<?php endif; ?>
<?php if (!empty($ev['note'])): ?>
<div class="kv-row"><dt>备注</dt><dd><?= nl2br(e((string)$ev['note'])) ?></dd></div>
<?php endif; ?>
<?php $roAs = \App\Attachment::list(\App\Calendar::ATTACH, (int)$ev['id']); ?>
<?php if ($roAs): ?>
<div class="kv-row"><dt>附件</dt>
<dd class="attach-row">
<?php foreach ($roAs as $a): ?>
<span class="attach-item">
<a href="<?= e(media_url((string)$a['key'])) ?>" target="_blank" rel="noopener">
<?= (int)$a['is_image'] === 1 ? '🖼' : '📄' ?> <?= e((string)($a['name'] ?: '附件')) ?>
</a>
<a class="link-quiet" href="<?= e(media_url((string)$a['key'], ['dl' => 1])) ?>">下载</a>
</span>
<?php endforeach; ?>
</dd>
</div>
<?php endif; ?>
</dl>
<?php /* ⚠️ 那句"只读权限"只在**整页**且不可写时给 ——
`?embed=detail` 是要"看更多信息",不是来改的,
底下挂一句"你不能改"会让浮层看着像出了问题。 */ ?>
<?php if (!$evDetail && !$canWrite): ?>
<p class="empty-hint">这本日历你只有只读权限,看得到但不能改。</p>
<?php endif; ?>
<?php elseif ($evDetail): ?>
<?php /* 理论到不了这里(`$evDetail` 时上面那条分支已经命中);
留着是为了将来有人把条件改散时有个明确的落点,而不是静默输出空。 */ ?>
<p class="empty-hint">没有更多信息。</p>
<?php else: ?>
<form method="post" action="<?= e(url('calendar/event_update')) ?>" enctype="multipart/form-data"
id="calEditForm">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$ev['id'] ?>">
<input type="hidden" name="ym" value="<?= e($ym) ?>">
<?php /* 这一次是哪一天:从日历点进来时 URL 带 ?d=YYYY-MM-DD。
"仅此一次 / 这一次及以后"都要靠它定位。 */ ?>
<input type="hidden" name="occ" value="<?= e($occ ?? '') ?>">
<?php /* 用户 2026-10-03 第 5 条:改一条**重复**日程时,先选"改哪些"。
只在它真的重复时才出现 —— 不重复的日程问这个是多余的。
🔴 v93(用户 2026-10-25):「编辑重复日历时,也会有这三个选项:
修改当前 / 修改从现在到以后 / 修改全部。」
⇒ 三档措辞与顺序都按用户原话(当前 → 从现在到以后 → 全部),
与删除那张表单**逐字同一套**,用户学一次就够。
⚠️ `$occ` 必须由 URL 的 `?d=` 带进来 —— 浮层那条路是靠
`fb_entry_json()` 把它拼进 `edit` 地址的(见 app/helpers.php)。
少了它这里只会渲染出"修改全部"一档,用户会以为重复日程
不能只改这一次(v93 之前的真实现状)。 */ ?>
<?php if (trim((string)($ev['rrule'] ?? '')) !== ''): ?>
<div class="card card-pad ev-scope">
<b>这是一条重复日程</b>
<p class="small muted" style="margin:6px 0 10px">
<?= e(\App\Repeat::describe((string)$ev['rrule'])) ?>
<?php if (!empty($occ)): ?>
<br>你打开的是 <b><?= e($occ) ?></b> 那一次。
<?php endif; ?>
</p>
<?php if (!empty($occ)): ?>
<label class="chk-inline"><input type="radio" name="scope" value="once">
<span>修改<b>当前</b>这一次(<?= e($occ) ?>)</span></label>
<label class="chk-inline"><input type="radio" name="scope" value="future">
<span>修改<b>从现在到以后</b>(<?= e($occ) ?> 起,过去的保持不变)</span></label>
<?php endif; ?>
<label class="chk-inline"><input type="radio" name="scope" value="all" checked>
<span>修改<b>全部</b>(整条系列)</span></label>
<?php if (empty($occ)): ?>
<p class="small muted" style="margin:6px 0 0">
(从日历点某一天的日程进来,才能选「修改当前 / 修改从现在到以后」。)
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="form-grid">
<?php /* 🔴 v43(用户第 5 条要求"观影式排版"):浮层里编辑时也用 `.mv-grid`,
与"新建日程"那个弹框一致(同一个字段块传 $evMvStyle 即可)。
⚠️ 整页形态(老链接直达)保持原来的 `.form-grid` 两列 ——
那是"页面表单"的排版习惯,不该跟着浮层一起变。 */
$evFields = $ev; $evAllDay = false;
if ($evEmbed) { $evMvStyle = true; echo '<div class="mv-grid">'; }
require __DIR__ . '/_event_fields.php';
if ($evEmbed) { echo '</div>'; unset($evMvStyle); }
?>
</div>
<?php if (!$evEmbed): ?>
<div class="btn-row">
<button class="btn" type="submit">保存</button>
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $ym])) ?>">取消</a>
</div>
<?php endif; ?>
</form>
<?php $as = \App\Attachment::list(\App\Calendar::ATTACH, (int)$ev['id']); ?>
<?php if ($as): ?>
<div class="ev-attach-list">
<span class="res-label">已有附件</span>
<?php
$attRows = $as;
$attCanEdit = true;
$attDelUrl = 'calendar/attach_del';
$attDelField = 'attach_id';
$attExtra = ['id' => (int)$ev['id']];
require __DIR__ . '/../_attachments.php';
?>
</div>
<?php endif; ?>
<?php /* ⚠️ 删除表单必须放在上面那个表单**外面**:<form> 套 <form> 是非法 HTML,
浏览器会把外层表单在切开处把外层提前关掉,"保存"按钮就失效了。 */ ?>
<?php
/* ==================== 删除(v24 第 20 批 用户第 7 条)====================
用户原话:「对于重复的日程,当点击删除的时候,应该提示是删除这一次,
还是全部都删除,还是删除这一次及以后的。」
⚠️ 用**原生 radio + 提交**而不是 JS 弹框:不用 JS 也能选范围,
而且"删哪一档"这件事必须让人看清文字再点(三种后果差别很大,手滑代价高)。
⚠️ 只有**重复日程**才给这三档。不重复的给三档会让人以为"是不是还有别的次数"
(其实就一条),反而制造困惑 —— 那种情况只给一个「删除」。
⚠️ `occ`(点开的是哪一天)必须带上:「只删这一次」「这一次及以后」都靠它定位。
从日历某一天点进来才有它(URL 的 ?d=);从列表直接打开就只给"全部删除"。
⚠️ 三档的措辞与顺序与**编辑**那三档、浮层里的删除范围态完全一致
(当前 → 从现在到以后 → 全部,v93 用户原话),用户学一次就够,
不用记第二套说法。改文案时**三处一起改**:
本文件(编辑 + 删除)与 `assets/js/entry_popup.js`(浮层删除范围)。
*/
$evIsRepeat = trim((string)($ev['rrule'] ?? '')) !== '';
?>
<?php if ($evEmbed): ?>
<?php /* 浮层形态:删除留给详情页(那里有重复日程的三档删除),这里不出。 */ ?>
<?php elseif ($evIsRepeat && !empty($occ)): ?>
<form method="post" action="<?= e(url('calendar/event_del')) ?>" class="ev-del ev-del-scope"
onsubmit="return confirm('按上面选的范围删除?')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$ev['id'] ?>">
<input type="hidden" name="ym" value="<?= e($ym) ?>">
<input type="hidden" name="occ" value="<?= e($occ) ?>">
<p class="ev-del-head">这是一条<b>重复日程</b>,删哪些?</p>
<div class="ev-del-picks">
<label class="chk-inline">
<input type="radio" name="scope" value="once">
<span>删除<b>当前</b>这一次(<?= e($occ) ?>)</span>
</label>
<label class="chk-inline">
<input type="radio" name="scope" value="future">
<span>删除<b>从现在到以后</b>(<?= e($occ) ?> 起,之前的保留)</span>
</label>
<label class="chk-inline">
<input type="radio" name="scope" value="all" checked>
<span>删除<b>全部</b>(整条系列)</span>
</label>
</div>
<button class="btn btn-danger btn-sm" type="submit">删除</button>
</form>
<?php else: ?>
<form method="post" action="<?= e(url('calendar/event_del')) ?>" class="ev-del"
onsubmit="return confirm('删除这条日程?')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$ev['id'] ?>">
<input type="hidden" name="ym" value="<?= e($ym) ?>">
<?php if (!empty($occ)): ?><input type="hidden" name="occ" value="<?= e($occ) ?>"><?php endif; ?>
<input type="hidden" name="scope" value="all">
<button class="btn btn-danger btn-sm" type="submit">删除这条日程</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div>
<?php unset($evEmbed, $evDetail, $evIsRepeat, $evIsAllDay, $evAllDay, $evFields, $evMvStyle, $ym, $occ); ?>