日期框换成自控输入(原生 date 会吞位)+ 气温图桌面端铺满 + 同行卡片不再被拉伸
三件事都由用户实测反馈,前两件先复现再动手。
一、日期框:敲 20260606 变成 202606/06(用户要「永远解决」)
- **实测复现**:写了个真实按键工具(CDP Input.dispatchKeyEvent 每字符带 text;
技能里的 `--press` 只给 Enter 带 text,数字敲不进去)。在 /stats 的「起始日期」上敲 20260606:
敲之前 `INPUT|date|`、attached=false;敲完显示 `202606/06`、`el.value` 是**空**(非法)—— 截图为证。
- **根因**:筛选区的起止日期是原生 `<input type="date">`,而挂载代码只写了
`querySelectorAll('input[type=datetime-local]')` ⇒ **type=date 从没人管**。
Chrome 的年段允许超过 4 位数字(年份上限 275760),8 位被切成 年=202606 + 月=06。
原生分段状态读不到、`setSelectionRange` 对 date 类输入直接抛 ⇒ 修不了,只能换控件
(用户也说了「如果当前控件无法实现就换别的」)。
- **做法**:`datetime-input.js` 加「只日期」模式(`SPEC.date`:年/月/日三段,输出 `Y-m-d`),
并提供**统一入口 `attachAll(root)`** 按 `el.type` 分派;`attach` 里先把控件换成 `text`
(引导会写 `2026-06-` 这种半成品,而原生控件对非法值一律置空)。
`datetime-input.js` 改为**无条件引入**(公共页筛选区也要用;flatpickr 仍按需加载 ——
没有它时只挂输入引导、不给日历,省 60 多 KB),挂载在 body 末尾**立刻**跑一次
(别只等 DOMContentLoaded,否则会先露出原生框再变文本框)。三个后台页改用 `attachAll`,
顺带覆盖行程筛选的 `mfrom/mto`(date 类走 `enableTime:false`,实测无时间选择器)。
- **验证**:真实按键敲 20260606 → `value="2026-06-06"`;端到端 `from=2025-08-01&to=2025-08-31`
筛出 **186 次**,与月度图峰值月(186)完全吻合(排除「解析成空导致恒 0」)。
门禁 `TestDateInputsGoThroughAttachAll`:模板里再出现按 type 挑的选择器即红(做过变异验证);
JS 单测补 11 条逐键模拟(45/45)。
二、逐月平均气温图在桌面端很小
- 量出来的:10 个月固定 36px 宽,只占 1042px 里的 **396px**,右边 600 多像素全空。
- 窄屏保持「固定 36px + 横滚」(挤在一起月份标签会糊),宽屏改 `flex: 1 1 36px; max-width: 96px`
并把轨道从 84px 加到 130px、字号提到 `--fs-sm`。
三、类型分布与一天中的分布同行高度不齐
- grid 默认 `align-items: stretch`,把 256px 的「一天中的分布」拉到与 1411px 的「类型分布」一样高,
卡片内部留一大片空白。改成 `.grid { align-items: start }`。
回归:./scripts/verify.sh **PASS=349 FAIL=0 SKIP=0**;移动端 390 / 桌面 1440 溢出与改前一致。
⚠️ 两个验证纪律(都写进 details 三十二):
- 验收跑着的时候别并发跑测试/变异实验(两次踩到,其中一次变异正好被验收抓到,那轮结果作废)。
- **变异验证「内嵌文件」(templates/static)之后必须 `go clean -cache`**:`//go:embed` 的内容
在「改坏→还原」后,构建缓存会继续给出变异期间编译的那份 —— 实测同一个测试
`go test ./internal/web/` 绿、`go test ./...` 红,`go clean -testcache` 不管用。
This commit is contained in:
@@ -1,28 +1,40 @@
|
||||
/* datetime-input:修掉 flatpickr allowInput 的「整串解析」毛病。
|
||||
/* datetime-input:把 <input type="date" / type="datetime-local"> 换成**我们完全掌控**的
|
||||
* 文本输入 + 手输引导。
|
||||
*
|
||||
* 背景(实测 flatpickr 4.6.13,见 memory/2026-10-08.md):
|
||||
* 1. flatpickr 会把 <input type="datetime-local"> 就地改成 type="text"
|
||||
* (altInput 默认 false、不新建节点),原生那套分段编辑器被整个替换掉,
|
||||
* 所以「敲满一段自动跳下一段」这个行为根本不存在。
|
||||
* 2. allowInput 的语义是「拿到整串再 parseDate」,不是「逐段消费」。实测:
|
||||
* parseDate('2026') → 2026 年 1 月
|
||||
* parseDate('202666') → 2026 年 6 月 ← 用户要打到这个长度才「看到月份」
|
||||
* parseDate('20260606') → 2030 年 12 月 ← 静默错解,且不报错
|
||||
* 年份得吃满 4 位才被认,之后月份位还要再吃 2 位 ⇒ 正是「年份很长才跳下一位」。
|
||||
* 为什么不用原生控件(两次踩过,都是实测):
|
||||
* 1. flatpickr 会把 type=datetime-local 就地改成 type=text(altInput 默认 false、不新建节点),
|
||||
* 原生那套分段编辑器被整个替换掉;而 allowInput 的语义是「拿到整串再 parseDate」,
|
||||
* 实测 parseDate('2026') → 1 月、'202666' → 2026-06、'20260606' → 2030-12(静默错解)。
|
||||
* 2. **原生 type=date 自己也不可靠**:Chrome 的年段允许超过 4 位数字(年份上限 275760),
|
||||
* 于是敲 20260606 会被切成 年=202606 + 月=06,框里显示 202606/06(zh-CN 用 / 分隔),
|
||||
* 而 el.value 是**空**=非法 ⇒ 筛选条件静默丢掉。2026-10-09 用户报的就是这条。
|
||||
* 原生分段状态读不到、光标也设不了(setSelectionRange 对 date 类输入直接抛),
|
||||
* 所以「修原生」这条路走不通,只能换掉控件。
|
||||
*
|
||||
* 这里做两件事:
|
||||
* parseDateStrict 宽松但**无歧义**的解析器:接受 2026/6/6、2026-6-6、20260606、
|
||||
* 2026 等写法;解析不出来就是 null,绝不猜。
|
||||
* attach 按键引导:年份满 4 位自动补分隔符并把光标推到月份段,
|
||||
* 月/日/时/分满 2 位同理,让手输 behave 像原生分段。
|
||||
* 于是这里做三件事:
|
||||
* parseDateStrict 宽松但**无歧义**的解析器:接受 2026/6/6、2026-6-6、20260606、2026 等,
|
||||
* 解析不出来就是 null,绝不猜(交给 flatpickr 的 parseDate 钩子)。
|
||||
* attach 输入引导:年满 4 位自动补分隔符并把光标推到月份段,其余段满 2 位同理。
|
||||
* attachAll **统一入口**:一次覆盖 type=datetime-local 与 type=date 两种输入。
|
||||
* 2026-10-09 的事故根因就是「只挂了 datetime-local」,后来加的 type=date
|
||||
* 筛选框没人管 —— 以后加日期字段一律走 attachAll,别在页面里散着手写选择器。
|
||||
*
|
||||
* 产出格式恒为 'Y-m-dTH:i'(即 2026-06-06T09:30),与服务端
|
||||
* handlers_edit.go / handlers_topics.go 里的 "2006-01-02T15:04" 同一形状。
|
||||
* 产出格式:datetime 恒为 'Y-m-dTH:i'(2026-06-06T09:30),只日期恒为 'Y-m-d'(2026-06-06),
|
||||
* 分别与服务端 handlers_edit.go 的 "2006-01-02T15:04" 和筛选区的 "2006-01-02" 同一形状。
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
var DATE_FMT = 'Y-m-dTH:i';
|
||||
var DAY_FMT = 'Y-m-d';
|
||||
|
||||
/* 两种版式。段长/分隔符/输出格式只差这几个数,算法完全共用。 */
|
||||
var SPEC = {
|
||||
dt: { LEN: [4, 2, 2, 2, 2], SEP: ['-', '-', 'T', ':', ''], fmt: DATE_FMT },
|
||||
date: { LEN: [4, 2, 2], SEP: ['-', '-', ''], fmt: DAY_FMT }
|
||||
};
|
||||
|
||||
function specFor(opts) { return (opts && opts.dateOnly) ? SPEC.date : SPEC.dt; }
|
||||
|
||||
function pad2(n) { return n < 10 ? '0' + n : String(n); }
|
||||
|
||||
@@ -137,6 +149,7 @@
|
||||
* 严格分段格式:年 4 位、月/日/时/分各 2 位,一律补零,敲满一段立刻补分隔符并跳下一段。
|
||||
* 于是「2026」+「06」+「06」+「09」+「30」会边敲边变成
|
||||
* 2026-06-06T09:30,敲完 4 位年份光标就已经在月份位(这正是原版做不到的那件事)。
|
||||
* 只日期版少掉时分两段,输出 2026-06-06。
|
||||
*
|
||||
* 不做的事(有意为之):
|
||||
* - **不自动补零、不做前滚**:月就按 06 敲,敲 '6' 不会变成 '06'。
|
||||
@@ -144,8 +157,6 @@
|
||||
* - **不静默改值**:月份敲 '13'、日敲 '45' 一律原样保留,交给解析器报错。
|
||||
* 把 13 月悄悄改成 1 月比报错坏得多——用户会以为敲对了。
|
||||
*/
|
||||
var SEG_LEN = [4, 2, 2, 2, 2]; // 年月日时分
|
||||
var SEP_AFTER = ['-', '-', 'T', ':', ''];
|
||||
|
||||
// 各段填满后的合法范围(只判能明确非法的:月 13、日 32、时 24、分 60)
|
||||
var SEG_FULL_MAX = [
|
||||
@@ -172,68 +183,91 @@
|
||||
return new Date(y, mo, 0).getDate();
|
||||
}
|
||||
|
||||
/* 原始输入里,紧跟第 n 位数字之后的那个分隔符是什么(没有就返回空)。
|
||||
* 用来在「段未填满」时把用户已经敲好的分隔符原样带回来,
|
||||
* 避免把 '2026-6' 硬补零成 '2026-06'(用户 1 月就该敲 01,但
|
||||
* 6 月可以只敲 6 —— 这两种都得能往下走)。 */
|
||||
function sepAfterDigits(raw, n) {
|
||||
var seen = 0;
|
||||
for (var k = 0; k < raw.length; k++) {
|
||||
var ch = raw.charAt(k);
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
seen++;
|
||||
if (seen === n) {
|
||||
var nx = raw.charAt(k + 1);
|
||||
if (nx && (nx === '-' || nx === 'T' || nx === ':')) return nx;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/* 按位切分并排出标准版式。返回 null 表示「用户敲的这段已越界,原样保留」。
|
||||
* 年份固定 4 位;月/日(时/分)固定 2 位 —— 不猜、不前滚、不自动补零。
|
||||
*
|
||||
* 入参 raw 是当前框里的值,**可以带我们自己补的分隔符**。这里按位取数时
|
||||
* 必须跳过分隔符(用户敲到 '2026-0' 时,数字是 20260 而不是 2020),
|
||||
* 否则位次会整体错位一格——这是实测踩出来的。
|
||||
*
|
||||
* 导出成纯函数(layoutTo)是为了能在 node 里直接跑断言,不用起浏览器。 */
|
||||
function layoutTo(spec, raw) {
|
||||
var chars = String(raw).split('');
|
||||
var digits = '';
|
||||
for (var k = 0; k < chars.length; k++) {
|
||||
var ch = chars[k];
|
||||
if (ch >= '0' && ch <= '9') digits += ch;
|
||||
}
|
||||
if (!digits) return { value: '', caret: 0 };
|
||||
if (digits.length > spec.LEN.reduce(function (a, b) { return a + b; }, 0)) return null;
|
||||
var out = '', pos = 0, year = 0, mon = 0;
|
||||
for (var i = 0; i < spec.LEN.length; i++) {
|
||||
var want = spec.LEN[i];
|
||||
var have = digits.slice(pos, pos + want);
|
||||
var full = have.length === want;
|
||||
var check = full ? SEG_FULL_MAX[i] : SEG_PREFIX_MAX[i];
|
||||
if (check && check(have, year, mon)) return null;
|
||||
// 本段还没填满 → 保持原样让用户继续敲。
|
||||
// 若原始输入里这一段后面已经带了分隔符(用户手敲 '2026-6'),
|
||||
// 就把它带回来,光标停在分隔符之后,而不是硬补零。
|
||||
if (!full) {
|
||||
var tailSep = sepAfterDigits(raw, pos + have.length);
|
||||
var val = out + have + tailSep;
|
||||
return { value: val, caret: val.length };
|
||||
}
|
||||
// 填满 → 结算本段
|
||||
if (i === 0) year = +have;
|
||||
if (i === 1) mon = +have;
|
||||
out += have;
|
||||
pos += want;
|
||||
var sep = spec.SEP[i] || '';
|
||||
// 数字用完了:本段后面还有段要填就立刻补分隔符、把光标推到下一段起点。
|
||||
// 这一步就是「敲完 4 位年份自动跳到月份位」的实现。
|
||||
if (pos >= digits.length) {
|
||||
if (!sep) return { value: out, caret: out.length };
|
||||
return { value: out + sep, caret: out.length + sep.length };
|
||||
}
|
||||
out += sep;
|
||||
}
|
||||
return { value: out, caret: out.length };
|
||||
}
|
||||
|
||||
function attach(el, opts) {
|
||||
opts = opts || {};
|
||||
if (el._rondAttached) return;
|
||||
el._rondAttached = true;
|
||||
var spec = specFor(opts);
|
||||
|
||||
/* 按位切分并排出标准版式。返回 null 表示「用户敲的这段已越界,原样保留」。
|
||||
* 年份固定 4 位;月/日/时/分固定 2 位 —— 不猜、不前滚、不自动补零。
|
||||
*
|
||||
* 入参 raw 是当前框里的值,**可以带我们自己补的分隔符**。这里按位取数时
|
||||
* 必须跳过分隔符(用户敲到 '2026-0' 时,数字是 20260 而不是 2020),
|
||||
* 否则位次会整体错位一格——这是实测踩出来的。 */
|
||||
function layout(raw) {
|
||||
// 只保留数字。必须跳过分隔符:用户敲到 '2026-0' 时数字是 20260 而不是 2020,
|
||||
// 否则位次整体错位一格 —— 这是实测踩出来的。
|
||||
var chars = String(raw).split('');
|
||||
var digits = '';
|
||||
for (var k = 0; k < chars.length; k++) {
|
||||
var ch = chars[k];
|
||||
if (ch >= '0' && ch <= '9') digits += ch;
|
||||
}
|
||||
if (!digits) return { value: '', caret: 0 };
|
||||
if (digits.length > 14) return null;
|
||||
var out = '', pos = 0, year = 0, mon = 0;
|
||||
for (var i = 0; i < SEG_LEN.length; i++) {
|
||||
var want = SEG_LEN[i];
|
||||
var have = digits.slice(pos, pos + want);
|
||||
var full = have.length === want;
|
||||
var check = full ? SEG_FULL_MAX[i] : SEG_PREFIX_MAX[i];
|
||||
if (check && check(have, year, mon)) return null;
|
||||
// 本段还没填满 → 保持原样让用户继续敲。
|
||||
// 若原始输入里这一段后面已经带了分隔符(用户手敲 '2026-6'),
|
||||
// 就把它带回来,光标停在分隔符之后,而不是硬补零。
|
||||
if (!full) {
|
||||
var tailSep = sepAfterDigits(raw, pos + have.length);
|
||||
var val = out + have + tailSep;
|
||||
return { value: val, caret: val.length };
|
||||
}
|
||||
// 填满 → 结算本段
|
||||
if (i === 0) year = +have;
|
||||
if (i === 1) mon = +have;
|
||||
out += have;
|
||||
pos += want;
|
||||
var sep = SEP_AFTER[i] || '';
|
||||
// 数字用完了:本段后面还有段要填就立刻补分隔符、把光标推到下一段起点。
|
||||
// 这一步就是「敲完 4 位年份自动跳到月份位」的实现。
|
||||
if (pos >= digits.length) {
|
||||
if (!sep) return { value: out, caret: out.length };
|
||||
return { value: out + sep, caret: out.length + sep.length };
|
||||
}
|
||||
out += sep;
|
||||
}
|
||||
return { value: out, caret: out.length };
|
||||
/* 先把控件换成 text。引导过程会往框里写 '2026-06-' 这种**半成品**,
|
||||
而 type=date / datetime-local 对非法值一律置空(值当场消失),
|
||||
只有 text 我们才拿得住。提交格式不变(date → Y-m-d、datetime → Y-m-dTH:i)。 */
|
||||
if (el.type !== 'text') {
|
||||
el.type = 'text';
|
||||
el.setAttribute('inputmode', 'numeric'); // 手机上直接给数字键盘
|
||||
el.setAttribute('autocomplete', 'off');
|
||||
if (!el.placeholder) el.placeholder = opts.dateOnly ? '2026-06-06' : '2026-06-06T09:30';
|
||||
}
|
||||
|
||||
/* input 事件统一排版:按键、粘贴、退格三个入口都会走到这里,比只挂 keydown 周全。
|
||||
*
|
||||
* 为什么要「延后一帧」挪光标:flatpickr 自己也监听 input/setDate,
|
||||
* 会在我们之后改写 selectionStart。我们在同步阶段设的位置会被它挪走,
|
||||
* 于是下一次按键就插到分隔符前面(实测出现 '2026-6' 这种月份位错位)。
|
||||
* 所以同步阶段只改 value,把光标用微任务补回去——那时 flatpickr 已经改完了。
|
||||
*
|
||||
* 交接判据是「值里除我们自己补的以外还有没有别的分隔符」:
|
||||
* 一旦用户亲手敲了 / 或 :,立刻彻底放手(否则会出现 '2026-/6/6' 这种
|
||||
* 我们补的 '-' 和用户的 '/' 混排的乱串),改由 fromSeparated 解他的写法。 */
|
||||
/* 分隔符是否全是标准套(- T :)。'2026-6-6' 算(横杠是我们和用户通用的),
|
||||
* '2026/6/6' 不算(外来写法,交给 fromSeparated 原样解)。 */
|
||||
function isStandardSep(v) {
|
||||
@@ -246,6 +280,12 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
/* input 事件统一排版:按键、粘贴、退格三个入口都会走到这里,比只挂 keydown 周全。
|
||||
*
|
||||
* 为什么要「延后一帧」挪光标:flatpickr 自己也监听 input/setDate,
|
||||
* 会在我们之后改写 selectionStart。我们在同步阶段设的位置会被它挪走,
|
||||
* 于是下一次按键就插到分隔符前面(实测出现 '2026-6' 这种月份位错位)。
|
||||
* 所以同步阶段只改 value,把光标用微任务补回去——那时 flatpickr 已经改完了。 */
|
||||
function onInput() {
|
||||
var cur = el.value;
|
||||
if (!cur) { el._rondLaidOut = ''; return; }
|
||||
@@ -256,7 +296,7 @@
|
||||
段还没填满就原样留着(他还在敲)。 */
|
||||
if (isStandardSep(cur)) { applyLayout(cur); return; }
|
||||
/* 外来写法(/ . 空格)。只做一件最小修复:把「紧跟在分隔符后面、
|
||||
与前一个叠起来」的那个分隔符压掉——'2026-' + '/' 会变成 '2026-/',
|
||||
与前一个叠起来」的那个分隔符压掉——'2026-' + '/' 会变成 '2026-',
|
||||
双分隔符会让 fromSeparated 判非法。除此之外一个字符都不动,
|
||||
保持用户原样。 */
|
||||
var fixed = cur.replace(/([-T:])([/.])/g, '$2').replace(/([-T:])\1/g, '$1');
|
||||
@@ -267,29 +307,8 @@
|
||||
el._rondLaidOut = '';
|
||||
}
|
||||
|
||||
/* 原始输入里,紧跟第 n 位数字之后的那个分隔符是什么(没有就返回空)。
|
||||
* 用来在「段未填满」时把用户已经敲好的分隔符原样带回来,
|
||||
* 避免把 '2026-6' 硬补零成 '2026-06'(用户 1 月就该敲 01,但
|
||||
* 6 月可以只敲 6 —— 这两种都得能往下走)。 */
|
||||
function sepAfterDigits(raw, n) {
|
||||
var seen = 0;
|
||||
for (var k = 0; k < raw.length; k++) {
|
||||
var ch = raw.charAt(k);
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
seen++;
|
||||
if (seen === n) {
|
||||
// 找到第 n 位数字,看它后面紧跟的是不是分隔符
|
||||
var nx = raw.charAt(k + 1);
|
||||
if (nx && (nx === '-' || nx === 'T' || nx === ':')) return nx;
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function applyLayout(cur) {
|
||||
var r = layout(cur);
|
||||
var r = layoutTo(spec, cur);
|
||||
// 越界(月份敲成 66、日敲成 45):原样保留用户输入,交给解析器报错。
|
||||
// 这里必须 return 且**不动 el.value**——el.value 一旦被我们改写却没同步
|
||||
// _rondLaidOut,下一轮就会拿旧串去排,越排越乱。
|
||||
@@ -317,9 +336,9 @@
|
||||
|
||||
el.addEventListener('input', onInput);
|
||||
|
||||
/* blur 时把「敲了一半」的串收敛成能被解析的形状:2026 → 2026-01-01T00:00。
|
||||
* 不这么做的话用户只填了年份就点走,flatpickr 拿到 '2026' 会按原版规则
|
||||
* 补成当月 1 号,但 '2026-' 这种带尾巴的残串会被判非法。 */
|
||||
/* blur 时把「敲了一半」的串收敛成能被解析的形状:2026 → 2026-01-01(只日期)
|
||||
* 或 2026-01-01T00:00(带时间)。不这么做的话用户只填了年份就点走,
|
||||
* flatpickr 拿到 '2026' 会按原版规则补成当月 1 号,但 '2026-' 这种带尾巴的残串会被判非法。 */
|
||||
function onBlur() {
|
||||
var cur = el.value.trim();
|
||||
if (!cur) return;
|
||||
@@ -330,47 +349,69 @@
|
||||
el.addEventListener('blur', onBlur);
|
||||
|
||||
function formatOut(d) {
|
||||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()) +
|
||||
'T' + pad2(d.getHours()) + ':' + pad2(d.getMinutes());
|
||||
var day = d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate());
|
||||
if (opts.dateOnly) return day;
|
||||
return day + 'T' + pad2(d.getHours()) + ':' + pad2(d.getMinutes());
|
||||
}
|
||||
el._rondFormatOut = formatOut;
|
||||
}
|
||||
|
||||
function init(selector, opts) {
|
||||
if (!global.flatpickr) return;
|
||||
var els = typeof selector === 'string' ? document.querySelectorAll(selector) : selector;
|
||||
Array.prototype.forEach.call(els, function (el) {
|
||||
attachPicker(el, opts);
|
||||
});
|
||||
}
|
||||
|
||||
/* 三处页面(编辑页 / 照片页 / 专题页)共用的挂载入口。
|
||||
配置刻意与原先完全一致,只多了 parseDate 与输入引导两件事:
|
||||
parseDate 修掉 allowInput 的「整串解析」与静默错解
|
||||
attach 让手输 behave 像原生分段(敲满 4 位年份就跳到月份位)
|
||||
提交格式仍是 'Y-m-dTH:i',服务端那条 "2006-01-02T15:04" 解析不受影响。 */
|
||||
function attachPicker(el, opts) {
|
||||
if (!global.flatpickr || el._rondPickerReady) return null;
|
||||
el._rondPickerReady = true;
|
||||
var inst = global.flatpickr(el, {
|
||||
enableTime: true,
|
||||
time_24hr: true,
|
||||
allowInput: true,
|
||||
// 补录靠回忆,没人记得 12:59:47——5 分钟步进贴合「大概那个点」的精度
|
||||
minuteIncrement: 5,
|
||||
dateFormat: DATE_FMT,
|
||||
parseDate: parseDateStrict
|
||||
});
|
||||
opts = opts || {};
|
||||
var dateOnly = !!opts.dateOnly;
|
||||
if (!global.flatpickr) { attach(el, opts); return null; }
|
||||
if (!el._rondPickerReady) {
|
||||
el._rondPickerReady = true;
|
||||
global.flatpickr(el, {
|
||||
enableTime: !dateOnly,
|
||||
time_24hr: true,
|
||||
allowInput: true,
|
||||
// 补录靠回忆,没人记得 12:59:47——5 分钟步进贴合「大概那个点」的精度
|
||||
minuteIncrement: 5,
|
||||
dateFormat: dateOnly ? DAY_FMT : DATE_FMT,
|
||||
parseDate: parseDateStrict
|
||||
});
|
||||
}
|
||||
attach(el, opts);
|
||||
return inst;
|
||||
return el._rondPicker;
|
||||
}
|
||||
|
||||
/* ---------- 统一入口 ----------
|
||||
* 覆盖两种日期输入:带时间的走 flatpickr + 引导,只日期的走(有 flatpickr 就一并给日历,
|
||||
* 没有就只挂引导——公共页不加载 flatpickr,见 layout.html 的说明)。
|
||||
* ⚠️ 以后再加日期字段一律走这里,别在页面里手写 querySelector('input[type=...]'):
|
||||
* 2026-10-09 的事故就是只挂了 datetime-local、新加的 type=date 没人管。 */
|
||||
function attachAll(root, opts) {
|
||||
root = root || document;
|
||||
var els = root.querySelectorAll('input[type=datetime-local], input[type=date]');
|
||||
Array.prototype.forEach.call(els, function (el) {
|
||||
var o = { dateOnly: el.type === 'date' };
|
||||
if (global.flatpickr) attachPicker(el, o);
|
||||
else attach(el, o);
|
||||
});
|
||||
return els.length;
|
||||
}
|
||||
|
||||
function init(selector, opts) {
|
||||
var els = typeof selector === 'string' ? document.querySelectorAll(selector) : selector;
|
||||
Array.prototype.forEach.call(els, function (el) { attachPicker(el, opts); });
|
||||
}
|
||||
|
||||
global.rondDatetime = {
|
||||
DATE_FMT: DATE_FMT,
|
||||
DAY_FMT: DAY_FMT,
|
||||
SPEC: SPEC,
|
||||
layoutTo: layoutTo,
|
||||
parseDate: parseDateStrict,
|
||||
attach: attach,
|
||||
attachPicker: attachPicker,
|
||||
attachAll: attachAll,
|
||||
init: init,
|
||||
pad2: pad2
|
||||
};
|
||||
})(window);
|
||||
})(window);
|
||||
|
||||
@@ -80,6 +80,35 @@ console.log('\n=== round-trip:解析结果必须能原样喂回 formatDate ===
|
||||
eq(bad, [], 'round-trip 一致');
|
||||
})();
|
||||
|
||||
console.log('\n=== 手输引导 layoutTo:逐键模拟(这才是这次 bug 的现场) ===');
|
||||
(function () {
|
||||
const D = api.SPEC.date, T = api.SPEC.dt;
|
||||
const lay = (spec, raw) => { const r = api.layoutTo(spec, raw); return r ? r.value : null; };
|
||||
// 逐键模拟:每敲一位,就把「当前框里的值 + 新键」再喂回去 —— 与浏览器里 input 事件一致
|
||||
const type = (spec, keys) => {
|
||||
let v = '';
|
||||
for (const k of keys) { const n = lay(spec, v + k); v = (n === null) ? (v + k) : n; }
|
||||
return v;
|
||||
};
|
||||
|
||||
// ⚠️ 回归:2026-10-09 用户报的就是这个 —— 原生的 type=date 里敲 20260606 变成了 202606/06
|
||||
eq(type(D, '20260606'), '2026-06-06', '只日期模式逐键敲 20260606 → 2026-06-06');
|
||||
eq(lay(D, '20260606'), '2026-06-06', '只日期模式一次性粘 8 位 → 2026-06-06(没有时间尾巴)');
|
||||
eq(type(D, '2026'), '2026-', '只日期:敲完年份就补横杠、光标进月份段');
|
||||
eq(type(D, '202606'), '2026-06-', '只日期:敲完月份就补横杠、光标进日段');
|
||||
eq(type(D, '20260606'), '2026-06-06', '只日期:敲完日段不再补尾巴');
|
||||
|
||||
// 重构后带时间那条必须一字不变(原来 34 条断言的语义)
|
||||
eq(type(T, '202606060930'), '2026-06-06T09:30', '带时间模式逐键敲 12 位 → 2026-06-06T09:30');
|
||||
eq(lay(T, '20260606'), '2026-06-06T', '带时间模式一次性粘 8 位 → 后面还留个 T 等时分');
|
||||
|
||||
// 越界一律「不接管」,原样留给解析器报错
|
||||
eq(lay(D, '20261306'), null, '只日期:月 13 → 不接管');
|
||||
eq(lay(D, '20260632'), null, '只日期:日 32 → 不接管');
|
||||
eq(lay(D, '20260230'), null, '只日期:2026-02-30 不存在 → 不接管');
|
||||
eq(lay(D, '202606061'), null, '只日期:9 位没有第 4 段可放 → 不接管');
|
||||
})();
|
||||
|
||||
console.log('\n=== 汇总 ===');
|
||||
console.log(' pass=' + pass + ' fail=' + fail);
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user