news 2026/2/25 3:45:43

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/2/20 15:33:52

【Java毕设源码分享】基于springboot+vue的民宿订购平台的设计与实现(程序+文档+代码讲解+一条龙定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/2/21 7:05:16

【Java毕设源码分享】基于springboot+vue的实验室实验报告管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/2/21 2:52:32

【Java毕设源码分享】基于springboot+vue的大学生校园线上招聘系统的设计与实现(程序+文档+代码讲解+一条龙定制)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/2/20 6:09:28

Intel NCS算力棒在Ubuntu16.04的部署指南

Intel NCS算力棒在Ubuntu16.04的部署指南 &#x1f3b5; 零样本语音克隆 情感表达 音素级控制 webUI二次开发 by 科哥 微信&#xff1a;312088415 在边缘计算和轻量化AI推理需求日益增长的今天&#xff0c;Intel Neural Compute Stick&#xff08;NCS&#xff09;作为一款低成…

作者头像 李华
网站建设 2026/2/25 2:23:06

熔融缩聚中影响线型缩聚物分子量的因素

熔融缩聚中影响线型缩聚物分子量的关键因素解析 在高分子材料合成领域&#xff0c;熔融缩聚是制备聚酯、聚酰胺和聚碳酸酯等工程塑料的核心工艺。这类反应无需溶剂&#xff0c;直接在高温熔融状态下进行&#xff0c;通过逐步缩合官能团并脱除小分子副产物&#xff08;如水、甲醇…

作者头像 李华
网站建设 2026/2/24 13:11:06

美团动态线程池,香啊!

「使用线程池 ThreadPoolExecutor 过程中你是否有以下痛点呢&#xff1f;」1.代码中创建了一个 ThreadPoolExecutor&#xff0c;但是不知道那几个核心参数设置多少比较合适2.凭经验设置参数值&#xff0c;上线后发现需要调整&#xff0c;改代码重启服务&#xff0c;非常麻烦3.线…

作者头像 李华