用户原话(6 条,一次给全):
①「观影详情页面。观影图片下面有一个块…要加照片请点上方「编辑」…这个块不要,
直接把这个块删除。」
②「编辑页面的时间…选日期的这个控件非常非常不好用。就是如果我把年和月那个地方
拉下来,我选完年和月之后,它并不会回到让我选日的那个界面,它还是不动的…
要么换一个控件,要么在那个地方加一个确定按钮…现在开始和结束时间分在了两行…
能放得下的话就应该放一排…不要让它默认显示在两行上…"散场时间可不填。"这句话去掉。」
③「首页上今天的日程…分了几列,列之间的空隙非常非常的大…日程里面的那两个色块
就是紧挨着的。所以…应该让它们紧挨着。」
④「现在怎么部署的时间这么长?…你修改了哪些页面,你就只测试哪些页面就可以了。」
⑤「日程模块的新建日程…"加上"和"取消"两个按钮在右上角,离得特别远,根本看不到…
每个输入框前面的标题都太小了…都应该加粗、变大。」
⑥「1.新建任务的截止时间,也和观影一致。2.新建任务时竟然没有添加附件…
3.新建任务也弹出一个框,新建完之后刷新看板页面。4.观影新增也弹框输入所有信息。」
② 日期控件:**真根因是"这个控件给不了"**
`<input type="date">` 的日历弹层由**浏览器自己画**,我们碰不到 DOM、也控制不了流程;
Chrome 在"点顶部 `2026年 10月` 切到年月视图"之后,要靠用户**再点一次日期或按 Esc**
才回得到日期视图,各家还不一致 ⇒ 用户遇到的"选完年月就卡住、选不了日"。
修:`_datetime.php` 重写成 **年 / 月 / 日 三个原生 <select> + 时 / 分**,
选完即生效、无需"确定";秒默认去掉(用户"只记录到时和分")。
⚠️ 提交契约**一字未改**:`*_date` 仍是真字段(改 `type=hidden`)+ `*_hour/_minute`。
⚠️ `required` 落在隐藏控件上会让 Chrome 报 "not focusable"(点保存什么都不发生)
⇒ 改用 `data-dt-required` 声明、JS 自查(12 个调用点全部受影响)。
起止两端:`.dt-range.dt-cols` + `flex-wrap` → **宽屏一行、窄屏自动换行**
(不用 JS 量宽度:字体/动画/缩放时量不准,flex-wrap 是浏览器布局的一部分)。
🔴 顺带挖出真 bug:旅程「修正」表单的 `.dt-range` 只有 **22px 宽**
—— 它长在 `.node-ops`(一排小按钮的容器)里,展开的内容只有"修正"两个字那么宽。
修:`.rev-panel{position:absolute}` 纯 CSS 浮出来(不动 DOM,避免牵动节点列表排版)。
③ 首页冲突色块"空隙特别大":与日历页逐字对比出来的
日历页 `left: calc(2px + L%)` / `width: calc(W% - 4px)` → 只留 4px、紧挨;
首页却写死 `calc(46px + L%)` / `calc(W% - 50px)` —— 每块左右各扣 23/25px,
两列时那 46px **没法均分**、全挤进了中间的缝(实测 ≈50px)。
修:`left: calc(46px + L% + 2px)` / `width: calc(W% - 4px)` → 缝恒为 4px。
⑤ 日程弹框"按钮看不到":**两个**根因叠加
① `calendar/index.php` 里**多写了一个 `</div>`** —— `.cal-modal-panel` 被提前闭合,
底栏 `<div class="cal-modal-foot">` 成了 `.cal-modal` 的直接子元素、被挤到面板外;
② `.cal-modal-panel` 是 flex 列,但**它下面的 `<form>` 不是** →
`.cal-modal-body{flex:1 1 auto}` 的上下文只到 form 为止,body 压不下去也不滚。
另有 `.cal-modal-panel` 的 max-height 没算上 `.cal-modal` 自己的 padding(差 4px)。
三处都修;标签只对**弹框里**的 `.field > label` 加粗放大(14px/600/深色)。
⑥ 任务新建:内联卡片 → 弹框;**新增附件**(`attach[]`,服务端零改动能收);
`back=tasks` → 新建完回**看板**刷新。观影新增:列表页内嵌同一份 form.php 弹框。
① 详情页删掉那句提示块。④ 流水线改**增量**:`run_suite_if <标签>` + 从 git 改动
推模块;**一碰共用件就退回全量**(app.js/app.css/layout/index.php/_datetime.php…),
认不出来也全量(保守:少跑一段发坏东西的代价远大于多跑十分钟)。
🔴🔴 本轮最值得记的教训:**DOM 嵌套错误比语法错误难查得多**
在 movies/form.php 与 calendar/index.php 各写坏一次嵌套(多/少 `</div>`、多 `</form>`),
两处**都通过 php -l、页面也打得开**,症状却是"按钮跑到面板外"与
"编辑保存静默失效"(保存键落到 form 外面)。⇒ 模板里 `<?php if ?>` 与 `</div>`
必须**按分支配对**,改完用真机渲染 dump 一次 div 深度;光靠 php -l 发现不了。
验证(全部真机 + 反向验证):
· _fb_b34_dtrange.py **61/0**(四个页面:年/月/日/时/分 都可见可点 +
选完三个下拉**立刻**写回 *_date + 改月重建"日"列 + 起止默认同一行 + 无 JS 报错);
· _fb_b34_dtrange_falsify **7/0**(三处注入各自必须变红);
· _fb_b37_ui.py **27/0**(详情页无提示块 / 首页冲突缝 ≤8px / 弹框按钮在面板内且在
视口内 / 标签加粗 / 任务弹框+附件+回看板 / 观影列表页弹框);
· _fb_b37_falsify **5/0**;
· _fb_b35_movie_media.py **16/0**、_fb_geom_interact **109/0**、e2e **1503/0**、
b30_ui2 16/0(含首页冲突分列)、_b30_import 26/0、b18/b21/b23/…全绿、
DAV 三套全绿、部署静态断言空跑通过;
· 线上 app.js / app.css / movies/form.php / calendar/index.php 的 SHA256 与本地一致。
· 顺带修掉 _b30_import 的一个**时序竞争**:它用外部上游(联想词服务)的二次探测
当对照,浏览器取词在前、探测在后,上游抖一下会误报成"回归"(已改成两次确认)。
237 lines
12 KiB
PHP
237 lines
12 KiB
PHP
<?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>
|
||
<?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">
|
||
<?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>
|
||
<span class="tip">可以选多个(到点各响一次);一个都不选就是不提醒。</span>
|
||
</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 一下即可。
|
||
⚠️ 顺带删掉「+ 添加结束时间 / 去掉结束时间」那套开关:
|
||
它切换显隐会让范围控件反复重挂;范围控件自己就能表达"没有结束"。 */ ?>
|
||
<div class="dt-range">
|
||
<?php /* ⚠️ 标签只有**一个**,挂在范围容器上(不是各挂各的):
|
||
升级成一个范围控件之后,"开始"与"结束"就是同一个控件的两个端点,
|
||
两个标签会变成"看着像两个字段、其实是一个"的错觉。 */ ?>
|
||
<span class="dt-range-label">开始 / 结束 *</span>
|
||
<div class="field ev-dt" data-dt>
|
||
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
|
||
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true;
|
||
$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';
|
||
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false;
|
||
$dtRange = 'end';
|
||
require __DIR__ . '/../_datetime.php'; ?>
|
||
</div>
|
||
</div><?php /* /dt-range */ ?>
|
||
<span class="field-hint" style="display:block;margin:-4px 0 10px">
|
||
结束留空就表示没有明确的结束(只占开始那一刻/那一天)。
|
||
</span>
|
||
|
||
<div class="field">
|
||
<label>
|
||
<input type="checkbox" id="ev-allday" name="all_day" value="1"
|
||
<?= $evIsAllDay ? 'checked' : '' ?>>
|
||
全天
|
||
</label>
|
||
<span class="field-hint">勾上就不管具体几点,只占这一天</span>
|
||
</div>
|
||
|
||
<?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>
|
||
|
||
<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">
|
||
<label>附件(可多选)</label>
|
||
<?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。
|
||
现在起止两端**始终可见**,并且由 Semi 的范围控件统一接管(一次选完两端),
|
||
所以那段"同步两端"的逻辑整个删掉 —— 留着不但没用,还会和范围控件抢着改那两个
|
||
原生字段(两处都写 → 值互相覆盖)。范围控件选完会自己把两端分别写回原生字段
|
||
(见 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); ?>
|