Files
FamilyBook/templates/_datetime.php
T
laoyang daf5ca904e v94 必填项改成「标签红色加粗」,去掉红色星号 (#3)
需求(用户 2026-10-25):必填项原来在标题后面加一个红色星号,排版经常不齐。
改成「标题本身红色 + 加粗」——用户习惯后,看到红色加粗的标题就知道是必填。

CSS(assets/css/app.css):
- :root 新增 --req:#B03A2E、--req-w:700。
  · 不用 --red(#C0533F):那是动作红(删除按钮底色),当文字色放在暖纸底
    #F7F3EA 上对比度只有 ≈3.4:1,13-14px 标签偏浅;#B03A2E 约 4.6:1。
  · --req-w 必须 700 不能 650/600:本站标签基线字重本来就是 600
    (.form-grid > .field > label / .card .field > label / .mv-row > .mv-lb /
    .dt-range-label 四处都是 600),650 会被取整回 600 = 完全没加粗。
- 文件末尾新增「必填项统一标记」段,覆盖四种标签形态:
  label.is-req / .mv-lb.is-req / .dt-inline-lab.is-req / .dt-range-label.is-req,
  用 !important 打穿各宿主选择器的特异性(颜色与字重是语义标记,不该被面板样式盖掉)。
- 冒号跟着变红:.mv-lb::after / .dt-inline-lab::after 原本是灰色 400,
  只改标签本体会剩「红字 + 灰冒号」的半截效果。
- 包裹层兜底(.field.is-req > label 等),只作用直接子级 ——
  不写 .field.is-req label,那会把「提醒」那排 checkbox 的小标签一起染红。
- 深色主题给备用色 #F08A78。
- 保留 .mv-row > .mv-lb > i(万一有没清干净的星号不至于露灰星花,且静态断言会报出来)。

模板(16 个文件 / 30 处):
- _event_fields.php / _datetime.php 的标签闭包第三参数 $star → $req,
  $dtLabelStar → $dtLabelReq,删掉死引用 $evLabelStar。
- 其余 14 个模板把 `<i>*</i>` 换成给标签加 is-req 类。
- 用显式 .is-req 而不靠 required 属性反推:CSS 无父选择器,:has() 旧 WebView
  不可用,且影院名称/板块名本来就靠 JS 校验没有 required。

验证:
- 静态 _fb_v94_static.py 22/0,反向 9/9(含「--req-w 必须严格大于从 CSS 解析出的
  标签基线字重」,不写死 700)。
- 真浏览器 _fb_v94_req.py 63/0,反向 5/5(加粗/必须红/!important 必要性/
  星号必须消失/每形态单独覆盖,五类注入各自精确命中预期断言)。
- 全量流水线(含 60+ 套件)通过并部署。
- 线上真实页面 _fb_v94_live.py 31/0:观影「片名」、旅程「旅程名」、
  成员「姓名」、导航「网址/所属板块」实测 color=rgb(176,58,46) weight=700,
  同页非必填 color=rgb(44,35,29) weight=600,冒号同色,无星号。
2026-10-26 06:45:38 +08:00

217 lines
15 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)
* $dtLabel 🔴 v44:把标签**内联到控件同一行**(写成该字符串 + 「:」)。
* · 传空串 = 不要标签(起止范围里的第二个端点就该留空);
* · 不传(null)= 沿用旧行为(标签由宿主模板自己放,通常在上方一行)。
* 为什么要它:用户 2026-10-12 第 2 条要求「日期范围:」与最新控件
* **在同一行**。宿主的 `.mv-grid` 行结构把标签与控件分在两行,
* 与其在 CSS 里跟行结构较劲,不如让控件**自带**标签 —— 位置确定。
* $dtLabelReq 该标签是否表示**必填**(配 $dtLabel 用)。
* 🔴 v94 起不再画星号,而是给标签加 `.is-req`(红字加粗)。
* 参数名 v94 起从 `$dtLabelReq` 更名为 `$dtLabelReq`:它原来叫
* "加星",现在只表示"这是必填",旧名会让人以为还在画星号。
* $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 ?? '');
/* 🔴 v44:内联标签(见文件头)。`null`(不传)与 `''`(显式不要)语义不同,
所以这里不能用 `??` —— 得先看有没有这个键。 */
$dtHasLabel = array_key_exists('dtLabel', get_defined_vars());
$dtLabel = $dtHasLabel ? (string)$dtLabel : '';
$dtLabelReq = (bool)($dtLabelReq ?? false);
/* 🔴 v38:插件开关(见文件头)。只在**起止成对**的场景下有意义 ——
RangePicker 天生就是"一个控件管两端",单点控件挂它反而多一个"确定"。
⚠️ 即便声明了插件,**原生控件一个都不删**:JS 没跑 / 资源拉不动时,
用户看到的仍是那套自建下拉(渐进增强的老规矩)。 */
$dtPlugin = (string)($dtPlugin ?? '');
$dtUseAntd = ($dtPlugin === 'antd' && $dtRange !== '');
/* 🔴 v44:单个**带时刻**的 AntD 控件(任务的「截止时间」)。
`$dtPlugin = 'antd-single'` ⇒ app.js 把这一档升级成 **AntD 的 DatePicker(showTime)**
(不是 RangePicker —— 截止时间只有一个时刻,用范围控件会多一个空端点)。
用途(用户 2026-10-12 第 5 条):「截止时间也用我们最新的日历控件,
记得把现在这个控件删掉。」 */
$dtUseAntdSingle = ($dtPlugin === 'antd-single' && $dtRange === '');
/* 🔴 v44:容器类与 id —— 让"日程"这对能用与"观影"完全一致的结构
(`.dt-range > .dt-input` 直接子),从而命中那条宽泛的收起规则(见 CSS)。
不传 `$dtClass` 时维持旧行为(`.dt-input` 直接挂在 `.dt-range` / `.field` 下)。 */
$dtClass = (string)($dtClass ?? '');
$dtFieldId = (string)($dtFieldId ?? '');
/* ⚠️⚠️ 临时变量**一律带 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' : ($dtUseAntdSingle ? 'antd-single' : null)];
?>
<?php /* ⚠️ v44:内联标签是**兄弟节点**、排在 `.dt-input` **之前** ——
绝不放进 `.dt-input` 里面。
血泪(真机几何量出来的):antd 挂上后那条"收起原生端点"的规则会把
整个 `.dt-input` 绝对定位 + `clip` 掉(因为那里装着要藏起来的原生下拉);
标签若在它内部 ⇒ **跟着一起被裁没**,用户只看到一个没有标签的光控件
(实测:标签盒还在 x=316 但 `elementFromPoint` 命中的是 `.ant-picker`)。
放成兄弟之后,收起规则只影响端点本身,标签安然无恙、
并且与 `_datetime.php` 外面的 `.mv-row` / `.dt-range` flex 容器天然同行。 */ ?>
<?php if ($dtHasLabel && $dtLabel !== ''): ?>
<?php /* ⚠️ 标签上**不带** `$dtClass`:那个类名里含 `field`/`ev-dt`,
而 `.dt-range.dt-cols > .field { flex: 1 1 250px }`(0,3,0)会盖过
`.dt-range > .dt-inline-lab { flex: 0 0 62px }`(0,2,0)——
实测标签被撑成 261px,把最新控件挤到右边去("不在同一行左侧"的观感)。 */ ?>
<?php /* 🔴 v94:必填不再画星号,改成标签自身红色加粗(`.is-req`)。
`$dtLabelReq` 参数名保持不变(调用点很多),语义已是"必填"。 */ ?>
<span class="dt-inline-lab mv-lb<?= $dtLabelReq ? ' is-req' : '' ?>"><?= e($dtLabel) ?></span>
<?php endif; ?>
<?php /* ⚠️ v44:不再套外层 `.field` —— 那会把 `.dt-input` 变成**孙子**,
于是 `> .dt-input` 那条宽泛的收起规则失效(日程"旧控件还在"就是这么来的)。
`$dtClass` 是**额外类名**,`.dt-input` 永远是本体;`$dtFieldId` 直接落在它身上。 */ ?>
<div class="dt-input<?= $dtClass !== '' ? ' ' . e($dtClass) : '' ?><?= $dtRange !== '' ? ' dt-part' : '' ?><?= $dtUseParts ? ' dt-parts' : '' ?><?= $dtRange === 'start' ? ' is-pending' : '' ?>"
<?= $dtFieldId !== '' ? 'id="' . e($dtFieldId) . '"' : '' ?>
<?= $dtUseAntd ? 'data-dt-antd="1"' : '' ?>
<?= $dtUseAntdSingle ? 'data-dt-antd-single="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, $dtUseAntdSingle,
$dtHasLabel, $dtLabel, $dtLabelReq, $dtClass, $dtFieldId,
$dtTs, $dtTsTmp, $dtDate, $dtY, $dtMo, $dtD, $dtDays,
$dtHour, $dtMin, $dtSec,
$dtH, $dtM, $dtS, $hv, $mv, $sv, $dtYy, $dtMm, $dtDd
); ?>