2026-10-03 08:18:05 +08:00
|
|
|
|
<?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 条)
|
|
|
|
|
|
*/
|
|
|
|
|
|
$evF = is_array($evFields ?? null) ? $evFields : [];
|
|
|
|
|
|
$evAllDayDefault = (bool)($evAllDay ?? false);
|
|
|
|
|
|
$evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evAllDayDefault;
|
|
|
|
|
|
?>
|
|
|
|
|
|
<div class="field full">
|
|
|
|
|
|
<label for="ev-title">标题 *</label>
|
|
|
|
|
|
<input class="input" id="ev-title" name="title" required
|
|
|
|
|
|
value="<?= e((string)($evF['title'] ?? '')) ?>" placeholder="如:家长会">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label for="ev-location">地点</label>
|
|
|
|
|
|
<input class="input" id="ev-location" name="location"
|
|
|
|
|
|
value="<?= e((string)($evF['location'] ?? '')) ?>" placeholder="如:学校三楼会议室">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>提醒</label>
|
2026-10-06 03:14:18 +08:00
|
|
|
|
<?php /* v24 第 20 批 第 8 条:**多选**(用户原话:「一个日程,我可以选择让它
|
|
|
|
|
|
5分钟提醒,10分钟提醒等等。」)。
|
|
|
|
|
|
⚠️ 用 checkbox 而不是 `<select multiple>`:后者在手机上要"按住再点",
|
|
|
|
|
|
几乎没法用(任务共享那个 multiple 下拉就是因此被换掉的)。
|
|
|
|
|
|
⚠️ 字段名必须是 `reminder_minutes[]` —— Calendar::reminderMinutes() 认得数组。
|
|
|
|
|
|
写成 `reminder_minutes`(不带 [])会让 PHP 只留最后一个勾选。 */
|
|
|
|
|
|
$curReminders = \App\Calendar::reminderList($evF['reminder_minutes'] ?? null); ?>
|
|
|
|
|
|
<div class="rem-picks">
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<?php foreach (\App\Calendar::REMINDERS as $mins => $word): ?>
|
2026-10-06 03:14:18 +08:00
|
|
|
|
<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>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<?php endforeach; ?>
|
2026-10-06 03:14:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<span class="tip">可以选多个(到点各响一次);一个都不选就是不提醒。</span>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<?php /* v24 第 5、6 条:起止各一个「日期 + 时分秒」合在一起的控件(全局统一),
|
|
|
|
|
|
不再拆成"日期一格 + 时刻一格"。两个控件长得一样,扫一眼就知道哪个是开始。 */ ?>
|
2026-10-04 10:46:50 +08:00
|
|
|
|
<?php /* 用户 2026-10-04 第 7 条:日程的起止也是一个**范围** ——
|
|
|
|
|
|
升级后由一个 Semi dateTimeRange 一次选完。服务端仍收两组 *_date/_hour/… 字段。 */ ?>
|
2026-10-06 12:08:40 +08:00
|
|
|
|
<?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 一下即可。
|
|
|
|
|
|
⚠️ 顺带删掉「+ 添加结束时间 / 去掉结束时间」那套开关:
|
|
|
|
|
|
它切换显隐会让范围控件反复重挂;范围控件自己就能表达"没有结束"。 */ ?>
|
2026-10-04 10:46:50 +08:00
|
|
|
|
<div class="dt-range">
|
2026-10-06 12:08:40 +08:00
|
|
|
|
<?php /* ⚠️ 标签只有**一个**,挂在范围容器上(不是各挂各的):
|
|
|
|
|
|
升级成一个范围控件之后,"开始"与"结束"就是同一个控件的两个端点,
|
|
|
|
|
|
两个标签会变成"看着像两个字段、其实是一个"的错觉。 */ ?>
|
|
|
|
|
|
<span class="dt-range-label">开始 / 结束 *</span>
|
|
|
|
|
|
<div class="field ev-dt" data-dt>
|
|
|
|
|
|
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
|
2026-10-10 00:26:42 +08:00
|
|
|
|
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true;
|
2026-10-06 12:08:40 +08:00
|
|
|
|
$dtRange = 'start';
|
|
|
|
|
|
require __DIR__ . '/../_datetime.php'; ?>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="field ev-dt" data-dt id="ev-end-field">
|
|
|
|
|
|
<?php $dtName = 'ends_at'; $dtId = 'ev-end';
|
2026-10-10 00:26:42 +08:00
|
|
|
|
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false;
|
2026-10-06 12:08:40 +08:00
|
|
|
|
$dtRange = 'end';
|
|
|
|
|
|
require __DIR__ . '/../_datetime.php'; ?>
|
|
|
|
|
|
</div>
|
2026-10-04 10:46:50 +08:00
|
|
|
|
</div><?php /* /dt-range */ ?>
|
2026-10-06 12:08:40 +08:00
|
|
|
|
<span class="field-hint" style="display:block;margin:-4px 0 10px">
|
|
|
|
|
|
结束留空就表示没有明确的结束(只占开始那一刻/那一天)。
|
|
|
|
|
|
</span>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label>
|
|
|
|
|
|
<input type="checkbox" id="ev-allday" name="all_day" value="1"
|
|
|
|
|
|
<?= $evIsAllDay ? 'checked' : '' ?>>
|
|
|
|
|
|
全天
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<span class="field-hint">勾上就不管具体几点,只占这一天</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-03 11:30:48 +08:00
|
|
|
|
<?php /* 用户 2026-10-03 第 5、6 条:重复日程。
|
|
|
|
|
|
放在「全天」之后的自然位置:先定"哪一天/几点",再定"要不要重复"。
|
|
|
|
|
|
周期用 FREQ 记号,结束条件三档(一直 / N 次 / 到某天),另加"工作日"。 */ ?>
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label for="ev-freq">重复</label>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field" id="ev-rep-end-box" hidden>
|
|
|
|
|
|
<label for="ev-rep-end">结束条件</label>
|
|
|
|
|
|
<select class="select" id="ev-rep-end" name="rep_end">
|
|
|
|
|
|
<?php foreach (\App\Repeat::ENDS as $k => $w): ?>
|
|
|
|
|
|
<option value="<?= e($k) ?>"<?= $rp['count'] > 0 ? '' : '' ?>><?= e($w) ?></option>
|
|
|
|
|
|
<?php endforeach; ?>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field" id="ev-rep-count-box" hidden>
|
|
|
|
|
|
<label for="ev-rep-count">重复次数(含第一次)</label>
|
|
|
|
|
|
<input class="input" type="number" min="1" max="2000" id="ev-rep-count" name="rep_count"
|
|
|
|
|
|
value="<?= (int)($rp['count'] ?: 10) ?>">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field" id="ev-rep-until-box" hidden>
|
|
|
|
|
|
<label for="ev-rep-until">重复到</label>
|
|
|
|
|
|
<input class="input" type="date" id="ev-rep-until" name="rep_until"
|
|
|
|
|
|
value="<?= e((string)($rp['until'] ?? '')) ?>">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-10-03 08:18:05 +08:00
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label for="ev-status">状态</label>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field">
|
|
|
|
|
|
<label for="ev-transp">这段时间算</label>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field full">
|
|
|
|
|
|
<label for="ev-note">备注</label>
|
|
|
|
|
|
<textarea class="input" id="ev-note" name="note" rows="3"
|
|
|
|
|
|
placeholder="要带什么、跟谁一起…"><?= e((string)($evF['note'] ?? '')) ?></textarea>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="field full">
|
2026-10-06 12:08:40 +08:00
|
|
|
|
<label>附件(可多选)</label>
|
|
|
|
|
|
<?php /* v24 第 21 批 第 7 条:统一成「点击或拖动进行上传」的框。
|
|
|
|
|
|
⚠️ 文件框的 id 保持 `ev-attach`(原来就叫这个)—— e2e 与部署断言里
|
|
|
|
|
|
有按 id 定位的写法,改名会把它们静默打成假红。 */
|
|
|
|
|
|
$sdName = 'attach[]'; $sdId = 'ev-attach'; $sdSlot = 'photo';
|
|
|
|
|
|
require __DIR__ . '/../_stage_drop.php'; ?>
|
2026-10-03 08:18:05 +08:00
|
|
|
|
</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();
|
2026-10-03 11:30:48 +08:00
|
|
|
|
|
2026-10-06 12:08:40 +08:00
|
|
|
|
/* ⚠️ v24 第 21 批 第 7 条:原来这里有一整段「结束时间显隐 + 跟着开始时间走」的 JS。
|
|
|
|
|
|
现在起止两端**始终可见**,并且由 Semi 的范围控件统一接管(一次选完两端),
|
|
|
|
|
|
所以那段"同步两端"的逻辑整个删掉 —— 留着不但没用,还会和范围控件抢着改那两个
|
|
|
|
|
|
原生字段(两处都写 → 值互相覆盖)。范围控件选完会自己把两端分别写回原生字段
|
|
|
|
|
|
(见 app.js 的 onRange → writeDT),服务端契约不变。 */
|
2026-10-03 11:30:48 +08:00
|
|
|
|
|
|
|
|
|
|
/* 重复:选了周期才显示"结束条件";结束条件选"次数"才显示次数框、选"到某天"才显示日期框。
|
|
|
|
|
|
三个框默认都 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('');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-10-03 08:18:05 +08:00
|
|
|
|
})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
<?php unset($evF, $evAllDayDefault, $evIsAllDay, $evFields, $evAllDay); ?>
|