Files
FamilyBook/templates/_datetime.php
T
laoyang d83c94e127 v37 第 37 批:日期控件换成「年/月/日 + 时/分」五个原生下拉 + 五处界面重做
用户原话(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 的一个**时序竞争**:它用外部上游(联想词服务)的二次探测
    当对照,浏览器取词在前、探测在后,上游抖一下会误报成"回归"(已改成两次确认)。
2026-10-10 00:26:42 +08:00

154 lines
9.5 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)
*/
$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 ?? '');
/* ⚠️⚠️ 临时变量**一律带 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];
?>
<div class="dt-input<?= $dtRange !== '' ? ' dt-part' : '' ?><?= $dtUseParts ? ' dt-parts' : '' ?><?= $dtRange === 'start' ? ' is-pending' : '' ?>"
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,
$dtTs, $dtTsTmp, $dtDate, $dtY, $dtMo, $dtD, $dtDays,
$dtHour, $dtMin, $dtSec,
$dtH, $dtM, $dtS, $hv, $mv, $sv, $dtYy, $dtMm, $dtDd
); ?>