Files
FamilyBook/templates/_datetime.php
T
laoyang b2ca4f336e v24 第 10 批:名册真性能(根因是每卡内联表单)等九条
- #6 名册线上真实数据(1184 人)卡顿:根因是**每张卡片内联一整份分组弹层**——
  全页 2369 个 form / 27234 个 input / 22496 个 radio / 98100 DOM 节点,
  密码管理器(Bitwarden 等)逐个扫描 form 把主线程占死(用户报的就是这个冲突)。
  改为卡片只带 data 属性 + 全页**唯一一份**共享浮层 #rosterMenus。
  实测:form 2369→3、input 27234→25、DOM 98100→17659、解压 7694→2306 KB、
  长任务 1/52ms → 0。新增哨兵 _fb_formbloat_check.py(含反向验证)。
- #5 日视图翻页失效:day_columns() 无条件吸到周一,天视图只有 1 列 → 永远显示那周周一。
  改为仅 span>1 时吸附(周视图行为不变)。
- #2 财务「欠款」类别(负债);并修一个真 bug:负债填负数会让净资产**反向变大**。
  写入口 normalizeAmount 归一化 + summary/timeline 侧 abs 防御 + Schema v17 纠正老数据。
- #3 账目行「盘点」「修改」两个并列按钮(各自就地弹面板);图移到账目上方,
  总金额图跨整行 + 每个账目一条线 + 类别筛选条;charts() 改为覆盖全部账目。
- #4 日程新增「农历」「节假日」两本不可删除只读日历(app/Holiday.php):
  两级自动更新 —— 内置规则(公历固定 + 清明节气 + 农历节日)保证断网也有;
  官方 JSON 镜像自动抓取补齐调休。抓取**不在渲染路径联网**(响应后 fastcgi_finish_request
  + 频率闸门),缓存落 storage/cache。仅派生本不可写日程。
- #6 日程页设置按钮前加「跳到某一天」(选中即跳)。
- #7 起止范围改用一个 Semi dateTimeRange 控件(观影/旅程/节点/日程共 4 处);
  新增 window.fbSetDT / fbUpgradeRange;修范围重挂(React 非受控组件须 unmount 再 createRoot)
  与范围里 required 未摘除导致表单无法提交两个真实问题。

验证:e2e 786/0、batch10 44/0、batch9 38/0、batch_e 91/0、upgrade 126/0、picker 28/0、
semi_integrate 27/0、formbloat 5/0(+反向 3 红)、mobile 688/0、interact 97/0、drag 5/0、geom 5/5;
线上自检 19/19 + 线上真实账号巡检 28/0(含 Firefox 实测)。
2026-10-04 10:46:50 +08:00

98 lines
6.0 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 小时制时:分:秒」组合输入控件(从 TripLog 的 _datetime.php 移植)。
*
* ⚠️ 为什么不用 <input type="datetime-local">:
* 它的显示格式由**浏览器 UI 语言 / 系统时间格式**决定,<html lang> 与元素 lang
* 都压不过它。中文环境实测会渲染成「下午 03:30」,用户很容易把上下午填反;
* 而且它只有时:分,**没有秒**。拆成「原生日期选择器 + 三个下拉」后,
* 24 小时制与"精确到秒"都由控件本身保证,浏览器改不掉。
*
* 日期部分仍是原生 `<input type="date">`(app.js 里的 showPicker 脚本对它同样生效,
* 点输入框任意位置都能弹出日历 —— 这正是"整个输入框点击都可以弹出"的落点)。
*
* 用法(include 前 set 好这些变量;模板里只读它们):
* $dtName 字段基名,如 'birthday_solar' → 产出 *_date / _hour / _minute / _second
* $dtValue 可空;'Y-m-d H:i:s' / 'Y-m-d H:i' / 'Y-m-d'。服务端用 read_dt_input($dtName) 取值
* $dtRequired 日期是否必填(默认 false)
* $dtId 可选;日期控件的 id(供 <label for>)
* $dtSeconds 是否要"秒"这一档(默认 true;生日要精确到时分秒)
*/
$dtName = (string)($dtName ?? 'time');
$dtValue = $dtValue ?? null;
$dtRequired = (bool)($dtRequired ?? false);
$dtId = (string)($dtId ?? '');
$dtSeconds = (bool)($dtSeconds ?? true);
/* 用户 2026-10-04 第 7 条原话:「所有的时间选择,都已更换新插件,但是有个问题,
如果是选择一个时间范围的情况,就是说,有开始时间,有结束时间,应该用这个日期插件的
时间范围选择方式,而不是将这个范围拆成两个,这样不好看,也多一步操作。」
→ 有起止两端的场景不再并列两个控件,而是**一个范围控件**。
实现方式是"服务端照旧渲染两个原生控件(表单契约/e2e 都不动),
由 app.js 在升级时识别成一对、只挂一个 Semi 范围选择器"。所以这里只加标记:
$dtRange = 'start' | 'end' —— 这对控件属于同一个范围(调用方还要用
<div class="dt-range"> 把两者包在一起,见 movies/form.php 等)。
留空 = 普通单点控件(默认,行为完全不变)。 */
$dtRange = (string)($dtRange ?? '');
// ⚠️⚠️ 临时变量**一律带 dt 前缀**,一个都不能漏:这里原来写成
// `$t = strtotime(...)`,把宿主模板里表示「这条任务 / 这条记录」的 `$t`
// 覆盖成了 int —— 于是 include 之后凡是读 `$t['id']`、`$t['status']` 的地方
// 全部拿到 null,子任务列表空、评论不渲染、@高亮消失,而且**不报任何错**
// (PHP 8 对 "Trying to access array offset on value of type int" 只 Warning)。
// tasks/show.php 上就是这么被 e2e 抓到的(2026-10-02)。
$dtTs = (!empty($dtValue) && ($dtTsTmp = strtotime(trim((string)$dtValue)))) ? $dtTsTmp : false;
$dtDate = $dtTs ? date('Y-m-d', $dtTs) : '';
$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 Design DatePicker(用户点名,样式与宣纸主题统一)。
策略是**渐进增强**,原生四字段原样保留:
· 表单契约(*_date/_hour/_minute/_second → read_dt_input())不变,服务端零改动;
· Semi 资源(3.4 MB JS)只有页面真出现 .dt-input 才由 app.js 懒加载;
· 加载成功 → 隐藏 .dt-native、在 .dt-semi 里挂 DatePicker,值回写原字段;
· 加载失败/断网 → 原生控件原样可见可用,页面不会坏。 */
$dtCfg = ['name' => $dtName, 'seconds' => $dtSeconds, 'required' => $dtRequired,
'id' => $dtId, 'range' => $dtRange];
?>
<div class="dt-input<?= $dtRange !== '' ? ' dt-part' : '' ?>"
data-dt="<?= e(json_encode($dtCfg, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>">
<span class="dt-semi" aria-hidden="true"></span>
<span class="dt-native">
<input type="date" class="dt-date" name="<?= e($dtName) ?>_date"
<?= $dtId !== '' ? 'id="' . e($dtId) . '"' : '' ?>
value="<?= e($dtDate) ?>"<?= $dtRequired ? ' required' : '' ?>
aria-label="日期">
<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,还不止一个 Warning(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,
$dtTs, $dtTsTmp, $dtDate, $dtHour, $dtMin, $dtSec,
$dtH, $dtM, $dtS, $hv, $mv, $sv
); ?>