news 2026/4/23 17:14:18

Vue日历组件实现农历与节日显示

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue日历组件实现农历与节日显示

Vue日历组件实现农历与节日显示

在旅游预订、假期规划或节庆活动报名这类业务场景中,用户选择日期时往往不只是看公历数字。他们更关心“那天是初几?”、“是不是春节?”或者“清明节放假吗?”。这种需求背后其实是一个典型的本地化交互问题:如何让一个看似简单的日历组件,既能承载现代时间体系,又能体现传统文化的温度?

我们最近在一个出境游产品中就遇到了这个挑战——用户需要选择出发和返回时间,但很多人会下意识地避开农历新年期间,或是特意选在中秋前后出行。如果日历只显示阳历,显然不够友好。于是我们决定从零打造一个支持农历与节日标注的Vue日历组件。

整个实现过程并不复杂,核心在于两点:一是准确的公农历转换算法,二是清晰的状态管理与渲染逻辑。接下来我们就一步步拆解这个组件的设计思路。

技术选型:轻量级 + 零依赖

市面上有不少成熟的日历库,比如element-ui的 DatePicker 或vue2-datepicker,但它们大多对农历支持有限,且定制成本高。我们最终选择了一条更灵活的路径:基于开源项目 jinzhe/vue-calendar 中提取出的calendar.js算法模块,自行封装UI。

为什么选它?

  • 纯函数计算:不依赖任何外部服务,所有逻辑都在前端完成。
  • 高精度农历转换:包含闰月处理、节气推算、干支纪年等完整规则。
  • 体积小:压缩后不足10KB,适合嵌入式使用。

引入方式也很简单:

import calendar from '@/common/js/calendar.js'

关键接口就是这一个方法:

calendar.solar2lunar(2025, 4, 5) // 返回对象包含农历年月日、天干地支、生肖、节气等信息

这套算法虽然没有AI模型那么“聪明”,但它解决的是另一种类型的推理问题——确定性的数学逻辑。就像我们写代码一样,只要规则明确,就能得出唯一正确的结果。

组件结构设计:数据驱动视图

我们创建了一个名为TourDays.vue的单文件组件,整体采用“月份为单位”的滚动布局,一次展示未来六个月的日历数据。

模板结构如下:

<template> <div class="tourDaysBox" ref="dayBox"> <!-- 顶部导航 --> <div class="visaDetailTop"> <span @click="$emit('close')">✕</span> <h3>选择出发时间</h3> </div> <!-- 月份标签栏 --> <ul class="mothBox"> <li v-for="(m, i) in dat" :key="i" @click="scrollToMonth(i)"> {{ m.y }}年{{ m.m + 1 }}月 </li> </ul> <!-- 可滑动日历主体 --> <scroll class="dayScroll" :style="{ height: h + 'px' }"> <div v-for="(item, index) in dat" :key="index" class="itemBox"> <div class="title">{{ item.y }}年{{ item.m + 1 }}月</div> <div class="dayBox"> <ul class="weekBox">日 一 二 三 四 五 六</ul> <ul class="daysBox"> <li v-for="(day, idx) in item.days" :key="idx" :class="computeClasses(day, item, idx)" @click="selDay(item.y, item.m, day, idx)"> <p>{{ formatDayText(day) }}</p> </li> </ul> </div> </div> </scroll> </div> </template>

这里的<scroll>是一个自定义滚动容器,用于解决移动端固定高度下的滚动穿透问题。你可以用better-scroll或原生overflow-y: auto实现。

数据初始化:预计算胜过实时渲染

性能优化的第一原则是:能提前算的,绝不等到渲染时再算。

我们在created()钩子中调用init()方法,一次性生成未来6个月的完整日历数据。这样做的好处是避免在模板中频繁调用solar2lunar()这类耗时操作。

methods: { init() { const now = new Date(); const currentY = now.getFullYear(); const currentM = now.getMonth(); const currentD = now.getDate(); this.dat = []; for (let i = 0; i < 6; i++) { const date = new Date(); const y = date.getFullYear(); let m = date.getMonth() + i; const realY = y + Math.floor(m / 12); m = m % 12; const lastDay = new Date(realY, m + 1, 0).getDate(); const firstWeekDay = new Date(realY, m, 1).getDay(); const days = []; // 补齐前面空白格子(非本月) for (let j = 0; j < firstWeekDay; j++) { days.push({ day: '', flag: -1 }); } // 填充每一天 for (let d = 1; d <= lastDay; d++) { const lunarInfo = this.getLunarInfo(realY, m + 1, d); const isPast = (realY === currentY && m === currentM && d < currentD); const isToday = (realY === currentY && m === currentM && d === currentD); days.push({ day: d, flag: isPast ? -1 : (isToday ? 0 : 1), feast: lunarInfo.isFestival ? lunarInfo.lunar : '', lunar: lunarInfo.lunar }); } this.dat.push({ y: realY, m: m, days: days }); } } }

这里有个细节值得注意:跨年处理。当m = currentM + i超过11时,必须正确进位到下一年。Math.floor(m / 12)m % 12就是用来处理这个边界情况的。

农历与节日识别:哈希查找提升效率

真正的难点来了:怎么判断某一天是不是节日?

我们的策略是分两步走:

  1. 使用calendar.solar2lunar()获取当天的农历信息;
  2. 查表匹配是否为已知节日。

为此我们维护两个映射表:

data() { return { festival: { lunar: { "1-1": "春节", "1-15": "元宵节", "2-2": "龙头节", "5-5": "端午节", "7-7": "七夕节", "7-15": "中元节", "8-15": "中秋节", "9-9": "重阳节" }, gregorian: { "1-1": "元旦", "3-8": "妇女节", "4-5": "清明节", "5-1": "劳动节", "10-1": "国庆节" } }, start: null, end: null, s: '', // 选中项标识符 e: '', // 结束项标识符 dat: [], h: 0, monthTxt: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'] } }

然后编写getLunarInfo方法进行判断:

getLunarInfo(y, m, d) { const lunar = calendar.solar2lunar(y, m, d); let lunarValue = lunar.IDayCn; // 默认显示“初五”、“廿三”等 let isFestival = false; // 匹配农历节日 const lunarKey = lunar.lMonth + '-' + lunar.lDay; if (this.festival.lunar[lunarKey]) { lunarValue = this.festival.lunar[lunarKey]; isFestival = true; } // 匹配公历节日 else if (this.festival.gregorian[m + '-' + d]) { lunarValue = this.festival.gregorian[m + '-' + d]; isFestival = true; } return { lunar: lunarValue, isFestival }; }

注意这里用了字符串拼接作为键名,比如"5-5"对应端午节。这种方式比遍历数组快得多,属于典型的以空间换时间。

渲染优化:动态样式绑定的艺术

为了让用户一眼看出哪些日子可点、哪些已选、哪些是节日,我们需要一套清晰的视觉反馈系统。

我们将日期分为几种状态:

状态样式类视觉表现
已过日期.txtColor灰色文字,不可点击
今天.txtBg浅灰背景,圆角边框
可选日期——正常黑色字体
选中日期.clickColor橙色背景+白色文字
区间内日期.bg淡橙背景+橙色文字

这些样式通过 Vue 的动态:class绑定来控制:

<li :class="[ items.flag === -1 ? 'txtColor' : '', isInSelectedRange(item.y, item.m, items.day) ? 'bg' : '', isSelectedDay(item.m, index) ? 'clickColor' : '' ]"> <p :class="{ txtColor: items.flag === -1, txtBg: items.flag === 0, clickColor: isSelectedDay(item.m, index) }"> {{ items.flag === 0 ? '今天' : (items.feast || items.day) }} </p> </li>

其中isInSelectedRangeisSelectedDay是辅助方法:

methods: { isSelectedDay(monthIndex, dayIndex) { const key = this.monthTxt[monthIndex] + dayIndex; return this.s === key || this.e === key; }, isInSelectedRange(year, month, day) { if (!this.start || !this.end) return false; const targetTime = new Date(year, month, day).getTime(); const startTime = new Date(this.start.y, this.start.m, this.start.day).getTime(); const endTime = new Date(this.end.y, this.end.m, this.end.day).getTime(); return targetTime >= startTime && targetTime <= endTime; }, formatDayText(day) { return day.flag === 0 ? '今天' : (day.feast || day.day); } }

这种将逻辑抽离成独立方法的做法,不仅提高了可读性,也便于后续测试和维护。

交互设计:双击选择行程区间

我们支持类似酒店预订的“入住-离店”模式:第一次点击设为出发日,第二次设为返回日。如果已经选择了两个日期,再次点击则重新开始。

selDay(y, m, val, index) { if (val.flag === -1) return; // 禁选过去日期 const key = this.monthTxt[m] + index; if (!this.s) { // 第一次选择出发时间 this.s = key; this.start = { y, m, day: val.day }; } else if (!this.e) { // 第二次选择返回时间 this.e = key; this.end = { y, m, day: val.day }; this.judgeDays(); // 自动校正顺序 } else { // 已有完整区间,重新选择出发时间 this.s = key; this.start = { y, m, day: val.day }; this.e = ''; this.end = {}; } }

其中judgeDays()会自动判断并交换起止时间,确保出发日在返回日之前:

judgeDays() { if (!this.start || !this.end) return; const startTime = new Date(this.start.y, this.start.m, this.start.day).getTime(); const endTime = new Date(this.end.y, this.end.m, this.end.day).getTime(); if (startTime > endTime) { [this.start, this.end] = [this.end, this.start]; [this.s, this.e] = [this.e, this.s]; } }

这种“无感纠偏”设计能有效减少用户误操作带来的困扰。

样式适配:移动端优先

移动端屏幕有限,我们必须合理利用空间。每周七天采用flex布局,每项宽度设为14.2857%(即 100%/7),保证整齐排列。

.weekBox li, .daysBox li { float: left; width: 14.2857%; text-align: center; } .txtColor { color: #b1b1b1; } .txtBg { background: #eee; border-radius: 4px; padding: 2px 0; } .clickColor { background: #fc6d23; color: #fff; border-radius: 4px; } .bg { background: #fdb691; color: #fc6d23; }

高度方面,通过mounted阶段动态计算可用区域:

mounted() { this.h = this.$refs.dayBox.offsetHeight - 68; // 减去顶部导航高度 }

这样即使页面布局变化,也能自适应填充剩余空间。

扩展建议:不止于节日

当前功能已经满足基本需求,但如果想进一步增强实用性,还可以加入以下特性:

节气标注

const term = calendar.solar2lunar(y, m, d).Term; if (term) { lunarValue = term; // 如“立春”、“谷雨” isFestival = true; }

生肖提示

const animal = calendar.getAnimal(year); // 返回“龙”、“蛇”等

干支纪年

const ganzhi = calendar.toGanZhiYear(year); // 返回“甲辰年”

这些都可以作为 hover 提示或小图标展示,既丰富信息又不干扰主视觉。

总结:小而美的工程实践

这个日历组件看起来只是多了几行中文,但背后涉及的知识点却不少:

  • 公农历转换的非线性周期算法;
  • 节日匹配的哈希查找优化;
  • 时间区间的边界条件处理;
  • 移动端滚动与样式的兼容性适配。

它不像大模型那样炫技,但却体现了前端开发的本质:用最小的成本,解决最实际的问题。正如一个好的算法,不一定需要庞大的参数量,只要逻辑严谨、设计精巧,就能在特定场景下发挥巨大价值。

如果你也在做类似的本地化功能,不妨试试这条路:不要盲目追求全功能库,有时候自己动手封装一层,反而更轻便、更可控。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 14:07:48

发现并分析一个PHP木马后门

发现并分析一个PHP木马后门 在一次常规的AI模型部署测试中&#xff0c;我们拉取了社区广泛推荐的 GLM-4.6V-Flash-WEB 开源视觉模型镜像。整个流程堪称“丝滑”&#xff1a;一键部署、脚本运行、网页推理&#xff0c;三步完成多模态能力上线。然而就在系统上线前的安全扫描环节…

作者头像 李华
网站建设 2026/4/23 12:47:03

差分隐私优化端到端语音识别技术

更好的端到端语音识别差分隐私技术 教师模型集成私有聚合技术相对于标准差分隐私方法&#xff0c;可将词错误率相对降低超过26%。 现代人工智能模型&#xff0c;如图像和语音识别模型&#xff0c;高度依赖数据。虽然有一些公共数据集可用于训练此类模型&#xff0c;但从实时运行…

作者头像 李华
网站建设 2026/4/22 9:40:42

AGV系统基础知识与应用详解

AGV系统基础知识与应用详解 在现代智能工厂的车间里&#xff0c;你可能会看到这样一幕&#xff1a;没有司机驾驶&#xff0c;一辆辆小巧灵活的运输车沿着既定路线穿梭于产线之间&#xff0c;精准地将物料送到工位旁&#xff0c;完成对接后自动返回。它们彼此避让、有序通行&…

作者头像 李华
网站建设 2026/4/23 9:17:30

数信云X3高性能版服务器

工欲善其事 必先利其器 生信人算力buff已加载&#xff0c;数信云提供多种场景共享服务器。新手友好X1&#xff1a;1197元/年 48线程512G内存&#xff0c;生信入门/小样本分析轻松拿捏&#xff0c;学生党闭眼冲~进阶选手X2&#xff1a;1597元/年 112线程4060GPU&…

作者头像 李华
网站建设 2026/4/23 9:16:59

鸿蒙学习实战之路-Core Vision Kit人脸检测实现指南

鸿蒙学习实战之路-Core Vision Kit人脸检测实现指南 Core Vision Kit&#xff08;基础视觉服务&#xff09;提供了机器视觉相关的基础能力&#xff0c;什么意思呢&#xff1f;通俗点说&#xff0c;就是让你的鸿蒙应用"长一双眼睛"——能看懂图片里的内容是人脸还是文…

作者头像 李华