#3154·layui

[laydate] 跨夏令时(DST)日期计算错误:选日期会错天/错月

Author: LuckShrCreated Sep 14, 2026Updated Sep 14, 2026

说明:本议题的定位、修复与验证由 AI 助手(OpenClaw agent,运行在提交者本机)协助完成,由账号持有者授权提交;补丁经 headless Chrome(冻结系统时间)A/B 验证,未见其它逻辑改动。

议题条件

议题类型

疑是 BUG

使用版本

v2.13.9(当前最新发布版;v2.9.14 同样可复现3.0.0-alpha.3getEndDate 为同一实现)

问题描述

现象:系统时区为 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」求上月末日:

javascript
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 * STAMPmin:1(明天)在跨 DST 时会得到“今天 23:00”或“明天 01:00”
  • isDisabledYearOrMonth 的天数:Math.floor((endDay - startDay) / millisecondsInDay) + 1 → 跨 DST 的月份会多算一天,导致“整月所有日期都禁用”的判定失败

复现(最小)

  1. laydate.render({ elem: '#d1', lang: 'en', min: <今天0点时间戳> }),点开面板
  2. 系统时区设为 America/Los_Angeles,本地时间取 2026-09-13 23:09(可用 headless Chrome + 页面内伪造 Date 冻结“现在”来实现)
  3. 点击 lay-ymd="2026-10-6" 的格子 → input.value === '2026-10-01'
  4. 把时间改为 2026-09-14 00:01 重跑 → '2026-10-06'(正确)

建议修复(3 处,各一行)

diff
// ① 求上月末日:改用“下月第 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 与整月/整年禁用。

(已核对:官方文档未见夏令时/时区相关说明与限制;最新发布版仍存在该问题。)

业务代码

javascript
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 良好的社区氛围。