Files
FamilyBook/templates/calendar/_event_fields.php
T

281 lines
15 KiB
PHP
Raw Normal View History

<?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 = '') use ($evMv): string {
return '<div class="' . ($evMv ? 'mv-row' : 'field') . ($full !== '' ? ' ' . $full : '') . '">';
};
$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') ?>
<input class="input" id="ev-location" name="location"
value="<?= e((string)($evF['location'] ?? '')) ?>" placeholder="如:学校三楼会议室">
</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 拉不动时
自动退回自建下拉(渐进增强)。 */ ?>
<div class="dt-range<?= $evMv ? ' mv-row' : '' ?>">
<?php /* ⚠️ 标签只有**一个**,挂在范围容器上(不是各挂各的):
升级成一个范围控件之后,"开始"与"结束"就是同一个控件的两个端点,
两个标签会变成"看着像两个字段、其实是一个"的错觉。 */ ?>
<?php if ($evMv): ?>
<span class="mv-lb dt-range-label">开始 / 结束<i>*</i></span>
<?php else: ?>
<span class="dt-range-label">开始 / 结束 *</span>
<?php endif; ?>
<div class="field ev-dt" data-dt>
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true;
$dtRange = 'start'; $dtPlugin = 'antd';
require __DIR__ . '/../_datetime.php'; ?>
</div>
<div class="field ev-dt" data-dt id="ev-end-field">
<?php $dtName = 'ends_at'; $dtId = 'ev-end';
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false;
$dtRange = 'end'; $dtPlugin = 'antd';
require __DIR__ . '/../_datetime.php'; ?>
</div>
</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 $rp = \App\Repeat::parse((string)($evF['rrule'] ?? '')); ?>
<?php foreach (\App\Repeat::FREQS as $k => $w): ?>
<option value="<?= e($k) ?>"<?= (string)$rp['freq'] === (string)$k ? ' selected' : '' ?>><?= e($w) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php echo $evRowOpen(); ?>
<?= $evLab('结束条件', 'ev-rep-end') ?>
<div id="ev-rep-end-box" 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(); ?>
<?= $evLab('重复次数(含第一次)', 'ev-rep-count') ?>
<div id="ev-rep-count-box" hidden>
<input class="input" type="number" min="1" max="2000" id="ev-rep-count" name="rep_count"
value="<?= (int)($rp['count'] ?: 10) ?>">
</div>
</div>
<?php echo $evRowOpen(); ?>
<?= $evLab('重复到', 'ev-rep-until') ?>
<div id="ev-rep-until-box" hidden>
<input class="input" type="date" id="ev-rep-until" name="rep_until"
value="<?= e((string)($rp['until'] ?? '')) ?>">
</div>
</div>
<?php echo $evRowOpen(); ?>
<?= $evLab('状态', 'ev-status') ?>
<select class="select" id="ev-status" name="status">
<?php $st = (string)($evF['status'] ?? 'confirmed'); ?>
<option value="confirmed"<?= $st === 'confirmed' ? ' selected' : '' ?>>确定</option>
<option value="tentative"<?= $st === 'tentative' ? ' selected' : '' ?>>待定</option>
<option value="cancelled"<?= $st === 'cancelled' ? ' selected' : '' ?>>取消</option>
</select>
</div>
<?php echo $evRowOpen(); ?>
<?= $evLab('这段时间算', 'ev-transp') ?>
<select class="select" id="ev-transp" name="transp">
<?php $tp = (int)($evF['transp'] ?? 0); ?>
<option value="0"<?= $tp === 0 ? ' selected' : '' ?>>忙碌</option>
<option value="1"<?= $tp === 1 ? ' selected' : '' ?>>空闲(别人还能约)</option>
</select>
</div>
<?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 —— 不重复时一个都不该出现(少即是清楚)。 */
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 syncRep() {
if (!freq || !endBox) return;
var repeating = freq.value !== '';
endBox.hidden = !repeating;
var how = endSel ? endSel.value : 'never';
if (cntBox) { cntBox.hidden = !repeating || how !== 'count'; }
if (untilBox) { untilBox.hidden = !repeating || how !== 'until'; }
}
if (freq) { freq.addEventListener('change', syncRep); }
if (endSel) { endSel.addEventListener('change', syncRep); }
// 回填:编辑一条已有的重复日程时,把结束条件按库里的值选对
<?php if ($rp['count'] > 0): ?>
if (endSel) { endSel.value = 'count'; }
<?php elseif ($rp['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); ?>