尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Element Plus日期选择器date与datetime切换封装避坑指南

Element Plus日期选择器date与datetime切换封装避坑指南 最近在维护一个后台管理系统时碰到了一个挺常见但又有点烦人的需求列表页的筛选区要支持按日期和按日期时间两种模式查询。用户今天可能只需要按天筛明天可能就要精确到秒去查某一条操作记录。说白了就是要在 Element 的el-date-picker组件上让date和datetime两种类型能随时切换。这个需求听起来简单不就是把type属性从date改成datetime吗如果你真这么干坑马上就来了切换之后 v-model 里的值可能直接清空、回显格式对不上、甚至组件内部面板都不刷新。我花了一下午把这些坑踩完最后封装了一个组件彻底解决问题。这篇就把整个排查过程、实现思路和完整代码分享出来希望能帮你少走弯路。1. 先搞清楚date 和 datetime 到底差在哪1.1 表面上是显示差异实际上是数据模型差异很多人觉得date和datetime只是UI上显示格式不一样一个不显示时分秒一个显示。但实际上在 Element Plus 内部这两种模式对应的数据模型完全不同。typedate时el-date-picker的v-model默认绑定的是一个字符串格式是YYYY-MM-DD比如2024-05-20。而typedatetime时默认绑定的是一个Date对象比如Mon May 20 2024 14:30:00 GMT0800。虽然 Vue 的响应式系统不会因为你切换 type 就自动帮你把字符串转成 Date 或者反过来但组件内部的逻辑却会因为这个差异做出不同的处理。如果你设置了value-format情况还会更微妙。value-format的作用是强制 v-model 的输出为指定格式的字符串。比如你设置了value-formatYYYY-MM-DD即使用 datetime 模式去选拿到的也是2024-05-20这个字符串。但这里就出现一个隐藏问题组件内部初始化、值回显时它是用value-format解析字符串还是用 Date 对象去解析不同版本的 Element 表现不一致这就为切换埋下了雷。打个比方date模式就像存储一个人的生日只需要年月日就够了datetime模式就像记录一条操作日志的时间必须精确到时分秒。你可以在生日上补充一个默认时间00:00:00当成 datetime 用但你不能把一个带分秒的时间直接截断成日期因为那会丢失精度。切换组件时你必须想清楚这两者之间怎么转换才能保证数据不出错。1.2 为什么切换类型容易出 Bug当我第一次直接在同一个el-date-picker上动态改type时遇到的问题非常典型第一切换后 v-model 里的值会消失。比如我先选了2024-05-20date 模式然后切换到 datetime 模式面板打开了但选中状态没了输入框里也是空的。原因是组件内部在 type 变化时会重新初始化面板状态但新初始化的状态里并不包含对旧值的重新解析逻辑值就这么丢了。第二时间格式对不上。如果我在 datetime 模式下产出的是一个完整的时间字符串2024-05-20 14:30:00切换回 date 模式后这个带时分秒的字符串根本没法被 date 模式解析输入框直接显示空。反过来的情况也一样date 模式只有日期没有时间切到 datetime 后虽然能显示日期但时间是空的如果不处理传给后端的值就成了2024-05-20 00:00:00这在某些按时间范围查询的业务里会造成误差。第三禁选逻辑没跟着变。很多搜索场景里有两个日期选择器开始时间和结束时间。结束时间不能早于开始时间。这里的限制如果用disabled-date做基于日期比较没问题但如果切到了 datetime 模式光限制日期不够还得限制时分秒否则用户选了结束日期的 00:00:00看起来没早于开始日期但实际上是同一天的 00:00:00 还是 10:23:45对业务结果影响很大必须用disabled-time做细粒度控制。这些问题单靠组件本身的type属性切换是解决不了的必须从数据层面和渲染层面做额外的同步处理。我在实际项目里的做法是分场景选择方案。如果只是简单展示用的日期选择直接动态改type也没太大问题但只要涉及 v-model 双向绑定、有默认值回显、有联动校验就别偷懒老老实实做一层封装保证切换前后的数据能被正确转换组件状态能正确重建。2. 方案选型三套切换思路两套能落地2.1 方案一同一个 picker 动态改 type先看看很多人第一直觉的写法template el-date-picker v-modelsearchDate :typepickerType :value-formatvalueFormat placeholder请选择日期 / el-button clickswitchType切换类型/el-button /template script setup import { ref, computed } from vue const pickerType ref(date) const searchDate ref() const valueFormat computed(() { return pickerType.value datetime ? YYYY-MM-DD HH:mm:ss : YYYY-MM-DD }) function switchType() { pickerType.value pickerType.value date ? datetime : date } /script这个方案用起来有非常明显的问题。先不说 v-model 值会不会丢单说切换后value-format变了之前存的值格式和新的格式不匹配输入框就显示不出内容。你可能会说那我切换的时候手动把searchDate也转换一下不就行了对是可以。但你又遇到了下一个问题组件内部的面板状态可能还停留在旧的模式下。你切换到 datetime 模式后打开选择面板发现面板还是纯日期的样式没显示时分秒选择区。这是因为el-date-picker的 type 变化虽然触发了属性更新但其内部面板的渲染状态没有完全重新初始化。这种情况在 Element Plus 2.x 的某些版本里表现还不一样有的版本会自动重建有的不会你根本没法控制。所以这个方案只适用于需求非常简单、不要求回显、不要求联动校验的场景。但凡复杂一点我都不建议。2.2 方案二双组件 v-if 切换既然组件内部状态不好控制那就干脆用两个组件通过v-if控制切换。这样两个组件互不影响状态各自独立各管各的。template el-date-picker v-ifpickerType date v-modelsearchDate typedate value-formatYYYY-MM-DD placeholder请选择日期 changehandleDateChange / el-date-picker v-else v-modelsearchDate typedatetime value-formatYYYY-MM-DD HH:mm:ss placeholder请选择日期时间 changehandleDateTimeChange / /template这种做法的好处是代码逻辑简单每个组件自己的行为都是符合文档预期不容易出内部状态残留的问题。而且切换时可以用Vue的nextTick或者直接给组件加key值强制重建绝对干净。但缺点也很突出如果只有一个字段代码重复一点也能忍但如果是一个搜索表单里有五六个日期字段每个都写两套组件模板会变得非常臃肿。再加上每个字段还要考虑传参、校验、禁选范围不同字段之间的联动会让代码跟意大利面条一样维护成本成倍增加。所以这个方案适合字段少、页面独立、以后也不怎么会改的场景。如果你的项目里只有这一个搜索条件用它完全够用。2.3 方案三封装一个支持切分的日期组件既然要复用而且要在多个字段之间保持一致的行为最靠谱的做法是封装一个组件。把切换状态、值转换、格式同步、联动校验、面板重建等逻辑放在组件内部管理外部只需要关心的就是这个组件到底绑定什么字段、要不要支持切换、默认是 date 还是 datetime以及联动校验时怎么限制另一端的可选范围。这是我最终选择的方案也是我认为在这个需求上最合理的做法。封装之后页面代码非常干净切换逻辑统一收敛不会出现五个字段五种写法的局面。封装组件要解决的核心问题有三个第一切换类型时 v-model 值不能丢。需要在 type 变化前把旧值记录下来根据新旧格式做转换再赋值给新组件。第二组件状态必须干净重建。我在实际操作中试过直接在同一个组件上改 type 改 value-format结果面板渲染不正确但给组件加一个:keytype强制让它销毁重建后一切恢复正常。这个技巧非常简单、实用强烈推荐。第三联动校验要跟上模式变化。如果开始时间用了 datetime 模式结束时间的禁用逻辑就不能只看日期还得看时分秒。封装时要把这些情况用 props 暴露出来让调用方可以按需配置。3. 核心实现封装一个可切换日期组件3.1 组件先用起来再谈细节先说这个组件怎么调用。我给它取名叫SwitchableDatePicker调用方式如下template div classfilter-form SwitchableDatePicker v-modelquery.startTime :allow-switchtrue default-typedatetime start-picker :end-valuequery.endTime / span至/span SwitchableDatePicker v-modelquery.endTime :allow-switchtrue default-typedatetime end-picker :start-valuequery.startTime / /div /template这里做了几个约定v-model绑定日期值组件内部统一输出字符串格式date 模式输出YYYY-MM-DDdatetime 模式输出YYYY-MM-DD HH:mm:ss。allow-switch控制是否显示切换按钮。有的字段固定只需要日期不需要切换可以关掉。default-type控制默认使用 date 还是 datetime父组件传一次就行组件内部会保存当前状态。start-picker和end-picker表示当前是开始时间还是结束时间用于联动校验。start-value/end-value接收另一端的值用于限制本组件的可选范围。我建议在实际封装时default-type和当前内部状态用一个 ref 保存。当用户点击切换按钮时内部状态变化同时做值的转换。如果外部要重置这个组件比如点击重置按钮清空搜索条件可以通过watch外部传入的 modelValue 为 null 时同步清空内部状态。3.2 值同步与格式转换封装的灵魂这个组件最关键的代码就是值转换逻辑。我在实现时写了两个函数一个负责把 date 格式转成 datetime一个负责把 datetime 格式转成 date。function dateToDateTime(val) { if (!val) return // 如果传入的是 2024-05-20补上默认时间 const timeStr 00:00:00 // 如果已经有时间部分就直接返回 return val.includes( ) ? val : val timeStr } function dateTimeToDate(val) { if (!val) return // 截取日期部分 return val.split( )[0] }我这里说的是字符串层面的转换后面会加上对 Date 对象的兼容。切换逻辑function handleSwitchType() { const newType currentType.value date ? datetime : date // 转换值 if (modelValue.value) { const nextValue newType datetime ? dateToDateTime(modelValue.value) : dateTimeToDate(modelValue.value) emit(update:modelValue, nextValue) } currentType.value newType }这样父组件的 v-model 值会同步被修改查询条件不会因为切换而丢。如果用户在 date 模式下选了2024-05-20切到 datetime 模式后值会自动变成2024-05-20 00:00:00查询时后端也能接受。如果你使用的是 Element Plus 2.x 且没有加value-formatv-model 绑定的可能是 Date 对象。那转换时要多做一步function dateToDateTime(val) { if (!val) return if (val instanceof Date) { const y val.getFullYear() const m String(val.getMonth() 1).padStart(2, 0) const d String(val.getDate()).padStart(2, 0) return ${y}-${m}-${d} 00:00:00 } return val.includes( ) ? val : val 00:00:00 }我个人建议在项目里统一用value-format让所有日期字段都输出字符串格式这样传参给后端、做比较、存日志都会更方便。3.3 强制重建组件实例前面提到同一个el-date-picker上改 type 会导致面板不刷新的问题。我的解决办法很简单给组件内部使用的el-date-picker加一个动态keykey 的值就是当前 type。这样每次切换类型时Vue 都会强制销毁旧的组件实例创建一个全新的实例。template div classswitchable-date-picker el-date-picker :keycurrentType v-modeldisplayValue :typecurrentType :value-formatcurrentValueFormat :disabled-datedisabledDate :disabled-timedisabledTime :placeholdercurrentType date ? 请选择日期 : 请选择日期时间 changehandleChange / el-button v-ifallowSwitch sizesmall link typeprimary clickhandleSwitchType {{ currentType date ? 切换到时间 : 切换为日期 }} /el-button /div /template这里要注意一点displayValue不能直接用父组件的 v-model否则组件内部在切换时会改到父组件的数据而父组件又可能对数据有额外的逻辑处理容易乱。我在组件内部维护了一个displayValueref初始化时从props.modelValue赋值change事件时 emit 给父组件。重点说明一下为什么加 key 能解决问题。Vue 的 diff 算法在 key 变化时会把旧组件卸载新组件挂载相当于从零开始创建一个全新组件。而el-date-picker初始化的过程里会根据 type 来决定面板渲染哪些区块日期表、时间选择列表以及如何解析初始值。旧组件即使属性更新了内部状态也未必跟着全部刷新所以直接换 key 是最省心、最可靠的方案。这也是我实测下来最稳的做法。3.4 联动校验开始时间不能晚于结束时间搜索热词里有个高频需求el-date-picker判断结束时间大约起始时间。在我的业务里这个场景非常典型用户选择了开始时间那结束时间必须晚于开始时间。如果只是 date 模式用disabled-date就够了。但 datetime 模式必须再叠加一层disabled-time。先说说disabled-date怎么写。它的返回值是 true 时该日期被禁用。比如结束时间的开始值等于2024-05-20那 5 月 20 日之前的日期都不可选const disabledDate (date) { if (!props.startValue) return false const startDate new Date(props.startValue) // 清空时间部分只比较日期 const startDateOnly new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()) return date.getTime() startDateOnly.getTime() }但 datetime 模式下如果用户选择结束日期为2024-05-20他还能选择当天的00:00:00这明显早于开始时间2024-05-20 10:23:45。这时候就需要disabled-time来做小时、分钟、秒级别的禁用。const disabledTime (date) { if (!props.startValue) return { disabledHours: () [], disabledMinutes: () [], disabledSeconds: () [] } const start new Date(props.startValue) const target new Date(date) // 只在同一天时限制时分秒 if (target.getFullYear() start.getFullYear() target.getMonth() start.getMonth() target.getDate() start.getDate()) { const startHour start.getHours() const startMinute start.getMinutes() const startSecond start.getSeconds() return { disabledHours: () { const hours [] for (let i 0; i startHour; i) hours.push(i) return hours }, disabledMinutes: (hour) { if (hour startHour) return Array.from({ length: 60 }, (_, i) i) if (hour startHour) { const minutes [] for (let i 0; i startMinute; i) minutes.push(i) return minutes } return [] }, disabledSeconds: (hour, minute) { if (hour startHour || (hour startHour minute startMinute)) { return Array.from({ length: 60 }, (_, i) i) } if (hour startHour minute startMinute) { const seconds [] for (let i 0; i startSecond; i) seconds.push(i) return seconds } return [] } } } // 不同日期不限制时间 return { disabledHours: () [], disabledMinutes: () [], disabledSeconds: () [] } }这块逻辑看起来长但核心思路很简单先判断日期是否一样日期不一样只限制 date 那一层就够了日期一样才需要在时间上做限制保证目标时间的时间戳大于开始时间。我在实际项目里开始时间的disabled-time逻辑和结束时间是反过来的限制的是当天的23:59:59之后也就是不能选晚于结束时间的时间。实现思路完全对称写一遍之后直接复制改造即可。4. 实战避坑真实项目中踩过的坑和排查思路4.1 切换类型后 v-model 被清空这个是我最初踩的第一个坑。直接在同一个el-date-picker上改type切换过后值就没了。排查时我一开始以为是 change 事件触发了什么特殊逻辑结果打断点发现组件内部在 type 变化时会把选中的日期重置而且不会用旧值做任何补救。解决办法就是我上面说的切换前先把值转成新格式然后 emit 给父组件再加 key 强制重建重建后的组件拿到新值就能正常显示。如果你不想封装组件也要至少保证切换时手动处理一次值格式并且给 picker 加key否则这个问题很难绕过去。4.2 金额相关的日期统计出现 8 小时误差这个坑和日期切换本身无关但容易在 datetime 模式下踩到。如果你用了 datetime 模式但没有设置value-formatv-model 绑定的是 Date 对象。把这个 Date 对象直接传给后端后端用new Date(str)去解析如果涉及到 UTC 和本地时区的转换可能出现 8 小时偏移导致统计数据出错。我当时的现象是选了2024-05-20 00:00:00传给后端查出来的是从2024-05-20 08:00:00开始的数据怎么看都不对。后来排查发现是前后端时区处理不一致。解决的思路有两条一是统一用字符串格式传参也就是设置value-formatYYYY-MM-DD HH:mm:ss后端拿到字符串直接解析不做时区转换二是后端统一存时间戳前端传时间戳由后端统一转成对应时区。我前端项目里统一用了方式一简单直接不涉及时区问题。4.3 Element Plus 2.11.4 表格阴影这个跟日期选择器没有直接关系但我在同一个项目里踩到过发现搜索热词里也有人问就顺便提一句。Element Plus 2.11.4 版本表格偶尔会出现莫名其妙的阴影样式上看像是某些行被覆盖了一层灰色背景。造成这个问题的原因一般是表格列的 fixed 属性和 hover 高亮样式在某些特殊场景下冲突特别是在列宽动态变化、表格数据更新频繁时容易触发。如果你也遇到了可以先看看是不是固定列和滚动条同时存在。临时解决办法是给表格加一个keyprop在数据变化时强制重渲染根治的办法是升级到更新版本Element Plus 2.11.x 之后的小版本修复了不少样式问题。这条经验不属于日期切换的核心逻辑但排查问题时如果同时遇到两个 UI 层 Bug可以先处理优先级高的别让它们互相干扰。4.4 常见问题速查表我把这个需求相关的实际问题整理成了一张表排查时可以对照参考问题现象可能原因解决思路切换 type 后 v-model 值清空组件内部初始化不兼容旧值切换前主动转换值格式加 key 强制重建输入框回显不出值value-format 变化导致格式不匹配统一用字符串格式切换时同步转格式面板不切换到时间选择样式组件内部面板渲染状态未重置给 el-date-picker 加动态 :keytype结束时间早于开始时间仍可选只做了 disabled-date 没做 disabled-timedatetime 模式补上 disabled-time 联动值传给后端时间偏移 8 小时Date 对象时区转换问题设置 value-format 用字符串传参多个日期字段代码冗余每个字段写了两套 picker封装公共组件统一传参切换后值变成了 00:00:00date 转 datetime 时补了默认时间确认业务是否允许默认时间必要时提示用户这张表里最核心的还是前三条本质上是同一个问题type 变了但值格式和组件内部状态没有跟着变。理解了这一点排查其他问题时候就有了大方向。5. 最后再分享两个小技巧我实际做完这个功能之后有几个体会特别深。一个是组件封装时不要过度设计。最初我想把 date、datetime、daterange、datetimerange 四种模式全做进一个组件里后来发现业务上只需要 date 和 datetime 两种多做的 daterange 不仅代码变复杂还容易引入边界问题。最后砍掉多余能力组件反而更稳定。如果你的业务里大概率只会用到某几种模式就只做那几种。另一个技巧是切换交互要加上提示。用户从日期模式切到日期时间模式时如果原来的值被自动补了00:00:00用户可能不知道这个默认时间的存在。我后来在切换的时候通过ElMessage做了个提示比如已切换为日期时间模式未设置时间部分默认为 00:00:00这样用户查询时心里有数不会觉得是系统出了问题。如果你也是在做 Element Plus 后台项目遇到了类似的日期选择器切换需求直接参考上面的思路去封装组件就行。核心原则就三条值格式要统一、组件状态要重建、联动校验要跟模式走。把这三件事处理好这个需求基本就稳了。
返回列表