做 uni-app 开发这几年uni-calendar 是我在项目里用得最多的官方扩展组件之一。平时弹窗选个单日、在页面上展示农历信息它都能应付。但前阵子产品提了个需求日历打开就要默认选中一段日期范围比如显示本周一到今天、上个月到本月这种连续区间。我翻了半天文档发现 uni-calendar 的 selected 属性只支持传入日期数组做高亮并没有一个现成的起始日期 结束日期配置入口。起初我想绕开它自己写一个日历后来仔细翻了源码发现只要弄清它的选中机制这个需求完全可以通过组件扩展实现甚至用不着重写。这篇文章我会从 uni-calendar 的选中机制讲起给出两个层级的解决方案第一层是不改源码、靠 selected 数组实现默认范围高亮第二层是直接改源码给组件加上真正的范围选择能力。后面还会补上跨月、反选、重复选择这些边界情况的处理以及二次封装成业务组件的思路。适合已经会用 uni-calendar 基础功能、想在它之上做定制开发的兄弟参考。1. uni-calendar 的选中机制先吃透 selected 和 dateClick1.1 selected 数组所有高亮状态的唯一来源uni-calendar 的源码放在 uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue 里实际渲染时每个日期格子都是一个 uni-calendar-item 子组件。这些子组件接收父组件传下来的 selected 数组然后做一次查找当前格子的日期字符串在不在数组里在就渲染高亮样式。翻一下 uni-calendar-item 内部的逻辑核心大概是这样的// uni-calendar-item.vue 内部 isSelected() { const date this.year - this.month - this.date return this.selected.some(item item.date date) }也就是说selected 是整个组件唯一的高亮信号源。任何日期只要它的日期字符串存在于 selected 数组中它就会被标成选中态。这个机制决定了默认选中日期范围这件事的本质把范围内的每一天都生成一个选中项塞进 selected 数组里范围就会完整显示出来。这里有两个细节容易踩坑。第一selected 数组项的 date 字段格式必须和组件内部拼接出来的日期字符串格式一致。很多版本直接拼${year}-${month}-${date}如果 year、month、date 没有补前导零生成的是 2024-6-1 这种格式而你手动塞的是 2024-06-01两边就匹配不上高亮怎么都不出来。第二selected 数组项除了 date 还可以带 info 字段这个字段会渲染在日期格子上官方 demo 里那种小圆点上的文字就是它。info 不写也能高亮但不同版本对它的处理有差异写上去更保险。1.2 dateClick 到 change 事件之间发生了什么选中态不是凭空产生的它来自用户点击某个日期触发的 dateClick 处理函数。这个函数在 uni-calendar.vue 的 methods 里点击日期格子后经过一番逻辑处理然后 emit 一个 change 事件。典型实现长这样dateClick(item) { if (this.lunar) { item this.getLunarDate(item) } if (!this.checkDateShow(item)) return this.$emit(change, { year: item.year, month: item.month, date: item.date, checked: item.checked, dateTime: item.year - item.month - item.date, lunar: item.isLunar ? item.lunar : null }) }change 事件带出来的对象里year、month、date 是用户点的年、月、日dateTime 是拼好的日期字符串checked 表示该日期当前是不是选中态。不同版本的 uni-calendar 返回的字段有差异有些版本还新增了 range、activeDate 之类的字段所以拿到之后第一件事就是用 console.log 打出来看一眼别想当然直接依赖 e.dateTime。搞清楚这个链路之后后续方案就清晰了如果只想展示默认范围初始化时往 selected 里塞日期数组就行如果想让用户通过点击首尾两天重新选范围就得接管或修改 dateClick 内部的逻辑。1.3 想让默认范围显示出来本质上要做什么一个完整的默认选中日期范围需求实际上由三件事组成第一生成默认范围内所有日期的字符串列表构造 selected 数组第二让日历初始显示到默认范围所在的月份而不是永远停在当前月第三如果交互上允许用户重新选范围还需要实现点击首尾两端的范围选择逻辑。很多人在网上搜到的基本只有第一步的代码做完发现只有高亮日历还停在当前月份或者用户一点日期selected 被覆盖了。原因就是第二、三步没做。下面我会把这三件事分别讲透。2. 零改动方案页面里用 selected 拼出默认范围2.1 生成连续日期数组的工具函数第一步先解决范围内所有日期字符串的生成问题。注意日期字符串的解析不能直接写new Date(2024-06-01)在 iOS 的 JavaScriptCore 和部分小程序环境里这种带横杠的字符串会被解析成 Invalid Date整个逻辑直接崩掉。更稳妥的方式是把横杠替换成斜杠或者干脆用年、月、日分别传入构造函数export function generateDateRange(startStr, endStr) { const list [] const startArr startStr.split(-).map(Number) const endArr endStr.split(-).map(Number) const current new Date(startArr[0], startArr[1] - 1, startArr[2]) const endTime new Date(endArr[0], endArr[1] - 1, endArr[2]).getTime() while (current.getTime() endTime) { const y current.getFullYear() const m String(current.getMonth() 1).padStart(2, 0) const d String(current.getDate()).padStart(2, 0) list.push({ date: ${y}-${m}-${d}, info: 已选 }) current.setDate(current.getDate() 1) } return list }这里有两个细节必须说清楚。第一我统一输出带前导零的 yyyy-MM-dd 格式如果你的 uni-calendar 内部拼接的日期字符串不带零是 2024-6-1 这种那就把 padStart 去掉或者改组件内部拼接逻辑反正两边必须保持一致。第二循环里用的是current.setDate(current.getDate() 1)不是每次加 86400000 毫秒。跨月末附近时按毫秒累加在某些端上会出现日期跳一天的问题用 setDate 是最没有歧义的。2.2 onLoad 初始化时注入默认范围有了生成函数页面逻辑就很简单了。假设需求是进页面默认选中本周一到今天import { generateDateRange } from /utils/date.js export default { data() { return { selectedList: [] } }, onLoad() { const start 2024-12-02 // 周一 const end 2024-12-06 // 今天 this.selectedList generateDateRange(start, end) }, methods: { handleCalendarChange(e) { console.log(用户点击了日期, e) } } }模板里直接绑定uni-calendar :selectedselectedList changehandleCalendarChange /这一步跑通之后默认范围内所有日期都会在日历上显示为选中态。我实测下来这个方案在 H5、微信小程序、App 端表现一致因为 selected 只是传给子组件的数组组件内部逻辑全程是同步的。但要注意这种写法只做了高亮展示。用户点击日期后uni-calendar 内部并不会因为 selected 里有这段范围就自动把点击行为变成范围选择它还是单选。如果产品只要求进来能看到默认范围到这里就够了。2.3 这个方案的三个明显局限先别急着高兴零改动方案的局限性也很明显遇到以下情况就得考虑动源码或者做二次封装能力零改动方案源码改造方案默认日期高亮支持支持初始月份自动定位不支持支持首尾范围交互不支持支持起止样式区分较难实现配合修改 item 可做改动范围页面代码组件源码 页面代码适用场景展示型日历交互型日历局限一初始月份不会自动定位。uni-calendar 的 now 初始化用的是 new Date()打开永远显示当前月。如果你的默认范围在 8 月而现在是 12 月打开日历看到的是 12 月空荡荡没有任何高亮用户得手动往回翻四个月才能看到默认范围。这一步靠 selected 是解决不了的。局限二没有范围选择交互。点击一个日期只高亮当天不会自动把从 A 到 B的连续区间选出来。想实现类似选火车票那种点起点、再点终点的交互还是得改组件。局限三样式无法区分起止。默认高亮样式是日期数字下方一个小圆点范围中间和起止日期长得一模一样用户看不出范围边界在哪里。3. 源码改造给 uni-calendar 增加真正的范围选择能力3.1 新增 rangeMode 和 initialDate 两个属性先明确改造对象uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue。动手前建议把整个 uni-calendar 目录复制一份到项目的 components 目录下改名引用避免以后在插件市场点更新把修改覆盖。我自己就吃过这个亏改完没复制晚上有人升级插件第二天整个日历变回原版范围选择功能消失。在 props 里加两个字段props: { // ... 原有 props rangeMode: { type: Boolean, default: false }, initialDate: { type: String, default: } }rangeMode 用来标识当前是否启用范围选择模式initialDate 用来告诉组件初始显示到哪个月份。这样加属性而不是直接改死逻辑既能满足新需求也不影响项目里其他页面继续使用默认行为。3.2 日历初始月份定位到 initialDate找到组件 data 里 now 的初始化位置典型代码是data() { return { now: new Date() } }改成data() { let now new Date() if (this.initialDate) { const tmp new Date(this.initialDate.replace(/-/g, /)) if (!isNaN(tmp.getTime())) { now new Date(tmp.getFullYear(), tmp.getMonth(), 1) } } return { now } }这里把 now 设为 initialDate 所在月的第一天是为了让日历统一从该月 1 号开始渲染。注意组件里可能还有 init() 方法会根据 now 生成日期数据不同版本的调用时机不同有的在 created 里调用有的 watch now。改完 data 之后再去确认一下 init() 有没有被正确执行如果组件初始没有自动生成日期数据手动在 created 里补一次调用。有一个更省事的替代做法不改 data 初始化而是在 created 里判断 initialDate 后直接调用 prev 或 next 跳到目标月份。但那需要计算两个月份之间的跨度代码反而更绕不如直接从源头把 now 改掉。3.3 dateClick 里的两段式范围选择逻辑接下来是核心部分改造 dateClick让它在 rangeMode 下表现成范围选择。逻辑用简单的状态机管理状态一还没有开始日期或者已经选完一整段范围start 和 end 都有值。此时点击任意日期把它作为新的开始日期。状态二已经有开始日期、还没有结束日期。此时点击任意日期把它作为结束日期如果结束日期早于开始日期自动交换两者。对应的代码dateClick(item) { if (this.lunar) { item this.getLunarDate(item) } if (!this.checkDateShow(item)) return if (this.rangeMode) { const currentDate this.formatDate(item) if (!this.rangeStart || (this.rangeStart this.rangeEnd)) { // 新一轮选择把当前点击作为开始日期 this.rangeStart currentDate this.rangeEnd this.mergedSelected [{ date: currentDate, info: 开始 }] } else { // 已有开始日期把当前点击作为结束日期 let start this.rangeStart let end currentDate if (end start) { const temp start start end end temp } this.rangeStart start this.rangeEnd end this.mergedSelected generateDateRange(start, end) } this.$emit(change, { start: this.rangeStart, end: this.rangeEnd, selected: this.mergedSelected }) return } // 原有单选逻辑 }这里有几个关键点要展开。第一日期字符串之所以可以直接用 和 比较是因为它们都是标准的 yyyy-MM-dd 定长格式字典序就是时间序不需要转成时间戳再比。第二end start 时直接交换用户先点后面的日期再点前面的日期也能得到正确区间不需要额外报错。第三代码里的 formatDate 需要自己封装用来给 item 里的 year、month、date 做前导零补齐因为 item 的 date 字段可能不带零直接拼会得到 2024-12-6 这种和 generateDateRange 输出的格式对不上formatDate(item) { return ${item.year}-${String(item.month).padStart(2, 0)}-${String(item.date).padStart(2, 0)} }另外组件顶部要记得引入 generateDateRangeimport { generateDateRange } from /utils/date.js3.4 内部 selected 与 props.selected 的合并策略不能在 dateClick 里直接给 this.selected 赋值因为 selected 是 prop子组件直接改 prop 虽然能跑但会触发 Vue 告警在 Vue3 严格模式下还可能失效。稳妥做法是在 data 里新增一个 mergedSelected把它作为传给日期格子组件的最终数据。data() { return { rangeStart: , rangeEnd: , mergedSelected: [] } }, watch: { selected: { handler(val) { if (this.rangeMode (this.rangeStart || this.rangeEnd)) return this.mergedSelected val || [] }, immediate: true } }模板里把传给 uni-calendar-item 的 selected 改成 mergedSelecteduni-calendar-item v-for(item, index) in weeks :keyindex :weeksitem :selectedmergedSelected :lunarlunar changedateClick /这个 watch 的逻辑是初始化时外部传进来默认 selectedmergedSelected 先变成默认范围高亮用户一旦进入交互rangeStart 或 rangeEnd 被赋值之后外部 selected 的变化就不会再覆盖用户当前的选择状态。这样在 rangeMode 下组件内部维护自己的选中数组不污染外部传入的 selected非 rangeMode 下mergedSelected 又和外部 selected 保持一致。完整的改动流程走下来本月的范围选择已经能正常工作了。但距离能上线还有一段路下面专门讲边界情况。4. 边界情况排查跨月、反选、重复选择4.1 跨月范围的显示与月份跳转范围选择最容易出问题的场景是跨月。假设用户选了 10 月 25 日到 11 月 5 日日历一次只显示一个月用户看到的是 10 月的 25 到 31 号高亮翻到 11 月才看到 1 到 5 号高亮。这不算 bug但确认范围时体验确实费劲。新版 uni-calendar 支持 range 属性可以双月并排显示直接在组件上加 range 属性就能同时看到两个月处理跨月范围直观很多。我这里说的是基于新版组件结构的实践如果你的版本不支持建议先升级插件再继续。升级之后组件上可能还会多出一些和 range 相关的字段注意打印检查一下避免属性名冲突。如果你的默认范围是固定的我建议把 initialDate 设置为范围开始日期所在月份用户进来第一眼看到的是起点再往后翻一个月看到终点整体更符合直觉。如果是动态范围就在每次范围变化后把 view 月份切换到 start 所在月。4.2 反选自动交换先点 11 月 5 日再点 10 月 25 日按正常业务逻辑应该得到 10 月 25 日到 11 月 5 日而不是报错结束日期早于开始日期。这个在上一章的代码里已经通过交换变量处理了。有一个容易忽略的点交换之后mergedSelected 生成的范围要重新调用 generateDateRange而不是把之前的 selected 倒序。因为倒序数组里的日期字符串还是原来从大到小排列的传给组件做 includes 判断时虽然数量对但如果有后续逻辑依赖数组顺序比如某些版本渲染动画按数组顺序播放就可能出问题。统一姿势就是始终用新的 start 和 end 重新生成。4.3 已选中状态下的重新选择逻辑范围选好之后用户想重新选一段直接点任意日期状态机判断 rangeStart 和 rangeEnd 都有值于是把新点的日期作为新的开始日期旧的 start/end 清空selected 只剩一个新点的高亮。这个行为在状态机里自然成立但有一个交互上的坑uni-calendar 原生的 checked 逻辑会认为新点的日期不在 selected 里把它标成未选中状态而你同时在内部把它设为了新的开始日期视觉上就会出现点了一个日期日历上没有高亮反馈的瞬间。解决方式是给 start 状态也生成一个选中项也就是在 3.3 代码里写的mergedSelected [{ date: currentDate, info: 开始 }]。这个细节非常容易被漏掉漏掉的后果是用户第一次点日期时日历毫无反应交互感特别差。4.4 用 start、end、disablePast 限制可选区域业务里往往不是全月随便选。比如机票场景只能选未来 30 天酒店场景只能选未来 90 天。uni-calendar 原生提供 start、end 两个属性限定可选范围还提供 disablePast 禁止今天之前的日期。这些属性和范围选择逻辑可以叠加使用。dateClick 里的 checkDateShow(item) 就是干这个的我在改造时保留了对它的调用。也就是说当用户点了超出可选范围的日期根本走不到范围选择逻辑直接 return 掉了。但要注意如果产品给的可选范围和默认选中范围存在交集问题比如默认范围开始日期在可选范围之前进来第一天就会出现高亮和可选态不一致的情况。这种情况需要在初始化时对默认范围做一次裁剪或者要求产品把默认范围圈在可选范围内。5. 封装成业务组件从能用走向好维护5.1 对外接口设计源码级的范围选择能力已经具备但如果项目里多个页面要用直接复制这段改动很痛苦。我建议再包一层业务组件把默认范围、可选范围、禁用项这些参数化对外暴露最小化的接口。我的 RangeCalendar 组件大概长这样template view classrange-calendar view classrange-calendar__header text classrange-calendar__label{{ startText || 开始日期 }}/text text classrange-calendar__arrow至/text text classrange-calendar__label{{ endText || 结束日期 }}/text /view uni-calendar :range-modetrue :initial-datestartText :selectedselected :startminDate :endmaxDate :disable-pastdisablePast changehandleRangeChange / /view /templateprops 设计成一张表属性类型默认值说明startTextString默认范围开始日期格式 yyyy-MM-ddendTextString默认范围结束日期格式 yyyy-MM-ddminDateString可选最小日期maxDateString可选最大日期disablePastBooleanfalse是否禁用今天之前的日期change 事件把选择结果抛给父级。组件内部把初始 selected 生成好页面调用方完全不需要关心组件内部是改过源码的 uni-calendar只需要传默认起始和结束日期、监听结果就行。generateDateRange 这个工具函数建议放在统一的 utils 里不要每个业务页面都复制一份后面维护唯一入口会省很多事。5.2 样式定制区分起止日期和区间日期范围选择真正落地时视觉上的区分是逃不掉的。默认的绿色小圆点只能表达被选中无法表达这是起点或这是终点。要改样式趁早规划。最简单的做法是通过 CSS 深度选择器覆盖 uni-calendar-item 的选中样式::v-deep .uni-calendar-item__box--selected { background-color: #007aff; color: #ffffff; border-radius: 50%; }如果想要起止日期和中间日期长得不同需要在 generateDateRange 生成 selected 数组时给不同位置的项加上范围角色标记list.push({ date: dateStr, info: dateStr start ? 起 : (dateStr end ? 止 : ), rangeType: dateStr start ? start : (dateStr end ? end : middle) })然后在 uni-calendar-item 的模板里根据 rangeType 追加不同 class。注意这一步意味着你得把 uni-calendar-item 也复制到本地因为 uni_modules 目录下的代码改动同样可能被插件市场更新覆盖。5.3 复盘中容易踩的坑最后把我自己做这个改造时踩过的坑集中列一下给准备动手的兄弟们提个醒。第一是版本差异。uni-calendar 在 HBuilderX 插件市场和 npm 上都有版本演进过程中 change 事件返回的字段、selected 数组支持的字段、是否内置 range 属性都不一样。网上的改造代码大多是针对某个具体版本的直接抄过来大概率编译报错。拿到代码后先把组件版本号记下来再对照着自己的源码改。第二是日期格式一致性。我在前面反复强调组件内部拼接的日期字符串可能不带前导零。在不同版本之间切换使用的时候最稳妥的办法是统一走 formatDate 函数把组件内部生成的日期字符串和外部传入的 selected 都交给它做格式化保证两边永远在一个频道上。第三是 App 端和 H5 端的差异。H5 上 new Date(2024-06-01) 能正常解析但 App 端和小程序端不一定。所有涉及日期字符串解析的地方统一用 replace(/-/g, /) 之后再处理习惯成自然能避免很多疑难 bug。第四是 uni_modules 更新的覆盖问题。这个前面提过但值得再说一遍直接在 uni_modules 里改源码每次插件更新你的改动可能全没了。要么复制出来要么启动时检测版本差异然后自动 patch。小团队复制出来改是最省心的。按我个人经验遇到日历默认选中日期范围这类需求不要一上来就改源码。先问产品一个关键问题用户需不需要在这个日历上重新选择范围如果只需要展示默认范围零改动方案十分钟就能搞定如果真的要交互式选范围再动手改 dateClick 和初始化月份。改的时候记住两个核心原则一是把日期格式统一到唯一标准二是把源码改动约束在 uni-calendar 组件内部对外通过 props 和事件暴露方便后续封装复用。最后再多说一句调试技巧拿到任何版本的 uni-calendar先在 dateClick 里 console.log 打出来事件对象看清楚字段结构再动手这一步能帮你少走八成弯路。
企业数字化 ERP 产品动态
相关推荐
定时混沌实验架构与测试实践:自动化故障注入提升系统稳定性 凌晨2点17分,手机在床头柜上震了三下。我眯着眼摸过来一看,不是报警电话,是告警群:支付网关超时率达到8%。打开电脑连上跳板机,查了二十分钟才发现,数据库连接池被一个异常流量高峰打满了,而那个… · 2026/9/24 22:45:57
老照片修复开源项目实战:从源码到Web应用的全流程指南 简介:基于深度学习的完整老照片修复项目,压缩包内提供全套Python源码与可直接运行的Web交互页面,适合计算机、电子信息等专业学生用于课程设计、期末大作业或毕业设计参考,也适合图像处理爱好者自学实践。资源共20个文件ÿ… · 2026/9/24 22:45:57
ST-GCN骨骼动作识别:从原理到PyTorch实现与踩坑经验 简介:一份基于时空图卷积网络(ST-GCN)的骨骼动作识别Python毕业设计项目,涵盖完整源代码、训练好的模型与项目文档,适合计算机视觉方向的高校毕业生、研究生及动作识别初学者参考。项目以人体骨骼关键点为输入… · 2026/9/24 22:45:50
视频剪辑素材网站怎么选?从版权到检索的实操指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:41:56
ZXCA:手写X.509证书链的Python可信CA工具 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:41:56
Astron Agent 快速开始:用 Docker Compose 两步拉起一套可登录的全链路体验环境 人工智能AI AgentAgent 编排RPA后端前端企业应用 【免费下载链接】astron-agent Enterprise-grade, commercial-friendly agentic workflow platform for building next-generation SuperAgents. 项目地址: https://gitcode.com/gh_mirrors/as/astron-agent 点击查看… · 2026/9/25 1:41:38
PSO优化RBF神经网络实战:手写数字分类与权重导出 简介:本资源是一个基于粒子群优化(PSO)算法自动调优径向基函数(RBF)神经网络的完整MATLAB实现项目,面向机器学习初学者、智能优化算法研究者及RBF网络应用开发者,解决传统RBF网络中隐层中心、宽… · 2026/9/25 1:41:38
ESP32轻量级WASM应用平台实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:41:38
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37