Files
FamilyBook/templates/_datetime.php
T
laoyang 8661ab6475 v38 第 38 批:观影试点 Ant Design 日期控件 + 弹框内联排版(对齐)+ 去提示 + 默认带自己
用户原话(四条):
  ①「我终于找到了一个我比较认可的日期选择插件
     https://ant-design.antgroup.com/components/date-picker-cn 现在观影这个模块进行
     测试吧,测试那个有开始、结束时间的。等这个模块测试完没问题了,再更新到其他的模块。」
  ②「记录一场电影,这个弹框因为标题和下面的输入框都是两行的,所以导致弹框非常长。
     能不能排排版,让前面的标题后面加个冒号,后面的输入框直接跟在它后面,这样就变成一行。
     包括开始、结束时间、影院、同行的人、简介等等,能把它排成一行的都排成一行,
     但是一定要注意对齐的问题,如果不对齐的话可能不好看。」
  ③「里面的一些提示,比如片名可以从豆瓣自动补充,还有刚新建的影院会自动选中,
     观影照片可以放这里且可多选,这些都把它去掉吧,节省一下上下的空间。」
  ④「另外一个同行的人,这里每个人添加的时候,应该默认带上自己,也就是自己不用选择
     就应该已经选上了。」

① Ant Design RangePicker —— **只在观影试点**(用户明确"试完再推广")
  · 资源 `assets/vendor/antd/{antd.min.js, dayjs.min.js, reset.css}`(unpkg 下载、随包上线);
    layout.php 加 `fb-antd` 清单;`_datetime.php` 加 `$dtPlugin = 'antd'` 开关 +
    `data-dt-antd="1"` 标记;app.js 加 `bootAntd()` / `mountAntd()`(按需加载)。
  · 🔴 UMD 的硬依赖:antd 从**全局**取 React / ReactDOM / dayjs。缺 React 的报错是
    `Cannot read properties of undefined (reading 'createContext')` —— 表现是"控件完全
    不出现",不是样式问题。项目本来就有一套 React 18.3.1(Semi 用的),直接复用同一份。
  · 🔴 实测出的关键行为(独立探针 `_antd_probe`,15/0):面板里有 **42 个日期格 +
    2 个内联时间列(84 项)+ 「确定」按钮** —— 正是之前 Semi 缺的那两样;
    且 **`onChange` 只在两端都齐了才触发**,单端改动走 `onCalendarChange` ⇒
    回写必须两个都挂,否则"只填了开始时间就提交"会丢值。
  · ⚠️ 原生那对下拉**全部保留**(值载体 + 拉不动时的兜底),挂上 antd 后用 clip 视觉收起
    (不用 display:none —— 保持可聚焦)。
  · 部署断言钉住试点边界:日程/任务**不该**出现 `$dtPlugin = 'antd'`。

② 弹框排版:标签内联 + 控件左缘对齐
  · `.mv-row` = 标签(固定 62px、右对齐、**冒号由 CSS ::after 给**):控件(占满剩余)。
  · 并排对:简介+备注、海报+观影照片 → `.mv-pair`(grid 1fr 1fr 保证两列等宽);窄屏回单列。
  · 🔴 真机量出**两个真对齐缺陷**(不是猜的):
    1) 时间行的控件左缘比别的行**偏 8px** —— `.dt-cols` 是 `gap: 10px 18px`(列距 18),
       而普通 `.mv-row` 只有 `gap:10px` ⇒ 改 `.mv-row.dt-range.dt-cols { column-gap: 10px }`;
    2) 海报/观影照片两行歪了 **130px** —— `_stage_drop.php` 在最前面插的 `.sr-only`
       `<input>`(绝对定位 1px)**占掉了一整份 flex:1 1 auto**,把上传框推出去
       ⇒ 这两块改纵向 `.mv-media`(标签一行、内容从 72px 起,与主列对齐)。
    ⇒ 现在**所有字段控件左缘都是 444**;A/B 实测**弹框 1036px → 817px(矮 219px)**。

③ 删掉三句冗余提示(豆瓣补充 / 新建影院自动选中 / 观影照片),简介 rows 6→3。

④ 同行的人默认带上自己:新增时预置 `[我]`;编辑以库值为准;`old()` 存在时尊重用户上次勾选。

🔴 顺带修的真 bug:**antd 面板与弹框抢 Esc** —— 两个处理器都挂 document 上,
  按一次 Esc 把日期面板和整个弹框一起关掉。已改成"antd 面板开着时弹框不吃 Esc"。

验证(全部真机 + 反向验证):
  · _fb_b38_movie_form.py **33/0**(antd 挂上/面板有时间列与确定/值写回隐藏 *_date/
    标签同行与左缘对齐/三句提示消失/默认带自己/试点边界);
  · _fb_b38_narrow.py **6/0**(宽屏并排、窄屏回单列、无横向溢出);
  · _fb_b38_falsify.py **8/0**(排版退回上下两行、antd 只挂 onChange、不预置自己 → 各自必须红);
  · e2e **1503/0**、日期控件真机 **53/0** + 反向 **7/0**、v37 两套 **27/0 + 5/0**、
    观影照片 **16/0** + **8/0**、b30 三件套、b18 选人 **10/0**、几何交互 **109/0**、
    DAV 三套、部署静态断言空跑 —— 全绿;
  · 线上 app.js / app.css / antd.min.js 的 SHA256 与本地一致。

⚠️ 顺带修了探针/夹具的三处**测试侧**问题(都不是页面 bug):
  · e2e 里 6 条 jsdom 用例(测选人弹框"暂存区机制")前提是"一个都没选",
    默认带自己后全假红 ⇒ 在**测试自己的 HTML 副本**上清掉 `data-pp-selected`;
  · `_fb_b18_picker` 真机探针同理,改成**点 chip 上的 ×**(组件把已选存在闭包里,
    改属性没用 —— 试过两种写法都是白试);
  · `_fb_b34_dtrange` 的"标签文案"断言写死了「开始 / 结束」,
    而 v38 把观影的标签简化成了「时间」⇒ 改成"有非空标签即可";
    另补一条 antd 分支(挂 antd 后原生被裁成 1px,`1px > 0` 会让旧判据**假通过**)。
  · 顺带:夹具名册只有 1 人时会误报"无法验证多选",已把实际候选数打出来。
2026-10-10 11:01:22 +08:00

167 lines
10 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
/**
* 「日期 + 24 小时制时:分」组合输入控件。
*
* ⚠️ 为什么不用 <input type="datetime-local">:
* 它的显示格式由**浏览器 UI 语言 / 系统时间格式**决定,<html lang> 与元素 lang
* 都压不过它。中文环境实测会渲染成「下午 03:30」,用户很容易把上下午填反。
*
* ════════════════════════════════════════════════════════════════
* 🔴🔴 v37:**日期部分默认改成「年 / 月 / 日」三个原生下拉**(不再用 <input type=date>)
* ════════════════════════════════════════════════════════════════
* 用户原话(2026-10-09 第 4 次反馈,两次都提到同一个痛点):
* 「编辑页面的时间,我还是不是特别满意…选日期的这个控件非常非常不好用。
* 就是如果我把年和月那个地方拉下来,我选完年和月之后,它并不会回到让我选日的
* 那个界面,它还是不动的,所以这时候就没有办法选日。这个地方要么换一个控件,
* 要么在那个地方加一个确定按钮…」
*
* 为什么会这样(不是我们调错了参数):`<input type="date">` 的日历弹层是
* **浏览器自己画的**,我们既拿不到它的 DOM,也控制不了它的交互流程。
* Chrome 的弹层在"点顶部 `2026年 10月` 切到年月视图"之后,**切回日期视图要靠
* 用户再点一次日期或按 Esc** —— 各家浏览器行为还不一致,用户遇到的就是"卡住"。
* ⇒ 换成**我们能完全控制的原生 `<select>`**:三个下拉,选哪个改哪个、
* **选完即生效**、不需要任何"确定"按钮。
*
* ⚠️ 提交契约**一个字都没变**:仍然产出
* `*_date`(隐藏框,'YYYY-MM-DD')/ `*_hour` / `*_minute`
* 服务端 read_dt_input()/read_due_input() 原样收,既有 e2e 与部署断言照过。
* ⚠️ 「秒」默认**不再产出**($dtSeconds 默认 false)——
* 用户 2026-10-09 第 2 条:「只记录到时和分就行了」。
* 需要秒的老调用方显式传 $dtSeconds = true。
*
* 用法(include 前 set 好这些变量;模板里只读它们):
* $dtName 字段基名,如 'starts_at' → 产出 *_date / _hour / _minute
* $dtValue 可空;'Y-m-d H:i:s' / 'Y-m-d H:i' / 'Y-m-d'
* $dtRequired 日期是否必填(默认 false)
* $dtId 可选;给外层一个 id(供 <label for> 与测试定位)
* $dtSeconds 是否要"秒"这一档(**默认 false**,见上面 v37 说明)
* $dtRange 'start' | 'end' —— 这对控件属于同一个起止范围(见下)
* $dtDateType 'select'(默认,年/月/日下拉)| 'native'(旧的原生 date 框)
* $dtYearFrom / $dtYearTo 年份下拉的范围(默认:今年 -120 ~ 今年 +10;生日建议 1900)
* $dtPlugin 🔴 v38 插件开关:'antd' = 由 app.js 把这两个端点升级成
* **Ant Design 的 RangePicker**(用户 2026-10-10 点名要的插件)。
* ⚠️ 只用在**起止成对**的两个端点上(见下),且**只在观影模块试点**;
* 其余页面留空即可(继续用自建下拉,行为一字不变)。
*/
$dtName = (string)($dtName ?? 'time');
$dtValue = $dtValue ?? null;
$dtRequired = (bool)($dtRequired ?? false);
$dtId = (string)($dtId ?? '');
/* 🔴 v37:秒默认关掉了(用户「只记录到时和分就行了」)。
⚠️ 这是**行为变更**:以前不传 = 有秒。所有调用方都已在本批逐一看过,
确实需要秒的(工作经历那种)会显式传 true。 */
$dtSeconds = (bool)($dtSeconds ?? false);
$dtDateType = (string)($dtDateType ?? 'select');
/* 年/月/日下拉 ⇄ 隐藏的 `*_date` 之间的同步由 app.js 负责(见 upgrade 里的 bindParts)。
`native` 模式则完全走旧路(原生 date 框),保留给将来可能的回退。 */
$dtUseParts = ($dtDateType !== 'native');
$dtYearFrom = (int)($dtYearFrom ?? ((int)date('Y') - 120));
$dtYearTo = (int)($dtYearTo ?? ((int)date('Y') + 10));
/* 起止成对的场景:两端属于同一个范围(调用方用 <div class="dt-range"> 包住两者)。
留空 = 普通单点控件。 */
$dtRange = (string)($dtRange ?? '');
/* 🔴 v38:插件开关(见文件头)。只在**起止成对**的场景下有意义 ——
RangePicker 天生就是"一个控件管两端",单点控件挂它反而多一个"确定"。
⚠️ 即便声明了插件,**原生控件一个都不删**:JS 没跑 / 资源拉不动时,
用户看到的仍是那套自建下拉(渐进增强的老规矩)。 */
$dtPlugin = (string)($dtPlugin ?? '');
$dtUseAntd = ($dtPlugin === 'antd' && $dtRange !== '');
/* ⚠️⚠️ 临时变量**一律带 dt 前缀**,一个都不能漏:这里原来写成
`$t = strtotime(...)`,把宿主模板里表示「这条任务 / 这条记录」的 `$t`
覆盖成了 int —— 于是 include 之后凡是读 `$t['id']`、`$t['status']` 的地方
全部拿到 null,子任务列表空、评论不渲染、@高亮消失,而且**不报任何错**。
(tasks/show.php 上就是这么被 e2e 抓到的,2026-10-02。) */
$dtTs = (!empty($dtValue) && ($dtTsTmp = strtotime(trim((string)$dtValue)))) ? $dtTsTmp : false;
$dtDate = $dtTs ? date('Y-m-d', $dtTs) : '';
$dtY = $dtTs ? (int)date('Y', $dtTs) : 0;
$dtMo = $dtTs ? (int)date('n', $dtTs) : 0;
$dtD = $dtTs ? (int)date('j', $dtTs) : 0;
$dtHour = $dtTs ? (int)date('G', $dtTs) : 0; // G = 0..23,天然 24 小时制
$dtMin = $dtTs ? (int)date('i', $dtTs) : 0;
$dtSec = $dtTs ? (int)date('s', $dtTs) : 0;
?>
<?php /* v24 第 9 批 #9:Semi DatePicker 曾用于这里;v37 起日期改自建下拉、
时间改原生 select,**Semi 只剩「工作经历的年月选择」还在用**(那是另一处)。
.dt-semi 这个占位 span 保留 —— 老代码路径与 CSS 还在引用它,删了会牵连。 */
$dtCfg = ['name' => $dtName, 'seconds' => $dtSeconds, 'required' => $dtRequired,
'id' => $dtId, 'range' => $dtRange, 'parts' => $dtUseParts || null,
'plugin' => $dtUseAntd ? 'antd' : null];
?>
<div class="dt-input<?= $dtRange !== '' ? ' dt-part' : '' ?><?= $dtUseParts ? ' dt-parts' : '' ?><?= $dtRange === 'start' ? ' is-pending' : '' ?>"
<?= $dtUseAntd ? 'data-dt-antd="1"' : '' ?>
data-dt="<?= e(json_encode($dtCfg, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>"
<?= $dtRequired ? 'data-dt-required="1"' : '' ?>>
<span class="dt-semi" aria-hidden="true"></span>
<span class="dt-native">
<?php if ($dtUseParts): ?>
<?php /* ---- 日期:年 / 月 / 日 三个原生下拉(v37) ----
⚠️ `*_date` 仍然是**真的那个字段**,只是变成隐藏框:
三个下拉只负责把它算出来(app.js 的 bindParts)。
这样"表单契约不变"是**结构上**保证的,不是靠约定。 */ ?>
<span class="dt-date-sel">
<input type="hidden" class="dt-date" name="<?= e($dtName) ?>_date"
<?= $dtId !== '' ? 'id="' . e($dtId) . '"' : '' ?>
value="<?= e($dtDate) ?>">
<select class="dt-year" aria-label="年">
<option value="">年</option>
<?php for ($dtYy = $dtYearTo; $dtYy >= $dtYearFrom; $dtYy--): ?>
<option value="<?= $dtYy ?>"<?= $dtYy === $dtY ? ' selected' : '' ?>><?= $dtYy ?></option>
<?php endfor; ?>
</select>
<select class="dt-month" aria-label="月">
<option value="">月</option>
<?php for ($dtMm = 1; $dtMm <= 12; $dtMm++): ?>
<option value="<?= $dtMm ?>"<?= $dtMm === $dtMo ? ' selected' : '' ?>><?= $dtMm ?>月</option>
<?php endfor; ?>
</select>
<select class="dt-day" aria-label="日">
<option value="">日</option>
<?php /* 初始按已选的年月给天数;用户改年/月时 app.js 会重建这一列 */
$dtDays = ($dtY && $dtMo) ? (int)date('t', mktime(0, 0, 0, $dtMo, 1, $dtY)) : 31; ?>
<?php for ($dtDd = 1; $dtDd <= $dtDays; $dtDd++): ?>
<option value="<?= $dtDd ?>"<?= $dtDd === $dtD ? ' selected' : '' ?>><?= $dtDd ?>日</option>
<?php endfor; ?>
</select>
</span>
<?php else: ?>
<input type="date" class="dt-date" name="<?= e($dtName) ?>_date"
<?= $dtId !== '' ? 'id="' . e($dtId) . '"' : '' ?>
value="<?= e($dtDate) ?>"<?= $dtRequired ? ' required' : '' ?>
aria-label="日期">
<?php endif; ?>
<span class="dt-time">
<select class="dt-hour" name="<?= e($dtName) ?>_hour" aria-label="时(24 小时制)">
<?php for ($dtH = 0; $dtH < 24; $dtH++): $hv = sprintf('%02d', $dtH); ?>
<option value="<?= $hv ?>"<?= $dtH === $dtHour ? ' selected' : '' ?>><?= $hv ?></option>
<?php endfor; ?>
</select>
<span class="dt-colon" aria-hidden="true">:</span>
<select class="dt-minute" name="<?= e($dtName) ?>_minute" aria-label="分">
<?php /* ⚠️ 循环变量一律带 dt 前缀:这里曾被写成 $m,把宿主模板里
表示「观影记录」的 $m 覆盖成了 int —— 编辑页的简介/备注回填
被静默清空、取消链接变 id=0(2026-09-30 e2e 抓到)。 */ ?>
<?php for ($dtM = 0; $dtM < 60; $dtM++): $mv = sprintf('%02d', $dtM); ?>
<option value="<?= $mv ?>"<?= $dtM === $dtMin ? ' selected' : '' ?>><?= $mv ?></option>
<?php endfor; ?>
</select>
<?php if ($dtSeconds): ?>
<span class="dt-colon" aria-hidden="true">:</span>
<select class="dt-second" name="<?= e($dtName) ?>_second" aria-label="秒">
<?php for ($dtS = 0; $dtS < 60; $dtS++): $sv = sprintf('%02d', $dtS); ?>
<option value="<?= $sv ?>"<?= $dtS === $dtSec ? ' selected' : '' ?>><?= $sv ?></option>
<?php endfor; ?>
</select>
<?php endif; ?>
</span>
</span>
</div>
<?php /* 用完即清,避免残留变量影响下一次 include */ unset(
$dtName, $dtValue, $dtRequired, $dtId, $dtSeconds, $dtCfg, $dtRange,
$dtDateType, $dtUseParts, $dtYearFrom, $dtYearTo, $dtPlugin, $dtUseAntd,
$dtTs, $dtTsTmp, $dtDate, $dtY, $dtMo, $dtD, $dtDays,
$dtHour, $dtMin, $dtSec,
$dtH, $dtM, $dtS, $hv, $mv, $sv, $dtYy, $dtMm, $dtDd
); ?>