[laydate] 跨夏令时(DST)日期计算错误:选日期会错天/错月
说明:本议题的定位、修复与验证由 AI 助手(OpenClaw agent,运行在提交者本机)协助完成,由账号持有者授权提交;补丁经 headless Chrome(冻结系统时间)A/B 验证,未见其它逻辑改动。
议题条件
- 我确认已查看官方使用文档:https://layui.dev ,但没有找到相关解决方案。
- 我确认已在 Issues 中搜索过类似的问题,但没有找到相关解决方案。
- 我已仔细阅读: Layui Issue 贡献指南
议题类型
疑是 BUG
使用版本
v2.13.9(当前最新发布版;v2.9.14 同样可复现;3.0.0-alpha.3 中 getEndDate 为同一实现)
问题描述
现象:系统时区为 America/Los_Angeles(启用夏令时)、本地时间 2026-09-13 23:09 时:
| 操作 | 实际结果 | 期望 |
|---|---|---|
| 9 月面板点击十月的补位格子「10月6日」 | 2026-10-01 | 2026-10-06 |
| 10 月面板点击「10月6日」 | 2026-11-6(该面板 10 月格子只剩 1 个,其余被错标为 11 月) | 2026-10-06 |
把本地时间改为 2026-09-14 00:01,或改用无夏令时的时区(如 Asia/Shanghai),结果恢复正常——说明与 DST 相关。
根因:laydate.getEndDate() 用「下月 1 号时间戳 − 86400000ms」求上月末日:
laydate.getEndDate = function (month, year) {
var thisDate = new Date(); // 保留了“当前时刻”(23:09)
thisDate.setFullYear(year || thisDate.getFullYear(), month || thisDate.getMonth() + 1, 1);
return new Date(thisDate.getTime() - 1000 * 60 * 60 * 24).getDate();
};setFullYear 会把“今天的时间部分”带入目标月;而 2026-11-01 是美国夏令时结束日(02:00 回拨 1 小时),
“减 24 小时”实际只倒退了 23 小时本地时间 → 没有跨过月份边界:
getEndDate(10, 2026):2026-11-01 23:09 → 减 24h → 2026-11-01 00:09 → getDate() = 1 (应为 31)随后 checkValid() 把用户点的「6 日」夹成「1 日」;calendar() 又把“本月天数”当成 1 天,导致面板格子错标。
另有两处同类写法(都用“固定 24 小时”当一天,DST 下不成立):
- 数值型
min/max的日偏移:time + day * STAMP→min:1(明天)在跨 DST 时会得到“今天 23:00”或“明天 01:00” isDisabledYearOrMonth的天数:Math.floor((endDay - startDay) / millisecondsInDay) + 1→ 跨 DST 的月份会多算一天,导致“整月所有日期都禁用”的判定失败
复现(最小):
laydate.render({ elem: '#d1', lang: 'en', min: <今天0点时间戳> }),点开面板- 系统时区设为
America/Los_Angeles,本地时间取 2026-09-13 23:09(可用 headless Chrome + 页面内伪造Date冻结“现在”来实现) - 点击
lay-ymd="2026-10-6"的格子 →input.value === '2026-10-01' - 把时间改为 2026-09-14 00:01 重跑 →
'2026-10-06'(正确)
建议修复(3 处,各一行):
// ① 求上月末日:改用“下月第 0 天”(纯日期运算,DST 无关)
- return new Date(thisDate.getTime() - 1000 * 60 * 60 * 24).getDate();
+ thisDate.setFullYear(year || thisDate.getFullYear(), month || thisDate.getMonth() + 1, 0);
+ return thisDate.getDate();
// ② 数值型 min/max 的日偏移
- time + day * STAMP
+ new Date(time).setDate(new Date(time).getDate() + day)
// ③ 整月/整年禁用时的天数
- Math.floor((endDay.getTime() - startDay.getTime()) / millisecondsInDay) + 1
+ Math.round((endDay.getTime() - startDay.getTime()) / millisecondsInDay)影响面:所有启用夏令时的时区(美/欧/澳等)在 DST 切换日附近使用 laydate 选择日期都可能出错;②③ 另影响数值型 min/max 与整月/整年禁用。
(已核对:官方文档未见夏令时/时区相关说明与限制;最新发布版仍存在该问题。)
业务代码
layui.use(function () {
var laydate = layui.laydate;
const nowDate = new Date();
nowDate.setHours(0, 0, 0, 0);
laydate.render({
elem: '#ID-laydate-start-date-1',
lang: 'en',
min: nowDate.getTime(),
value: start_date || '',
done: function (value, date) {}
});
});浏览器
Chrome(headless=new,TZ=America/Los_Angeles,页面内伪造 Date 冻结“现在”以稳定复现);在手机浏览器(Safari/Chrome)亦同样复现。
演示地址
(无演示站;按上述最小复现步骤可在本地稳定复现)
友好承诺
- 我承诺将本着相互尊重、理解和友善的态度进行交流,共同维护 Layui 良好的社区氛围。
Source: layui/layui