Files
FamilyBook/templates/calendar/_event_fields.php
T
laoyang 32e9e9ab84 v47:地址联想三处接入 / 读书页慢的真根因(会话锁)/ 笔记两处 / 日程状态下线
用户 2026-10-13 七条。拍板:影院只记地址、不存经纬度(后续用高德反查);
日程状态只删界面与展示、数据库列保留(「后续会有一个整体清理的工作」)。

①🔴 读书页慢 —— 真根因是**会话锁**,不是封面(我先判断错了方向)
  用户原话:「读书现在还是非常慢,就是进入读书这个页面,刚开始还是非常的慢。」
  线上实测(真公网、同一会话连打 3 次):
      第 1 次 TTFB 0.08s  ← 触发微信读书同步
      第 2 次 TTFB **8.73s**  ← 用户说的"刚开始非常慢"
      第 3 次 TTFB 0.07s
  插桩拿到的服务端时间线把因果钉死:
      21:18:05.483 QUEUED        ← 第 1 个请求排队
      21:18:05.501 FINISH  2ms   ← 响应确实提前发出去了(这步是好的)
      21:18:14.674 SYNC    9.2s  ← 但它仍占着这个人的会话文件锁跑完 9.2 秒
      21:18:14.682 QUEUED        ← 第 2 个请求此刻才真正开始执行
  ⇒ **`fastcgi_finish_request()` 只断开客户端连接、不释放 PHP 文件会话锁。**
    后台同步要 9.2 秒(真打微信读书接口),同一个人的下一个请求一路等到它跑完。
  修法(缺一不可):
    · `WeRead::queueBackgroundSync()` 回调里 **先 `session_write_close()`、
      再 `fastcgi_finish_request()`**(顺序不能反);
    · `index.php` 两处"渲染前排同步"的入口排完**立刻放锁**
      (shutdown 回调不一定执行:客户端中断/服务器超时才不放过);
    · 顺带修 **`Holiday::queueBackgroundFetch()` 的同型问题**(联网 12 秒)。
  为什么以前没发现:v29 引入"后台化"时只验证了"响应是不是提前发出去",
  **单次请求测量永远看不到这个 bug** —— 必须同一会话连打两次才现形。
  线上复验:修复后连打 4 次 = 0.15 / 0.15 / 0.09 / 0.15 秒(修复前第 2 次 8.73s);
  后台同步仍正常(`weread_sync_at` 已更新)。

② 封面:顺手也修了(但这不是用户说的那个问题)
  · `media.php` 读 OSS 前先放会话锁(原来从不放,35 张封面串行);
  · 新增 `Storage::cacheLocally()` 磁盘二级缓存(原来 `read()` 只读不写回);
  · 让 304 不再先拉整张图(原来先取回 body、再判 `If-None-Match`)。
  线上实测:冷取 149ms(走 OSS)→ 热取 35ms(本地缓存、字节一致)
  → 带 `If-None-Match` 返回 **304 / 19.8ms / 空体**;35 张封面 103ms → 20ms(5 倍)。

③ 地址联想:抽成全站共用一份(`assets/js/addr_suggest.js`)+ 接入三处
  · 影院管理页、观影新增页的「新建影院」弹框、日程地点、行程出发/到达;
  · `layout.php` 按需加载(`[data-addr]` 出现才拉 SDK + 我们的脚本),
    并暴露 `fbLoadAddr(scope)` 给 AJAX 片段(v42 踩过"内联 script 不执行");
  · 安全密钥在 SDK 之前设置;回填支持 `data-addr-fill` / `data-addr-for`、
    **只补空白字段**(不覆盖用户手写)、直辖市 `province==city` 去重;
  · 字段名有两套(影院 `address`/`place`,行程 `from_addr`/`to_addr` 缩写 `_addr`),
    `fieldFor()` 带同义词表 + 后缀匹配;
  · 影院**只存地址、不存经纬度**(按用户要求)。

④ 笔记两条
  · 「点保存没反应」真因:附件区是个 `<form>` **套在保存表单里** ——
    HTML 不允许 form 套 form,浏览器丢掉内层标签、保留它的 `</form>`,
    于是**保存按钮成了孤儿**(`button.form === null`),点了不提交也不报错。
    真浏览器实测:修前 `btnInsideForm:false` / 点后 POST=0;修后 true / POST=1。
    改法:附件区改 `<div id="noteAttForm">`,上传按钮用 `formaction`。
  · 详情页宽度:`.note-read { max-width: 820px }` → `none`
    (内容卡 820px vs 下方共享卡 1032px;修后两者都是 1032px)。

⑤ 日程状态下线
  · 删表单里的「状态」下拉、`Calendar::statusWord()`、浮层 JSON 的 `status_word`;
  · **保留** `events.status` 列与 `Calendar::statusOf()`(契约不动,等整体清理)。

验证(新增 5 个套件):
  · `_fb_v47_addr.py`(接线 24 条)/ `_fb_v47_addr_sim.py`(真浏览器 + 假高德,16 条)
  · `_fb_v47_readperf.py`(缓存与会话锁,23 条)
  · `_fb_v47_notes.py`(真浏览器,9 条)
  · `_b47_readperf_falsify.py`(反向验证 9 个注入)—— **9/9 全抓住**
  · 全量 e2e **1510/0**;vCard 规格+往返 40/0;TB 探针 44/0;
    v44 反向验证 17/0;全流水线 **PUSH_RC=0**(含"第47批形态"静态断言)
  · **线上终验**:读书页整页 81ms、封面 103→20ms、media 冷 149→热 35→304 19.8ms、
    修复前第 2 次请求 8.73s → 修复后 0.15s、地址联想 meta[fb-addr] 带真高德 key

顺带修掉三处**探针自身**的假红/假绿(详见 pitfalls.md):
  · `_b44_falsify.py` 的"整小时"注入在**整点**与正确实现等价 ⇒ 换成恒错值;
  · `_fb_b44_ui.py` 3b 的并发计时断言在单进程 `php -S` 下必然假红 ⇒ 改断言确定事实;
  · 多处探针的"清理删除"撞沙箱删除门禁 ⇒ 探针**中途死掉**看起来像产品坏了
    ⇒ 统一改成 `_safe_rm()` / 不删。
2026-10-14 01:20:40 +08:00

353 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
/**
* 日程表单的字段块(新建与编辑共用一份 —— 复制一遍迟早改漏一处)。
*
* 字段按 **RFC 5545(iCalendar)/ RFC 4791(CalDAV)** 的 VEVENT 属性定:
* 标题 = SUMMARY
* 地点 = LOCATION
* 开始/结束 = DTSTART / DTEND(DATETIME,精确到秒)
* 全天 —— VEVENT 没有"全天"这个属性,约定俗成是把 DTSTART 写成 DATE 值;
* 我们统一存 DATETIME,全天时时间部分为 00:00:00,导出 ics 时再落成 DATE 值。
* 提醒 = VALARM 的 TRIGGER(提前多久);不提醒 = 不写 VALARM
* 状态 = STATUS(CONFIRMED / TENTATIVE / CANCELLED)
* 忙闲 = TRANSP(OPAQUE=忙 / TRANSPARENT=空闲)
* 备注 = DESCRIPTION
* 附件 = ATTACH
*
* 用法(include 前设好):
* $evFields 正在编辑的日程(新建时传空数组)
* $evAllDay 全天的默认值(新建默认**不勾**,用户 2026-10-02 第 6 条)
* $evMvStyle 🔴 v43:「标签 62px 右对齐 + 冒号 + 控件」的**观影式**排版开关。
* 新建日程那个弹框要它(用户:日程新建页排版和观影/行程一样);
* 编辑页(`calendar/event.php`)继续用原来的 `.form-grid` 两列。
*/
$evF = is_array($evFields ?? null) ? $evFields : [];
$evAllDayDefault = (bool)($evAllDay ?? false);
$evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evAllDayDefault;
/* 🔴 v43(用户 2026-10-11 第 5 条):观影式排版开关。 */
$evMv = !empty($evMvStyle);
/* 标签包装:观影式用 `.mv-row` + `.mv-lb`(冒号由 CSS ::after 给);
否则用原来的 `.field` + `<label>`。
⚠️ 抽成闭包是为了**不在下面每处 if/else 手抄两遍结构** ——
抄两遍必然有一天两边不同步(v37 那种"改了 A 忘了 B"的坑)。
⚠️ 「必填星号」作为**参数**传(不要用 `use` 捕获 —— 闭包定义时那个变量还没赋值,
捕获到的是 null,星号永远不出现)。 */
$evRowOpen = static function (string $full = '', bool $hidden = false) use ($evMv): string {
return '<div class="' . ($evMv ? 'mv-row' : 'field') . ($full !== '' ? ' ' . $full : '') . '"'
. ($hidden ? ' hidden' : '') . '>';
};
/* 重复规则**先算好**:下面"结束条件 / 重复次数 / 重复到"三行要不要默认藏起来,
取决于它(见 v44 第 2 条)。原来 `$rp` 是在「重复」那一行才 parse 的,
那三行排在它后面 ⇒ 拿不到。提到最前面,只算一次。 */
$evRp = \App\Repeat::parse((string)($evF['rrule'] ?? ''));
$evRepeating = (string)($evRp['freq'] ?? '') !== '';
$evLab = static function (string $text, string $for = '', bool $star = false) use ($evMv): string {
if ($evMv) {
// 观影式:标签是 <span class="mv-lb">,冒号由 CSS ::after 给
return '<span class="mv-lb">' . $text . ($star ? '<i>*</i>' : '') . '</span>';
}
return '<label' . ($for !== '' ? ' for="' . e($for) . '"' : '') . '>' . $text . ($star ? ' *' : '') . '</label>';
};
?>
<?php echo $evRowOpen('full'); ?>
<?= $evLab('标题', 'ev-title', true) ?>
<input class="input" id="ev-title" name="title" required
value="<?= e((string)($evF['title'] ?? '')) ?>" placeholder="如:家长会">
</div>
<?php echo $evRowOpen(); ?>
<?= $evLab('地点', 'ev-location') ?>
<?php /* 🔴 v47(用户 2026-10-13):「日程中,当我输入地点的时候,也应该调用高德的接口,
补齐详细地址。」⇒ 地点框挂高德联想。
⚠️ 日程表里**没有**单独的"详细地址"字段(地点就一栏),所以这里不做"回填",
只把**联想选中的正式地名**写回输入框 —— 价值是"把口语化的'学校三楼会议室'
换成高德认得的规范地名",后续要查经纬度/导航才查得到。
⚠️ 这段表单既在整页(各视图的日程弹框)也在 AJAX 片段里出现,
所以打好 `data-addr` 标记即可 —— 由 layout 的 loader 与
弹框打开时的 `fbLoadAddr(scope)` 负责绑定(与 person_picker 同一套)。 */ ?>
<input class="input" id="ev-location" name="location"
value="<?= e((string)($evF['location'] ?? '')) ?>" placeholder="如:学校三楼会议室"
data-addr>
</div>
<?php echo $evRowOpen(); ?>
<?= $evLab('提醒') ?>
<?php /* v24 第 20 批 第 8 条:**多选**(用户原话:「一个日程,我可以选择让它
5分钟提醒,10分钟提醒等等。」)。
⚠️ 用 checkbox 而不是 `<select multiple>`:后者在手机上要"按住再点",
几乎没法用(任务共享那个 multiple 下拉就是因此被换掉的)。
⚠️ 字段名必须是 `reminder_minutes[]` —— Calendar::reminderMinutes() 认得数组。
写成 `reminder_minutes`(不带 [])会让 PHP 只留最后一个勾选。
🔴 v43(用户第 5 条):他要求把这一栏底下那句「多选与不提醒」的说明去掉
⇒ 这里删掉那个 `.tip`(多选语义由复选框本身表达)。 */
$curReminders = \App\Calendar::reminderList($evF['reminder_minutes'] ?? null); ?>
<div class="rem-picks">
<?php foreach (\App\Calendar::REMINDERS as $mins => $word): ?>
<label class="chk-inline rem-pick">
<input type="checkbox" name="reminder_minutes[]" value="<?= (int)$mins ?>"
<?= in_array((int)$mins, $curReminders, true) ? 'checked' : '' ?>>
<span><?= e($word) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
<?php /* v24 第 5、6 条:起止各一个「日期 + 时分秒」合在一起的控件(全局统一),
不再拆成"日期一格 + 时刻一格"。两个控件长得一样,扫一眼就知道哪个是开始。 */ ?>
<?php /* 用户 2026-10-04 第 7 条:日程的起止也是一个**范围** ——
升级后由一个 Semi dateTimeRange 一次选完。服务端仍收两组 *_date/_hour/… 字段。 */ ?>
<?php /* 🔴 v24 第 21 批 第 7 条:「开始时间和结束时间那里,用我们统一的时间范围选择器
重做,现在功能无法使用。」
「功能无法使用」的真根因(真机探针实测出来的,不是猜的):
app.js 的 upgradeRange() 有一道「**两端都得可见**才升级成范围控件」的判断
(`boxes.every(bx => bx.offsetParent !== null)`)。而这一版**结束那一端默认
带 `hidden`**(要点「+ 添加结束时间」才出现)→ 判断永远不成立 →
范围控件**从来没被挂上去过**(实测 data-semi-range-done 恒为 null)。
更糟的是 CSS 还有 `html.js .dt-range:not(.is-semi-range) > .dt-input
{ visibility: hidden }` —— 两端于是都是**不可见**的:
屏幕上一片空白、点哪儿都没反应。这就是「无法使用」的全部。
修法:**结束那一端不再默认藏起来**,范围控件一次性就挂上。
起止成对本来就该一起填。想表达"只有一个时刻"的话,范围控件 clear 一下即可。
⚠️ 顺带删掉「+ 添加结束时间 / 去掉结束时间」那套开关:
它切换显隐会让范围控件反复重挂;范围控件自己就能表达"没有结束"。
🔴🔴 v43(用户 2026-10-11 第 5 条):「开始和结束时间也没有用我们最新的控件,
直接就是四个字"时间范围"。」
—— 两处真缺陷一起修:
① 这个字段块**没传 `$dtPlugin='antd'`** ⇒ 用户看到的还是年/月/日 + 时/分
五个原生下拉,而不是他点名要的 Ant Design 控件;
② 标签写的是「时间范围」四个字,没告诉人"这是开始 / 结束"。
⇒ `$dtPlugin='antd'`(与观影/行程同一套:面板有可点的时/分列、中文「确定」),
标签改「开始 / 结束」(并去掉那颗必填星号的位置歧义 —— 只标在开始上)。
⚠️ 契约一字未改:隐藏字段仍是 `*_date/_hour/_minute`;antd 拉不动时
自动退回自建下拉(渐进增强)。
🔴🔴 v44(用户 2026-10-12 第 2 条):「开始/结束,改为:日期范围:。
之前的日期选择控件还有,需要删除。」
—— 用户要三种东西:① 标签「日期范围」;② 标签与**最新控件同行**;
③ 原生那套(年/月/日 + 时/分 五个下拉)**真的消失**。
🔴🔴 **"还在"的真根因(结构性,不是样式没写)**:
观影那对是 `‹div.mv-row.dt-range› ‹div.dt-input›`(**直接子**),
而日程这对是 `‹div.dt-range› ‹div.field.ev-dt› ‹div.dt-input›`
—— `.dt-input` 是**孙子**。
于是那条最常见的收起规则 `.dt-range.is-antd > .dt-input`(**直接子**选择器)
**在日程上根本不命中** ⇒ antd 挂上了、原生五个下拉照样显示
(用户看到两个控件并排,"之前的日期选择控件还有")。
⚠️ 与 v43 纪念日那次是**同一个家族**:结构一变,直接子选择器就静默失效。
⇒ 两手都做:① 把日程的结构改成与观影**一致**(`.dt-input` 直接子);
② CSS 里把 `>` 放宽成后代选择器(0,2,0 → 仍高于基类),
将来结构再变也不会又冒出第二个控件。
⇒ 形态:由 `_datetime.php` 的 `$dtLabel` 把「日期范围:」
**内联在控件那一行**(不靠宿主的行结构),一个 `.mv-lb` 挂在
`.dt-input` 里 → 三样天然同行,宽度不够时整体换行。 */ ?>
<div class="dt-range<?= $evMv ? ' mv-row is-tagged' : '' ?>">
<?php /* ⚠️ 标签只有**一个**,挂在**第一个端点**上(不是各挂各的):
升级成一个范围控件之后,"开始"与"结束"就是同一个控件的两个端点,
两个标签会变成"看着像两个字段、其实是一个"的错觉。
⚠️ 观影式:标签由 `_datetime.php` 内联(`$dtLabel`)—— 要的是与控件同行;
两列式(编辑整页):标签仍是 `.dt-range-label`,在两端之上。 */ ?>
<?php if (!$evMv): ?>
<span class="dt-range-label">日期范围 *</span>
<?php endif; ?>
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true;
$dtRange = 'start'; $dtPlugin = 'antd'; $dtClass = 'field ev-dt'; $dtFieldId = '';
if ($evMv) { $dtLabel = '日期范围'; $dtLabelStar = true; }
require __DIR__ . '/../_datetime.php'; ?>
<?php $dtName = 'ends_at'; $dtId = 'ev-end';
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false;
$dtRange = 'end'; $dtPlugin = 'antd';
$dtClass = 'field ev-dt'; $dtFieldId = 'ev-end-field';
/* 结束端点不重复标签(同一个范围控件,标签已经挂在开始那一端上) */
$dtLabel = ''; $dtLabelStar = false;
require __DIR__ . '/../_datetime.php'; ?>
</div><?php /* /dt-range */ ?>
<?php /* 🔴 v43(用户第 5 条):他要求把「结束留空即无明确结束」的那条提示去掉
⇒ 那句 `.field-hint` 整块删除(不再输出)。 */ ?>
<?php echo $evRowOpen(); ?>
<label class="chk-inline">
<input type="checkbox" id="ev-allday" name="all_day" value="1"
<?= $evIsAllDay ? 'checked' : '' ?>>
<span>全天</span>
</label>
<?php /* 🔴 v43(用户第 5 条):他要求把「全天只管日期、不管几点」的说明也去掉
⇒ 那句 `.field-hint` 整块删除。 */ ?>
</div>
<?php /* 用户 2026-10-03 第 5、6 条:重复日程。
放在「全天」之后的自然位置:先定"哪一天/几点",再定"要不要重复"。
周期用 FREQ 记号,结束条件三档(一直 / N 次 / 到某天),另加"工作日"。 */ ?>
<?php echo $evRowOpen(); ?>
<?= $evLab('重复', 'ev-freq') ?>
<select class="select" id="ev-freq" name="freq">
<?php foreach (\App\Repeat::FREQS as $k => $w): ?>
<option value="<?= e($k) ?>"<?= (string)$evRp['freq'] === (string)$k ? ' selected' : '' ?>><?= e($w) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php /* 🔴 v44(用户 2026-10-12 第 2 条):「重复这里,只有选择重复的时候,
下面的结束条件和重复次数、重复到,这几个选项才出现,否则就隐藏。」
⇒ 三行**服务端**就按"是否重复"给出 `hidden`(不是只靠 JS):
· 回填编辑(本来就有 rrule)时它们该出现;
· 新建(默认"不重复")时它们从一开始就是隐藏的 —— 页面上不会闪一下才收起来。
⚠️ 行**本身**也藏:原来只有行**里面**那个 box 带 hidden,
于是标签还孤零零留在表单里(用户看到的"还在")。
⚠️ 用户同条还要「重复次数(含第一次)」那个括号去掉 ⇒ 标签改「重复次数」。 */ ?>
<?php echo $evRowOpen('', !$evRepeating); ?>
<?= $evLab('结束条件', 'ev-rep-end') ?>
<div id="ev-rep-end-box"<?= $evRepeating ? '' : ' hidden' ?>>
<select class="select" id="ev-rep-end" name="rep_end">
<?php foreach (\App\Repeat::ENDS as $k => $w): ?>
<option value="<?= e($k) ?>"><?= e($w) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<?php echo $evRowOpen('', !$evRepeating || (int)$evRp['count'] <= 0); ?>
<?= $evLab('重复次数', 'ev-rep-count') ?>
<div id="ev-rep-count-box"<?= ($evRepeating && (int)$evRp['count'] > 0) ? '' : ' hidden' ?>>
<input class="input" type="number" min="1" max="2000" id="ev-rep-count" name="rep_count"
value="<?= (int)($evRp['count'] ?: 10) ?>">
</div>
</div>
<?php echo $evRowOpen('', !$evRepeating || (string)$evRp['until'] === ''); ?>
<?= $evLab('重复到', 'ev-rep-until') ?>
<div id="ev-rep-until-box"<?= ($evRepeating && (string)$evRp['until'] !== '') ? '' : ' hidden' ?>>
<?php /* 🔴 v45(用户 2026-10-12 第 1 条):「添加日程这里,当选择了重复,
并且选择了重复到某天为止,这时候重复到会出现,有一个日历选择框,
这个日历选择框也改为我们最新的控件。」
⇒ 把原来那个原生 `<input type="date">` 换成 AntD **纯日期** DatePicker。
⚠️ 做法与「纪念日 → 日期」完全同一套(v42/v43 已验证):
· 外层 `.dt-input` + `data-antd-date="1"` → app.js 的 `mountAntdDate` 接管;
· 值载体仍是**同一个** `name="rep_until"` 的 `<input type="date">`,
只是被 `.dt-native` 包起来(否则那条"收起原生框"的 CSS 不命中 ⇒
会出现"多了一个控件",正是 v43 纪念日踩过的坑);
· 服务端契约**一字不改**:`Repeat::build($_POST)` 照旧读 `rep_until`。
⚠️ 千万别给这个 `.dt-input` 加 `data-dt="…"`:那是"年/月/日+时/分"那套
四字段契约的标记,加上去会被 `upgrade()` 抢走、反而绑错处理器。 */ ?>
<div class="dt-input dt-antd-single" data-antd-date="1" id="ev-rep-until-wrap">
<span class="dt-native">
<input type="date" class="dt-date" id="ev-rep-until" name="rep_until"
value="<?= e((string)($evRp['until'] ?? '')) ?>" aria-label="重复到">
</span>
</div>
</div>
</div>
<?php /* 🔴 v47(用户 2026-10-13):「日程的那个状态,什么确定、待定、取消,
我觉得没有什么意义,把它给删掉吧,不要了。」
⇒ 「状态」整块**删除**。
⚠️ 服务端契约**刻意保留**(用户明确说"数据库可以保留,后续会有一个整体的清理"):
- `events.status` 列留着、不删;
- `Calendar::save()` 收到不带 status 的表单时,用**原值**(更新)或
`'confirmed'`(新建),与删掉这一栏之前**完全一致** ——
不会因为删了控件就把已有日程的状态改写掉(见 index.php 里那两处
`(string)($ev2['status'] ?? 'confirmed')`)。
- 导出 iCalendar 时 STATUS 照旧按原值走,别人的日历里不会看到状态变化。
下方原来还有一段给 `ev-status` 的联动脚本,也一并删。 */ ?>
<?php /* 🔴 v44(用户 2026-10-12 第 2 条):「这段时间算忙碌,这一块直接给它去掉就行了,
没有什么用。」⇒ 「这段时间算 / 忙碌 / 空闲」整块删除。
⚠️ 服务端契约**没变**:`transp` 字段没随表单提交时,`!empty($_POST['transp'])`
一律得 false(= 忙碌),与原来的默认值一致 —— 不会因为删了这一块
而把已有日程的忙闲改掉(更新时传的是 `(int)($ev2['transp'] ?? 0)` 原值)。
下方 script 里原来还有一段给 `ev-transp` 的联动,也一并删。 */ ?>
<?php echo $evRowOpen('full'); ?>
<?= $evLab('备注', 'ev-note') ?>
<textarea class="input" id="ev-note" name="note" rows="3"
placeholder="要带什么、跟谁一起…"><?= e((string)($evF['note'] ?? '')) ?></textarea>
</div>
<?php echo $evRowOpen('full'); ?>
<?= $evLab('附件(可多选)') ?>
<?php /* v24 第 21 批 第 7 条:统一成「点击或拖动进行上传」的框。
⚠️ 文件框的 id 保持 `ev-attach`(原来就叫这个)—— e2e 与部署断言里
有按 id 定位的写法,改名会把它们静默打成假红。 */
$sdName = 'attach[]'; $sdId = 'ev-attach'; $sdSlot = 'photo';
require __DIR__ . '/../_stage_drop.php'; ?>
</div>
<script>
(function () {
/* 「全天」勾上就把两个时刻控件的**时间部分**收起来 —— 日期还得留着。
只藏 .dt-time(时/分/秒那一段),.dt-date 是日期,藏了就没法选哪天了。 */
var box = document.getElementById('ev-allday');
if (!box) return;
function sync() {
Array.prototype.forEach.call(document.querySelectorAll('[data-dt] .dt-time'), function (el) {
el.hidden = box.checked;
});
}
box.addEventListener('change', sync);
sync();
/* ⚠️ v24 第 21 批 第 7 条:原来这里有一整段「结束时间显隐 + 跟着开始时间走」的 JS。
现在起止两端**始终可见**,并且由范围控件统一接管(一次选完两端),
所以那段"同步两端"的逻辑整个删掉 —— 留着不但没用,还会和范围控件抢着改那两个
原生字段(两处都写 → 值互相覆盖)。范围控件选完会自己把两端分别写回原生字段
(见 app.js 的 onRange → writeDT),服务端契约不变。 */
/* 重复:选了周期才显示"结束条件";结束条件选"次数"才显示次数框、选"到某天"才显示日期框。
三行默认都 hidden —— 不重复时一个都不该出现(少即是清楚)。
🔴 v44(用户第 2 条):藏的是**整行**(`.mv-row` / `.field`),不是行里那个 box ——
只藏 box 的话标签还孤零零留在表单里。行 = box 的 `closest('.mv-row, .field')`。 */
var freq = document.getElementById('ev-freq');
var endBox = document.getElementById('ev-rep-end-box');
var endSel = document.getElementById('ev-rep-end');
var cntBox = document.getElementById('ev-rep-count-box');
var untilBox = document.getElementById('ev-rep-until-box');
function rowOf(el) {
return el && el.closest ? el.closest('.mv-row, .field') : null;
}
function toggle(el, on) {
if (!el) { return; }
el.hidden = !on;
var row = rowOf(el);
if (row) { row.hidden = !on; } // 连带整行一起显隐
}
function syncRep() {
if (!freq || !endBox) { return; }
var repeating = freq.value !== '';
toggle(endBox, repeating);
var how = endSel ? endSel.value : 'never';
toggle(cntBox, repeating && how === 'count');
toggle(untilBox, repeating && how === 'until');
}
if (freq) { freq.addEventListener('change', syncRep); }
if (endSel) { endSel.addEventListener('change', syncRep); }
// 回填:编辑一条已有的重复日程时,把结束条件按库里的值选对
<?php if ((int)$evRp['count'] > 0): ?>
if (endSel) { endSel.value = 'count'; }
<?php elseif ((string)$evRp['until'] !== ''): ?>
if (endSel) { endSel.value = 'until'; }
<?php endif; ?>
syncRep();
/* 「全天」+「重复」的组合提示:重复日程必须有开始日期,
否则展开时找不到锚点(后端会当成"不重复"处理)。 */
var startDate = document.querySelector('[data-dt] input.dt-date');
if (freq && startDate) {
freq.addEventListener('change', function () {
if (freq.value !== '' && startDate.value === '') {
startDate.setCustomValidity('重复日程要先选开始日期');
} else {
startDate.setCustomValidity('');
}
});
}
})();
</script>
<?php unset($evF, $evAllDayDefault, $evIsAllDay, $evFields, $evAllDay, $evMv, $evMvStyle,
$evRowOpen, $evLab, $evLabelStar, $evRp, $evRepeating); ?>