Files
FamilyBook/templates/calendar/event.php
T
laoyang 1bf56b114b v49:条目浮层的「详情/编辑/删除」按**来源**分形态(三颗靠右 + 就地展开/编辑 + AJAX 删除)
用户 2026-10-14 第 2 条。用户原话:
  「下面三个按钮,都放在下面的右侧…分别是详情、编辑和删除。详细用蓝色,编辑用黄色,删除用红色。
    点击详细的时候,**如果这是一个日程**(正常日程里的事件、不是来自观影/旅程等模块)
    → 就**在当前页面让模态框变大**、显示更多信息。
    点击编辑,也是**在当前的界面**弹出编辑日程的框,就地编辑;编辑完之后下面两个按钮:取消和保存。
    点击删除,就直接把这个日程给删除。
    如果当前这个日程**来自其他模块**(观影 / 旅程 / 其它)→ 点击详情就**进入那些模块**展示它。
    **这种情况下,下面就没有编辑、也没有删除,就只有一个详情。**」
拍板:① 应用范围 = 首页日程 + 日程页条目(首页近期任务保持 v43 现状);
      ② 重复日程删除 → 弹三档范围(这一次 / 这一次及以后 / 全部)。

实现(7 个文件):
 · app/helpers.php  fb_entry_json() 扩契约:source('event'/'derived') / ref / del / info /
   repeat / occ。判据 `kind==='custom' && ref_id>0`(**不是**"有没有 edit")。
 · app/Calendar.php  month() 给自定义条目带 _can_write(供 del 判定)。
 · templates/_entry_popup.php  底栏重构成「右侧按钮组 + 7 颗」:详情态三颗、
   编辑态两颗(取消/保存)、删除范围态两颗(取消/确认删除);另加一颗
   **遮罩层里的 CSRF**(正文会被整块换掉,放正文会丢)。
 · assets/js/entry_popup.js  四态状态机(detail/info/edit/delscope),按 source 决定显哪几颗。
 · assets/css/app.css  .fb-entry-acts(margin-left:auto 靠右)+ fb-a-detail/edit/del 三色
   + .is-wide(620→860px)。⚠️ 全是**作用域类**,不动全局 .btn-edit/.btn-danger 的语义。
 · index.php  新增 calendar/event?embed=detail(只吐只读详情块);
   calendar/event_del **认 X-Requested-With 回 JSON**($done() 收口,失败也回原因)。
 · templates/calendar/event.php  支持 $evDetail(只吐 kv 只读块,与可写与否无关)。

本轮修掉两个真 bug(都是"藏不掉"):
 ① `.btn{display:inline-flex}` 是作者样式,**盖过 [hidden] 的浏览器默认**
    ⇒ 底栏四态切换时前面的按钮收不起来、全挤在一条上(派生条目本该只有「详情」,
      实测三颗都露着)。⇒ 显式写 `.fb-entry-acts .btn[hidden]{display:none!important}`。
      本项目文件前部已有同款注释(.pick-row[hidden] 等),属第 N 次同类。
 ② `footShow('detail')` 把三颗无条件点亮,**盖掉** paintDetail 按来源设的 hidden。
    ⇒ detail 组只列「详情」,且 paintDetail 里**先 footShow 再设 edit/del**(顺序写进注释)。

验证(新增 2 个套件):
 · _fb_v49_entry.py(33 条真机断言:三颗靠右/配色/派生只有详情/详情就地变宽/编辑就地/
   删除落库/日程页入口/重复弹范围)
 · _b49_falsify.py(**10 注入 / 21 断言全抓**)
 · 更新 _fb_b43_ui.py 旧断言(「详细信息」→「详情」;自定义日程的详情语义从"跳页"改"就地展开")
 · familybook_deploy.sh 加 v49 静态断言段
 · 全量 e2e / vCard / 几何 / 各批反向验证全绿;部署脚本 0 FAIL(含"第49批形态");
   线上 7 个文件 md5 与本地逐字节一致;线上门禁诊断 13 项全 YES,临时页已删并复验 404

⚠️ 反向验证抓到一条**假绿**(值得记下来):注入「给派生条目也塞 del」时探针**没变红** ——
   因为按钮显隐是 `source` **且** `del` 的多条件"与",只塞 del 在界面上显不出来。
   ⇒ 补一条**数据层**断言(直接读 data-fb-entry 的 del 必须为空),两层判据都在。

⚠️ 末段唯一"失败"是沙箱的安全删除门禁(SAFE_DELETE_BULK_CONFIRM_REQUIRED,按轮次累计、
   本轮流水线删超 50 个文件):单独重跑 _fb_v46_carddav.py 是 **29/0**,与环境有关,与本次改动无关。
2026-10-14 19:05:13 +08:00

216 lines
12 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 条:改一条**重复**日程时,先选"改哪些"。
只在它真的重复时才出现 —— 不重复的日程问这个是多余的。 */ ?>
<?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>
<label class="chk-inline"><input type="radio" name="scope" value="all" checked>
<span>改<b>全部</b>(整条系列)</span></label>
<?php if (!empty($occ)): ?>
<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="once">
<span>只改<b>这一次</b>(<?= e($occ) ?>)</span></label>
<?php else: ?>
<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=);从列表直接打开就只给"全部删除"。
⚠️ 三档的措辞与**编辑**那三档保持一致(改全部 / 改这一次及以后 / 只改这一次),
用户学一次就够,不用记第二套说法。
*/
$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); ?>