Files
FamilyBook/templates/_datetime.php
T
laoyang ecf121772f v24 第 12 批:5 条(删除按钮位置/日期控件三修/日历跳转/节气节日/财务图)
用户 2026-10-05 的 5 条。第 11 批交付后用户追问"确定这些需求都做完了?"—— 没有,
这 5 条是进一步要求。实现过程中查出 4 个真 bug。

#1 详情页删除按钮紧贴「开通账号」
   上一版放在页面最下方独立区块(danger-zone),用户要「就放在开通账号按钮下面,
   紧挨着,长度大小和开通账号一样,颜色改成红色」。→ 搬进「登录账号」卡片内
   (.del-inline + 细分隔线 + .btn-block-lg)。
   线上几何实测:同一张卡片、左边缘对齐 255==255、宽度一致 990==990、红色 rgb(192,83,63)。

#2 日期控件三修(两个真 bug + 一次闪烁)
   · 灰底:Semi **外层** .semi-datepicker-input 自带 rgba(46,50,56,.05)、高 32px;
     内层 wrapper 白底 40px → 灰的露在外面。实测「灰底元素」= 1 个 → 修后 0 个。
   · 不垂直居中:外层 32px < 内层 40px,实测 gapTop=1 / **gapBottom=17**(文字贴顶)
     → 修后 gapTop=1 / gapBottom=1。
   · 闪烁:实测首 **200ms** 原生输入框可见(要等 3.4MB React 解析)。
     用 no-js/js 二态:<html class="no-js"> + head 内联脚本立刻换 js +
     CSS `html.js .dt-input:not(.is-semi) .dt-native { visibility:hidden }`
     (配同高骨架 ::after 防布局跳动)。实测「出现过原生可见」= False。

#3 日历跳转保留视图 + 下拉日历
   原来写死 view=day(在月视图点跳转会被踢进日视图)。→ 表单带当前 $view(月还带 ym);
   选择器从"居中弹窗"改成**按钮下方下拉** .cal-jump-drop。
   实测:月视图跳 8/15 仍在月视图且是 8 月;周视图跳 8/15 → 落 8/10 那一周;
   日视图跳 8/15 → 显示 8/15。

#4 传统节日 + 二十四节气两个系统本(Schema v18)
   Holiday::SOLAR_TERMS(24 项 C 常数近似公式 + 例外表)+ traditionalRange
   (农历推导;除夕用"次日是否正月初一"反推,写死腊月廿九会有一半年份错)。
   实测 2026:节气 24 条全对(清明 4/5、冬至 12/22);春节 2/17、除夕 2/16、中秋 9/25。
   ⚠️ ENUM 必须显式 ALTER(对已存在的表不生效)——同一坑第二次踩,VERSION 17→18。

#5 财务(一个真 bug + 一个假起点 + 一个真 bug)
   · chart_line 读 `cents` 而 timeline() 返回 `net` → 键名不匹配,**总资产图每点读成 0**
     (正是用户说的"图上是 0")。修:兼容 cents ?? net ?? asset。
     实测 timeline net=800000(8000 元),线上 polyline 有真实起伏。
   · 去掉 timeline 人为插的"前一天=0"假起点(用户:"总资产不应该是 0,
     应该是所有账目创建时的金额的总数")。
   · ppSetSelected 只用 document.querySelector 按名字找,而财务页有**两个** person_id
     组件(「记一笔」+ 修改弹框)→ 永远灌到第一个。加 root 参数、调用方传 panel。

验证:e2e 786/0、交互 92/0、升级 129/0(+3 条 v18 断言)、batch_e 91/0、batch10 46/0、
batch9 38/0、picker_unify 28/0、picker_lazy 11/0、crop 17/0、formbloat 9/0、semi 27/0、
mobile 688/0、drag 5/0、geom 5/5;第 12 批专项:财务 7/0、日历 19/0。
部署静态断言新增 5 组(含反向验证:注入回退 → FAIL=1)。
线上自检 19/19;线上真实账号验收 **30/0**(含真实浏览器几何量删除按钮位置)。
2026-10-04 19:43:00 +08:00

98 lines
6.1 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' : '' ?><?= $dtRange === 'start' ? ' is-pending' : '' ?>"
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
); ?>