Files
FamilyBook/templates/_datetime.php
T
laoyang 075ac6bbeb v44 第 44 批:界面 5 条 + 移动端 API v1 全量
界面(用户 2026-10-12 第 1~5 条)
1. 新建日程「日历本」并入 .cs 自绘下拉,颜色画进候选项,三字与下拉同行。
2. 新建日程「开始/结束」改「日期范围:」,删旧日期控件;
   重复相关三行仅选重复时出现;删「(含第一次)」与「这段时间算忙碌」。
3. 条目浮层加「地点」、去掉「状态」行;首页红线按分钟定位(原只取整小时);
   去掉左侧红点只留红线。
4. 任务页删掉状态说明提示(STATUS_HINT 三值清空)。
5. 新建任务:标题在前树框在后;截止时间改最新日历控件;负责人默认自己;
   删冗余提示;「上面要做什么」→「任务名称」;清理括号说明。

移动端 API(用户第 6 条:导航模块除外,其余全量)
- 新增 Bearer 令牌鉴权(api_tokens 表,Schema v32),与登录口令 / DAV 口令三套分立。
- Api.php 内核(鉴权/取参/信封/分页/分发 + _method 覆写)+
  ApiHandler.php 17 条资源 + ApiToken.php + api.php 入口。
- 路由:router.php / .htaccess / index.php 三处 /api/ 兜底(部署脚本自动追加线上规则)。
- 个人设置新增「App 接口令牌」卡;docs/API.md 接口文档;
  docs/API-QUESTION-LIST.md 自主决策与待确认缺口清单。

验证:本地 e2e 1510/0、v43 真机 33/0、v43 反向 9/0、
v44 真机 41/0、v44 API 63/0、v44 反向 19/0;全流水线 PUSH_RC=0。
2026-10-12 15:31:03 +08:00

212 lines
14 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 里跟行结构较劲,不如让控件**自带**标签 —— 位置确定。
* $dtLabelStar 标签后是否跟一颗必填星号(配 $dtLabel 用)
* $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 : '';
$dtLabelStar = (bool)($dtLabelStar ?? 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,把最新控件挤到右边去("不在同一行左侧"的观感)。 */ ?>
<span class="dt-inline-lab mv-lb"><?= e($dtLabel) ?><?= $dtLabelStar ? '<i>*</i>' : '' ?></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, $dtLabelStar, $dtClass, $dtFieldId,
$dtTs, $dtTsTmp, $dtDate, $dtY, $dtMo, $dtD, $dtDays,
$dtHour, $dtMin, $dtSec,
$dtH, $dtM, $dtS, $hv, $mv, $sv, $dtYy, $dtMm, $dtDd
); ?>