简介:DateTimePicker 是一套面向网页前端开发者的 jQuery 日期时间选择插件,适用于注册表单、预约系统、后台筛选等需要用户输入或选取日期时间的场景。压缩包共9个文件,以4张界面预览图、2个JavaScript文件、1个CSS样式表及HTML演示页为主,整体约73KB,轻量易引入,便于快速查看效果与调试。插件支持多语言切换、自定义日期时间格式、日/周/月/年多视图以及时分秒选择,还可设置禁用日期、绑定选择与月份切换等回调,并提供皮肤定制、响应式布局和无障碍操作支持。包内演示页面与截图能帮助读者直观理解组件的交互细节,源码结构也便于按项目需求调整参数和样式。已有283人学习,适合需要在前端项目中集成日期时间控件的开发者参考与二次开发。
1. 先别急着贴代码:DateTimePicker 这类 jQuery 日期时间插件到底值不值得拆
后台表单里最容易被低估的控件就是日期时间输入。jQuery 日期和时间插件 DateTimePicker 这类资源,本质上是一个挂在 jQuery 上的 JS特效-日期时间 落地件:把文本框变成日历面板,补齐时分秒选择、范围限制和回填格式。它不解决业务规则,但能把“手输 2024-13-40”这种脏数据挡在提交前。适合还在维护 jQuery 技术栈的后台、预约、筛选和报表页面,不适合追求零依赖的轻量项目。
如果你手上正好有 DateTimePicker.zip,先别把它当黑匣子丢进项目。下面按“跑通最小页面—收参数—接表单—排坑—验证”的顺序拆,新手能照着复现,熟手能对照边界。
2. 拆开资源包:jQuery 插件初始化链路与最小可跑页面
2.1 先确认它是不是 jQuery 依赖型插件
jQuery 插件不是独立组件,它靠全局 jQuery 注册方法。压缩包解开后先看示例页的引入顺序,常见做法是先引 jQuery,再引插件脚本,样式表放在 head 里。这个顺序反了,控制台通常会给两个结果:$ is not a function或者datetimepicker is not a function。前者是 jQuery 没就绪,后者是插件脚本没注册上,也可能是路径大小写不对导致 404。
最小页面不用做完整业务,只放一个 input、一个样式表、两个脚本。路径按包内实际文件名替换,不要照抄我这里的占位名。readonly加在 input 上,目的是避免手机浏览器点输入框先弹软键盘,把日历面板顶到屏幕外。autocomplete="off"是防止浏览器历史下拉盖住面板,后台表单里这个细节很烦人。
<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 先引插件样式,再引页面样式,方便覆盖 --> <link rel="stylesheet" href="./assets/datetimepicker.css"> <link rel="stylesheet" href="./assets/page.css"> </head> <body> <label for="startTime">开始时间</label> <input id="startTime" type="text" readonly autocomplete="off" placeholder="选择日期时间"> <!-- jQuery 必须在插件脚本之前 --> <script src="./assets/jquery.min.js"></script> <script src="./assets/datetimepicker.js"></script> <script> // DOM 就绪后初始化,避免找不到 input jQuery(function($){ $('#startTime').datetimepicker({ format: 'Y-m-d H:i', timepicker: true, step: 15 }); }); </script> </body> </html>这段代码的逻辑很直白:head 里引样式,body 末尾先引 jQuery 再引插件,DOM 就绪后对#startTime初始化。参数上,format同时管显示和回填格式,timepicker打开时分选择,step: 15表示时间按 15 分钟走一格。这里没有写版本号,因为不同项目用的 jQuery 大版本不同,以包内示例页能跑通的组合为准。初始化只给一个 input,是为了先排除项目里其他脚本和样式的干扰。
跑通后别急着往下写业务,先在控制台按顺序确认三件事。把下面三行粘进去,输出对不上就停在这一步查。
// 最小页面跑通后,在控制台按顺序确认 console.log('jQuery 版本:', window.jQuery && jQuery.fn.jquery); console.log('插件方法:', typeof jQuery.fn.datetimepicker); console.log('input 值:', jQuery('#startTime').val());第一个输出如果是undefined,说明 jQuery 没引上或者全局被覆盖;第二个不是function,说明插件脚本没注册或加载失败;第三个用来确认回填值有没有被格式化。这三步看起来笨,但比在项目里猜半天快。我一般会先让最小页面单独跑,确认面板能弹、能选、值能回填,再往真实表单里搬。
2.2 最小页面只做三件事:引顺序、给 input、初始化
很多人拿到插件压缩包,第一反应是把脚本和样式全塞进项目布局页,结果日历面板要么不弹,要么弹出来被侧边栏裁掉。问题往往不在插件本身,而在项目里已经有一套复杂的 CSS:box-sizing、overflow、z-index、transform都会影响绝对定位面板。最小页面的价值就是先把这些变量拿掉,只保留“引顺序、给 input、初始化”三件事。
引顺序上,jQuery 只能保留一份。老后台里常见的情况是布局页引了一个 jQuery,某个业务组件又引了一个,后引的会把前一个覆盖掉,插件注册的方法也跟着丢。判断办法不是看页面里写了几行<script>,而是在控制台看jQuery.fn.jquery和$.fn.datetimepicker是否同时存在。如果项目里有noConflict,初始化时用jQuery(function($){})把$限定在局部,别在全局乱挂。
input 的类型用text,不要用date或datetime-local。原生日期控件和插件面板会打架,尤其在 Chrome 里点一下出两个面板,用户选完值还可能被原生控件格式化掉。readonly不是必须,但在移动端很实用;如果业务要求手输,就把readonly去掉,同时在validateOnBlur或表单校验里补格式检查,别让用户绕过面板输入非法值。
初始化时不要一次写十几个参数。先给format、timepicker、step三个,确认面板正常,再逐个加minDate、maxDate、lang、theme。参数加得越快,出问题越难定位。插件初始化后通常会给 input 挂数据或生成面板容器,具体类名以包内为准,别拿网上抄来的类名硬查。
2.3 初始化后看 DOM:面板挂在哪里,样式为什么会被盖住
面板不显示不一定是插件坏了,先看它挂在哪。常见实现会在 input 附近插入一个绝对定位的容器,点 input 时切换显示。你可以在控制台用$('#startTime').next()看它后面多了什么节点,也可以直接搜页面里新增的日历容器。知道面板的父级是谁,才能判断它为什么被裁切。
被盖住的高频原因有三个。第一,父容器有overflow: hidden,面板超出父级高度就被裁掉,表现是只露一条边或者闪一下。第二,父容器有transform,会创建新的层叠上下文,面板的z-index再大也跳不出这个上下文。第三,弹窗或表格的z-index比面板高,面板被压在下面。解决顺序是先看父级overflow,再看transform,最后才调z-index。上来就写!important通常会把别的弹窗搞乱。
如果包内支持把面板挂到 body,优先用这个配置,能绕开大部分裁切问题。不支持的话,把 input 从overflow: hidden的容器里挪出来,或者给容器留出足够高度。调完样式后拿真机看一眼,桌面浏览器缩放 100% 正常不代表手机正常,移动端点开面板的位置和滚动容器关系很大。
3. 参数不是越多越好:format、时间粒度和可选范围怎么落到配置里
3.1 先把 format 和 value 绑死:显示格式与回填格式必须同源
format是这类插件最容易翻车的地方。常见实现用的是 PHP 风格格式符,Y-m-d H:i表示年月日时分,不是 moment 那套YYYY-MM-DD HH:mm。把YYYY直接塞进去,面板上可能真的显示四个字母 Y。更麻烦的是回填:后端给2024-06-01 09:30:00,你的format只到分钟,直接val()进去插件可能解析不了,或者解析成别的日期。
先把常用参数列成一张表,配的时候对着看,别凭记忆写。
| 参数 | 作用 | 常见坑 |
|---|---|---|
format | 显示和回填格式 | 混用 moment 的YYYY会显示字面量 |
timepicker | 是否选时分 | 关掉后format还带H:i会不一致 |
step | 时间分钟步长 | 单位是分钟,不是秒;和allowTimes别混用 |
minDate/maxDate | 日期边界 | 字符串格式依赖解析,优先传 Date 对象 |
minTime/maxTime | 时间边界 | 跨天场景要配合日期判断 |
lang | 语言 | 需要语言包或内置支持,不是写了就生效 |
theme | 主题 | CSS 没引会裸奔 |
todayButton/clearButton | 快捷按钮 | 不同分支参数名有差异,先搜示例页 |
defaultTime | 默认时间 | 只在未选时间时生效 |
validateOnBlur | 失焦校验 | 和表单校验重复时注意触发顺序 |
配置先收在一处,别每个页面各写一套。下面这段只放常用项,路径和语言包按包内实际文件改。
// 把默认配置收在一处,页面只做覆盖 const dateTimeDefaults = { format: 'Y-m-d H:i', // 显示到分钟,和后端格式保持可转换关系 timepicker: true, // 打开时分选择 step: 15, // 时间步长 15 分钟 lang: 'zh', // 需要包内语言包支持 theme: 'default', // 主题名以包内 CSS 为准 minDate: new Date(), // 传 Date 对象,避免字符串解析歧义 defaultTime: '09:00', // 只选日期没选时间时的默认值 todayButton: true, clearButton: true, validateOnBlur: true, closeOnDateSelect: false // 开时间选择时别选完日期就关 }; $('#startTime').datetimepicker(dateTimeDefaults);这段的逻辑是把默认值集中到一个对象里,页面初始化时直接传,后续要改格式或步长只动一处。参数上,minDate用new Date()表示今天起可选,比写字符串稳;defaultTime在用户只点日期没点时间时补一个默认值;closeOnDateSelect关掉,是因为开了时间选择后面板还要留着让用户选时分。lang和theme不是写了就一定生效,语言包和主题 CSS 没引进来,面板还是默认样子。
回填时也要守同一个格式。后端给的是2024-06-01 09:30:00,而format是Y-m-d H:i,就在回填前截到分钟,或者统一转成Y-m-d H:i再塞。不要今天用Y-m-d,明天用Y/m/d,插件解析和表单校验都会跟着乱。我的习惯是把“显示格式”和“提交格式”写成两个函数,显示归插件管,提交归业务管,中间不混。
3.2 minDate、maxDate 和 step:业务边界比日历皮肤重要
日期控件的皮肤可以后面再调,边界一开始就要定。预约场景里开始时间不能选过去,结束时间不能早于开始时间;报表场景里查询范围不能跨年;活动场景里报名截止后日期要灰掉。这些规则不写进配置,用户就能选出业务上非法的组合,后端再拦一次,体验很差。
下面这段用开始时间联动结束时间,选完开始时间后给结束时间设最小边界,并留 15 分钟缓冲。setOptions是常见方法,但不同分支可能叫法不同,先看包内示例页有没有这个方法;没有的话就销毁后重建,或者重新初始化前先清掉旧面板。
const $start = $('#startTime'); const $end = $('#endTime'); $start.datetimepicker({ format: 'Y-m-d H:i', timepicker: true, step: 15, onChangeDateTime: function(current){ if (!current) return; // 结束时间至少比开始时间晚 15 分钟 const minEnd = new Date(current.getTime() + 15 * 60 * 1000); $end.datetimepicker('setOptions', { minDate: minEnd }); } }); $end.datetimepicker({ format: 'Y-m-d H:i', timepicker: true, step: 15 });逻辑是开始时间变化时,用当前选中的 Date 对象算出结束时间的最小值,再通过setOptions推给结束时间控件。参数上,current是插件回调给的 Date 对象,getTime()拿毫秒戳,加 15 分钟再包回 Date。这样结束时间面板里早于这个点的日期或时间就选不了。step只控制时间列的步长,不控制业务缓冲,缓冲要自己算。
step和allowTimes别混着理解。step: 15是把时间按 15 分钟切格,适合大多数预约;allowTimes是直接给一个可选时间列表,适合固定班次,比如只有 09:00、10:30、14:00 三个时段。列表一长,面板滚动和渲染都会变重,移动端尤其明显。能用step解决的,不要上allowTimes。
minDate和maxDate能传 Date 对象就传 Date 对象。字符串写法各分支支持程度不一样,+30d这种相对写法也不是每个包都认。业务边界一旦跨时区或跨天,字符串解析很容易差一天。血泪经验是:边界值先在控制台打印toISOString()和本地格式各看一遍,确认没有偏移再写进配置。
3.3 语言、主题和按钮:别让默认文案进生产
默认英文面板放进中文后台,用户第一眼就会觉得“这是临时拼的”。lang: 'zh'能不能用,取决于包内有没有对应语言包,或者插件本身内置了中文。没有的话,面板上的月份、星期还是英文,不要硬改源码,先看示例页有没有lang目录或语言配置。主题也一样,theme写了但对应 CSS 没引,面板会变成裸样式,按钮和日历格子挤在一起。
快捷按钮要同步隐藏字段。todayButton和clearButton只是面板上的按钮,点了之后 input 的显示值变了,你提交用的隐藏字段不一定跟着变。如果业务用隐藏字段传后端,就在onChangeDateTime里统一同步,或者在提交前从显示值再转一次。清空按钮尤其要测:有的实现清空 input 后不触发change,表单校验还挂着旧值,用户以为清了,提交上去还有。
defaultTime适合“先选日期、时间默认上午”的场景,但它只在用户没选时间时补位。用户手动选了 14:30,又回头改日期,时间通常保留,不会重置回默认值。这个行为要跟业务确认,预约类应用里用户改日期往往希望保留时间,报表类应用可能希望重置。别靠猜,拿一个真实流程点一遍。
4. 接进真实表单:校验、回填、清空和动态行怎么不打架
4.1 显示格式和提交格式分开:别让后端猜你用的是哪种日期
插件管的是显示值,后端要的是提交值,这两个不要共用同一个 input。常见做法是页面上放一个可见 input 给用户看,再放一个隐藏 input 给表单提交。用户看到的是2024-06-01 09:30,后端收到的是2024-06-01 09:30:00,中间用回调转换。这样即使后面改显示格式,提交格式也不受影响。
<form id="bookingForm"> <input id="startTime" type="text" readonly autocomplete="off" placeholder="选择开始时间"> <input type="hidden" id="startTimeValue" name="start_time"> <button type="submit">提交</button> </form> <script> $('#startTime').datetimepicker({ format: 'Y-m-d H:i', timepicker: true, step: 15, onChangeDateTime: function(current, $input){ // 面板选中后同步隐藏字段,提交只认隐藏字段 const value = current ? formatForApi(current) : ''; $('#startTimeValue').val(value); } }); function formatForApi(date){ const pad = n => String(n).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` + `${pad(date.getHours())}:${pad(date.getMinutes())}:00`; } $('#bookingForm').on('submit', function(e){ if (!$('#startTimeValue').val()) { e.preventDefault(); alert('请选择开始时间'); } }); </script>逻辑是可见 input 只负责展示和弹面板,隐藏字段负责提交。onChangeDateTime回调里拿到currentDate 对象,用formatForApi转成后端要的YYYY-MM-DD HH:mm:ss,写进隐藏字段。参数上,current为空表示用户清空了选择,这时隐藏字段也要置空;$input是当前输入框,动态行场景里可以用它定位行内其他字段。提交前只校验隐藏字段,不要拿显示值做字符串比较,显示格式一改校验就废。
formatForApi里没用toISOString(),因为它按 UTC 输出,本地时间 09:30 可能变成 01:30。时区问题放到避坑章细说,这里先记住:提交给后端的字符串要么带明确时区,要么就是本地时间字符串,不要让前端偷偷转 UTC。
4.2 回填与清空:val()、change 和插件状态要一起处理
编辑页回填是第二个高频翻车点。后端给2024-06-01 09:30:00,你直接$('#startTime').val(apiValue),如果format只到分钟,插件可能解析失败,面板打开后日期是空的,或者跳到今天。稳妥做法是先截成显示格式,再触发change,让插件和表单校验都收到通知。
// 回填:显示值截到分钟,隐藏字段保留后端原值 const apiValue = '2024-06-01 09:30:00'; const displayValue = apiValue.slice(0, 16); // Y-m-d H:i $('#startTime').val(displayValue).trigger('change'); $('#startTimeValue').val(apiValue); // 清空:不要 remove input,清 value 并触发 change,让插件状态跟着重置 $('#clearBtn').on('click', function(){ $('#startTime').val('').trigger('change'); $('#startTimeValue').val(''); });这段的逻辑是回填时把后端值拆成显示值和提交值,显示值截到分钟,提交值保留原样。trigger('change')是为了让绑定在 input 上的校验和插件状态更新。清空时不要用remove()删 input,删了插件绑定和事件都没了,后面再显示还得重新初始化。如果包内有clear或reset方法,优先用包内方法,没有再走val('')加change。
slice(0, 16)只适合固定格式YYYY-MM-DD HH:mm:ss。后端如果换格式,这里就断了。更稳的做法是写一个toDisplayValue函数,用 Date 解析后再格式化,解析失败就返回空字符串并打日志。别在回填里用new Date('2024-06-01 09:30:00')直接赌浏览器解析,Safari 对带空格的日期字符串兼容性一直不太好。
4.3 动态行与重复初始化:事件委托、命名空间和销毁顺序
动态表格里加日期行,最容易犯的错是每加一行就对整张表重新初始化。结果是一行 input 挂多个面板,点一次出两个日历,选完值还互相覆盖。正确顺序是只初始化新行,删除行时先销毁插件实例再删 DOM。下面这段用事件委托绑定删除按钮,新增行只对行内 input 初始化。
const rowTpl = ` <tr> <td><input class="js-date" name="date[]" type="text" readonly autocomplete="off"></td> <td><button type="button" class="js-remove">删除</button></td> </tr>`; $('#addRow').on('click', function(){ const $row = $(rowTpl); $('#rows').append($row); // 只初始化新行,避免全表重复初始化 $row.find('.js-date').datetimepicker({ format: 'Y-m-d', timepicker: false }); }); $('#rows').on('click', '.js-remove', function(){ const $input = $(this).closest('tr').find('.js-date'); // 常见实现有 destroy 或 remove;没有就至少解绑后移除 if ($input.data('datetimepicker')) { $input.datetimepicker('destroy'); } $(this).closest('tr').remove(); });逻辑是新增行时只对新行里的.js-date初始化,删除时先判断有没有插件实例,有就调destroy,再删行。参数上,data('datetimepicker')这个键名以包内实现为准,不确定就先在控制台看初始化后 input 上挂了什么数据。destroy方法名也不是每个分支都有,没有的话退化成.off()解绑事件后删除 DOM,至少别把面板容器留在 body 里。
事件委托要绑在稳定的父容器上,比如#rows,不要绑在行内按钮上。行删了,绑在行上的事件也跟着没了,后面新增的行不生效。如果页面里还有别的组件也用change事件,给插件相关事件加命名空间,比如.datetimepicker,删除时按命名空间解绑,避免误伤其他逻辑。
5. 避坑与排查:五类高频翻车记录和一条固定检查链
5.1 先按“引入顺序—初始化—格式化—时区”走一遍检查链
面板不出来,不要先改插件源码。按固定顺序查:引入顺序、初始化、格式化、时区。先把下面几行在控制台跑一遍,输出正常再往下看业务代码。
// 固定检查链:顺序执行,别跳步 console.log('jQuery:', window.jQuery && jQuery.fn.jquery); console.log('插件方法:', typeof jQuery.fn.datetimepicker); console.log('input 数量:', jQuery('.js-date').length); console.log('面板数量:', jQuery('.xdsoft_datetimepicker').length); // 类名以包内为准 console.log('当前值:', jQuery('#startTime').val());第一个输出为空,查 jQuery 引入和全局覆盖;第二个不是function,查插件脚本路径和加载顺序;第三个数量对不上,查选择器是不是写错;第四个面板数量大于 input 数量,基本就是重复初始化;第五个值格式不对,回头查format和回填逻辑。这个检查链看起来简单,但能把大部分“插件坏了”的误判挡掉。
5.2 五类高频翻车记录:从现象倒推原因
第一类,控制台报datetimepicker is not a function。现象是 input 完全没反应,点上去连面板都不闪。原因通常是插件脚本 404、jQuery 在插件之后才引、或者项目里有两个 jQuery 把方法覆盖了。解决顺序是先在 Network 看脚本状态码,再看控制台typeof jQuery.fn.datetimepicker,最后搜页面里所有 jQuery 引入,只留一个。路径大小写也要看,Linux 服务器上DateTimePicker.js和datetimepicker.js是两个文件。
第二类,面板被弹窗或表格遮挡。现象是点 input 后日历闪一下,或者只露出顶部一条边。原因多半是父容器overflow: hidden裁切,或者父级transform创建了新的层叠上下文,面板的z-index再高也出不去。解决时先看父级计算样式,能挪出容器就挪,包内支持挂到 body 就挂 body,最后才调z-index。不要直接给面板写!important,容易把其他弹窗压乱。
第三类,重复初始化导致多个面板和值错乱。现象是点一次出两个日历,选完日期 input 里的值又跳回去。原因是对整张表反复调.datetimepicker(),或者删除行动态时没销毁实例。解决是只初始化新行,删除前调destroy或解绑事件,初始化前用data或类名判断是否已经初始化过。动态行多的页面,面板数量要作为固定检查项。
第四类,
本文还有配套的精品资源,点击获取