场景
实现日历功能我们一般是直接使用UI库的日历组件就能满足需求,但是如果项目对UI有特殊要求:除了展示基本的日期天数,还需要在日历上展示其他东西,这种情况如果改UI库的日历组件就比较麻烦了,如下案例
在这个项目中,不仅需要展示日历基本的日期时间,还需要展示农历信息和易经八卦(有可能不对,不太懂这个)的信息,常规的UI组件库的日期组件肯定无法满足需求了,自定义改起来也麻烦。
实现效果
单选模式:
多选模式:
选择范围模式:
组件参数
| 参数名 | 描述 | 示例 |
|---|---|---|
| date | 初始化选中的日期 | |
| selectType | 日历选择模式 | 字符串:single 单选 multiple 多选 range 范围 |
| isLunar | 是否显示农历信息 | Boolean |
| range | 限制日期选择范围 | Array:['2025/03/24','2025/10/01'] |
参数介绍
date:
- selectType为single时传入:字符串:'2025/01/01'、'156416156156'(毫秒时间戳)
- selectType为multiple时传入:数组:Array:['2025/03/24','2025/10/01','2025/11/08'] (选择的所有日期)
- selectType为range时传入:数组:Array:['2025/03/24','2025/10/01'] (只传入开始日期和结束日期即可)
封装功能
注意点:
- 使用了dayjs日期时间处理库(懒得自己封装处理日期的函数了🤷♀️)
目前实现的效果就是案例的效果
- 主要功能就是实现日期数据的获取,日期数据是常规的6*7的格式
- 切换月份时有左右位移的动画效果
- 支持手指左右滑动日历来切换月份
- 支持单选日期、多选日期、选择范围日期
日期数据获取正确后,只需要关心样式调整即可
封装代码
<template>
<div class="main-box">
<div class="operation-box">
<div class="operation-box-btn" @click="changeMonth(-1)" style="margin-right: 20rpx"> 《 </div>
<div
class="operation-box-time"
style="margin-right: 20rpx"
@click="openDateTimePicker('year')"
>{{ year }}年</div
>
<div class="operation-box-time" @click="openDateTimePicker('month')">{{ month }}月</div>
<div class="operation-box-btn" @click="changeMonth(1)" style="margin-left: 20rpx"> 》 </div>
<div class="operation-box-today" @click="goToday" v-if="selectType == 'single'">今日</div>
</div>
<div class="week-box">
<div v-for="(item, index) in weekList" :key="index" class="week-item">{{ item }}</div>
</div>
<div
class="day-box"
:class="slideDirection"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
>
<div
:class="[
'day-item',
{
'current-month': day.isCurrentMonth == 'current',
'current-day':
props.selectType === 'single'
? currentDay == day.date
: props.selectType === 'multiple'
? selectedDays.includes(day.date)
: dayjs(day.date).isSame(dayjs(rangeStartDay)) ||
dayjs(day.date).isSame(dayjs(rangeEndDay)),
'range-day':
props.selectType === 'range' &&
rangeStartDay &&
rangeEndDay &&
dayjs(day.date).isAfter(dayjs(rangeStartDay)) &&
dayjs(day.date).isBefore(dayjs(rangeEndDay)),
'range-start':
props.selectType === 'range' && dayjs(day.date).isSame(dayjs(rangeStartDay)),
'range-end': props.selectType === 'range' && dayjs(day.date).isSame(dayjs(rangeEndDay)),
'range-disabled': day.rangeDisabled,
},
]"
v-for="(day, index) in dayList"
:key="index"
@click="clickDay(day)"
>
<div class="day-item-label">
<text>{{ day.label }}</text>
</div>
<div class="day-item-lunar" v-if="props.isLunar">
<text>{{ day.lunar.IDayCn }}</text>
</div>
</div>
</div>
<myDateTimePicker
v-model:timeShow="dateTimePickerShow"
:currentTime="`${year}/${month}/01`"
:type="timeType == 'year' ? '7' : '8'"
@confirm="confirmDateTimePicker"
>
</myDateTimePicker>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch, getCurrentInstance, nextTick } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
const { proxy } = getCurrentInstance();
import myDateTimePicker from './myDateTimePicker.vue';
import dayjs from 'dayjs';
const props = defineProps({
date: {
required: true,
},
// 日历选择模式 single 单选 multiple 多选 range 范围
selectType: {
type: String,
default: 'single',
},
// 是否显示农历信息
isLunar: {
type: Boolean,
default: false,
},
// 限制日期选择范围
range: {
type: Array,
default: [],
},
});
const emit = defineEmits(['clickDay']);
import solarLunar from './solarLunar.js';
onMounted(() => {
if (props.range.length > 0) {
startDate.value = props.range[0];
endDate.value = props.range[1];
}
// console.log(props.date);
let date = '';
if (props.selectType == 'single') {
date = props.date || dayjs().format('YYYY/MM/DD');
currentDay.value = date;
year.value = dayjs(date).year();
month.value = dayjs(date).month() + 1;
} else if (props.selectType == 'multiple') {
date = props.date || [dayjs().format('YYYY/MM/DD')];
selectedDays.value = date;
year.value = dayjs(date[date.length - 1]).year();
month.value = dayjs(date[date.length - 1]).month() + 1;
} else if (props.selectType == 'range') {
date = props.date || [dayjs().format('YYYY/MM/DD')];
rangeStartDay.value = date[0];
if (date.length > 1) {
rangeEndDay.value = date[1];
}
year.value = dayjs(date[date.length - 1]).year();
month.value = dayjs(date[date.length - 1]).month() + 1;
}
getDays(year.value, month.value);
});
const startDate = ref('');
const endDate = ref('');
const weekList = ['一', '二', '三', '四', '五', '六', '日'];
const year = ref();
const month = ref();
const slideDirection = ref(''); // 滑动方向
// 添加触摸相关的变量
const touchStartX = ref(0);
const touchEndX = ref(0);
const minSwipeDistance = 50; // 最小滑动距离,防止误触
// 触摸开始
function handleTouchStart(event) {
touchStartX.value = event.touches[0].clientX;
}
// 触摸结束
function handleTouchEnd(event) {
touchEndX.value = event.changedTouches[0].clientX;
const swipeDistance = touchEndX.value - touchStartX.value;
// 判断滑动距离是否足够
if (Math.abs(swipeDistance) > minSwipeDistance) {
// 向左滑动,月份加1
if (swipeDistance < 0) {
changeMonth(1);
}
// 向右滑动,月份减1
else {
changeMonth(-1);
}
}
}
const timeType = ref('');
const dateTimePickerShow = ref(false);
async function openDateTimePicker(type) {
timeType.value = type;
await nextTick();
dateTimePickerShow.value = true;
}
function confirmDateTimePicker(timestamp, formattedDate) {
// console.log(timestamp, formattedDate);
if (timeType.value == 'year') {
year.value = formattedDate * 1;
month.value = 1;
getDays(year.value, month.value);
} else if (timeType.value == 'month') {
month.value = formattedDate * 1;
getDays(year.value, month.value);
}
}
// 返回今天
function goToday() {
if (dayjs().isSame(dayjs(currentDay.value), 'day') && isCurrentDatePage.value) {
return;
}
const today = dayjs().format('YYYY/MM/DD');
if (isCurrentDatePage.value) {
year.value = dayjs().year();
month.value = dayjs().month() + 1;
if (props.selectType === 'multiple') {
if (!selectedDays.value.includes(today)) {
selectedDays.value.push(today);
}
emit('clickDay', selectedDays.value);
} else if (props.selectType === 'range') {
rangeStartDay.value = today;
rangeEndDay.value = '';
emit('clickDay', {
startDate: rangeStartDay.value,
endDate: rangeEndDay.value,
dates: [rangeStartDay.value],
});
} else {
currentDay.value = today;
emit('clickDay', today);
}
} else {
const time1 = today;
const time2 = dayjs(`${year.value}/${month.value}/01`).format('YYYY/MM/DD');
year.value = dayjs().year();
month.value = dayjs().month() + 1;
if (props.selectType === 'multiple') {
if (!selectedDays.value.includes(today)) {
selectedDays.value.push(today);
}
emit('clickDay', selectedDays.value);
} else if (props.selectType === 'range') {
rangeStartDay.value = today;
rangeEndDay.value = '';
emit('clickDay', {
startDate: rangeStartDay.value,
endDate: rangeEndDay.value,
dates: [rangeStartDay.value],
});
} else {
currentDay.value = today;
emit('clickDay', today);
}
if (dayjs(time1).isAfter(dayjs(time2))) {
slideDirection.value = 'slide-left';
} else {
slideDirection.value = 'slide-right';
}
getDays(year.value, month.value);
// 动画结束后清除类名
setTimeout(() => {
slideDirection.value = '';
}, 500);
}
}
// 改变月份
function changeMonth(num) {
// 设置滑动方向
slideDirection.value = num > 0 ? 'slide-left' : 'slide-right';
if (month.value + num > 12) {
month.value = 1;
year.value = year.value + 1;
} else if (month.value + num < 1) {
month.value = 12;
year.value = year.value - 1;
} else if (month.value + num <= 12 && month.value + num > 0) {
month.value = month.value + num;
}
getDays(year.value, month.value);
// 动画结束后清除类名
setTimeout(() => {
slideDirection.value = '';
}, 500);
}
const dayList = ref([]);
const isCurrentDatePage = ref(false); // 日历是否处于当前日期页面
// 生成日期列表
async function getDays(year, month) {
year = year * 1;
month = month * 1;
// 判断日历是否处于当前日期页面
if (year == dayjs().year() && month == dayjs().month() + 1) {
isCurrentDatePage.value = true;
} else {
isCurrentDatePage.value = false;
}
// console.log('日历是否处于当前日期页面', isCurrentDatePage.value);
// 获取选中月份的第一天是星期几(0-6,周日为0)
let weekDay = dayjs(`${year}-${month}-01`).day();
// 为了符合中国人的习惯,将周日改为7
if (weekDay === 0) {
weekDay = 7;
}
// console.log('选中月份的第一天是星期几', weekDay);
// 获取上个月要展示的天数
let lastMonthDayNum = weekDay - 1;
let lastMonthDays = [];
if (lastMonthDayNum > 0) {
lastMonthDays = getAllDaysInMonth(year, month - 1, 'before').splice(-lastMonthDayNum);
}
// console.log('获取上个月要展示的天数', lastMonthDays);
// 获取当前月份要展示的天数
let currentMonthDays = getAllDaysInMonth(year, month, 'current');
// console.log('获取当前月份要展示的天数', currentMonthDays);
// 获取下个月要展示的天数
let nextMonthDayNum = 42 - lastMonthDays.length - currentMonthDays.length;
let nextMonthDays = getAllDaysInMonth(year, month + 1, 'after').splice(0, nextMonthDayNum);
// console.log('获取下个月要展示的天数', nextMonthDays);
dayList.value = [...lastMonthDays, ...currentMonthDays, ...nextMonthDays];
// console.log('生成日期列表', dayList.value);
}
function getAllDaysInMonth(year, month, isCurrentMonth) {
// 获取当月的总天数
const daysInMonth = dayjs(`${year}-${month}`).daysInMonth();
// 初始化当月的日期数组
const allDays = [];
// 遍历当月的所有天数
for (let i = 1; i <= daysInMonth; i++) {
const day = dayjs(`${year}-${month}-${i}`).format('YYYY/MM/DD');
// 推入每一天到数组中,包含日期和星期几等信息
let info = {
label: dayjs(day).format('DD'), // 展示的日期(日)
date: day, // 完整日期
isCurrentMonth, // 是否属于当月 before:上个月 current:当月 after:下个月
isToday: dayjs(day).isSame(dayjs(), 'day'), // 是否是当天
rangeDisabled: isDateDisabled(day), // 日期是否禁用
};
if (props.isLunar) {
info.lunar = solarLunar.solarStringToLunar(day);
}
allDays.push(info);
}
return allDays;
}
// 判断日期是否在可选范围外
function isDateDisabled(date) {
if (props.range.length === 0) {
return false; // 没有设置范围限制,不禁用
}
const currentDate = dayjs(date);
const startLimit = dayjs(startDate.value);
const endLimit = dayjs(endDate.value);
// 日期在开始日期之前或结束日期之后,则禁用
return currentDate.isBefore(startLimit, 'day') || currentDate.isAfter(endLimit, 'day');
}
const currentDay = ref('');
// 点击日期
function clickDay(day) {
// 如果日期被禁用,不执行任何操作
if (day.rangeDisabled) {
return;
}
if (day.isCurrentMonth == 'before') {
changeMonth(-1);
} else if (day.isCurrentMonth == 'after') {
changeMonth(1);
}
if (props.selectType === 'multiple') {
// 多选模式
const index = selectedDays.value.indexOf(day.date);
if (index > -1) {
// 如果日期已经选中,则取消选中
selectedDays.value.splice(index, 1);
} else {
// 如果日期未选中,则添加到选中数组
selectedDays.value.push(day.date);
}
emit('clickDay', selectedDays.value);
} else if (props.selectType === 'range') {
// 范围选择模式
if (!rangeStartDay.value || (rangeStartDay.value && rangeEndDay.value)) {
// 如果没有开始日期,或者已经选择了一个完整的范围,则重新开始选择
rangeStartDay.value = day.date;
rangeEndDay.value = '';
// 不触发事件,等待选择结束日期
} else {
// 如果已有开始日期,则设置结束日期
if (dayjs(day.date).isBefore(dayjs(rangeStartDay.value))) {
rangeEndDay.value = rangeStartDay.value;
rangeStartDay.value = day.date;
} else {
rangeEndDay.value = day.date;
}
// 获取范围内的所有日期
const dateRange = getDatesBetween(rangeStartDay.value, rangeEndDay.value);
// 只有当有结束日期且日期范围大于等于2天时才触发事件
if (rangeEndDay.value && dateRange.length >= 2) {
emit('clickDay', {
startDate: rangeStartDay.value,
endDate: rangeEndDay.value,
dates: dateRange,
});
}
}
} else {
// 单选模式
currentDay.value = day.date;
emit('clickDay', day.date);
}
}
const selectedDays = ref([]); // 存储多选的日期
const rangeStartDay = ref(''); // 范围开始日期
const rangeEndDay = ref(''); // 范围结束日期
// 获取日期范围内的所有日期
function getDatesBetween(startDate, endDate) {
const dates = [];
let currentDate = dayjs(startDate);
const lastDate = dayjs(endDate);
while (currentDate.isSame(lastDate, 'day') || currentDate.isBefore(lastDate, 'day')) {
dates.push(currentDate.format('YYYY/MM/DD'));
currentDate = currentDate.add(1, 'day');
}
return dates;
}
</script>
<style lang="scss" scoped>
.main-box {
background: transparent;
}
.operation-box {
display: flex;
justify-content: center;
align-items: center;
position: relative;
margin-bottom: 36rpx;
.operation-box-btn {
font-weight: 400;
font-size: 30rpx;
color: #666666;
}
.operation-box-time {
font-weight: 400;
font-size: 30rpx;
color: #666666;
}
.operation-box-today {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
background: #ffffff;
border-radius: 50rpx 0 0 50rpx;
border: 1rpx solid #707070;
padding: 10rpx 16rpx;
display: flex;
justify-content: center;
align-items: center;
font-weight: 400;
font-size: 30rpx;
color: #666666;
}
}
.week-box {
margin-bottom: 30rpx;
display: grid;
grid-template-columns: repeat(7, 1fr);
.week-item {
display: flex;
justify-content: center;
align-items: center;
font-weight: 800;
font-size: 30rpx;
color: #333333;
}
}
.day-box {
transform: translateX(0);
transition: all 0.5s ease-in-out;
touch-action: pan-y pinch-zoom;
user-select: none;
&.slide-left {
animation: slideLeft 0.5s ease-in-out;
}
&.slide-right {
animation: slideRight 0.5s ease-in-out;
}
display: grid;
grid-template-rows: repeat(6, 1fr);
grid-template-columns: repeat(7, 1fr);
margin-bottom: 25rpx;
.day-item {
padding: 20rpx 10rpx;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
background: transparent;
border: 4rpx solid transparent;
.day-item-label {
font-weight: 500;
font-size: 30rpx;
color: #adadad;
}
.day-item-lunar {
margin-top: 6rpx;
font-weight: 400;
font-size: 24rpx;
color: #adadad;
}
}
.current-month {
.day-item-label {
color: #333;
}
.day-item-lunar {
color: #333;
}
}
.current-day {
background: #e1f0f1;
border-radius: 16rpx;
border-color: #4a8c90;
border-width: 4rpx;
border-style: solid;
.day-item-label {
color: #333;
}
.day-item-lunar {
color: #333;
}
}
.range-day {
background: #e1f0f1;
border-radius: 0;
.day-item-label {
color: #333;
}
.day-item-lunar {
color: #333;
}
}
.range-start {
background: #e1f0f1;
border-radius: 0;
.day-item-label {
color: #333;
}
.day-item-lunar {
color: #333;
}
}
.range-end {
background: #e1f0f1;
border-radius: 0;
.day-item-label {
color: #333;
}
.day-item-lunar {
color: #333;
}
}
.range-disabled {
opacity: 0.4;
pointer-events: none;
.day-item-label {
color: #999;
text-decoration: line-through;
}
.day-item-lunar {
color: #999;
}
}
}
@keyframes slideLeft {
0% {
transform: translateX(0);
opacity: 1;
}
50% {
transform: translateX(-100%);
opacity: 0;
}
51% {
transform: translateX(100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideRight {
0% {
transform: translateX(0);
opacity: 1;
}
50% {
transform: translateX(100%);
opacity: 0;
}
51% {
transform: translateX(-100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
</style>
注意:
组件选择年月的功能使用了一个自定义组件:myDateTimePicker ,这个组件是我另一个组件:juejin.cn/post/738659…
页面使用
<myCalendar :date="date" @clickDay="clickDay" :selectType="selectType"></myCalendar>
import myCalendar from './myCalendar.vue';
const date = ref('');
// const selectType = ref('single');
// const selectType = ref('multiple');
const selectType = ref('range');
function clickDay(day) {
console.log(day);
}
农历相关
如果isLunar为true,需要新加一个solarLunar.js文件
solarLunar.js文件来源:
solarLunar.js文件是solarlunar插件的源代码
- www.npmjs.com/package/sol…(详细的返回值可在链接地址查看)
我将代码copy下来后做了一点小改动
- 移除了自执行函数 (function() { ... })()
- 将其改为支持使用ES6导入
- 源码中调用函数需要传入多个参数:年、月、日,我新增了两个函数用于代替原本的公农历转换函数
tip:讲解一下改动后的js文件
1.调用公历转农历函数由原来的solar2lunar函数改成了solarStringToLunar,参数支持支持 2025/01/07 或 2025-01-07 格式
2.调用农历转公历函数由原来的lunar2solar函数改成了lunarStringToSolar,参数支持支持 2025/01/07 或 2025-01-07 格式
改造后的solarLunar.js文件
// 首先定义所有常量
const lunarInfo = [
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
//1900-1909
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
//1910-1919
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
//1920-1929
0x06566, 0x0d4a0, 0x0ea50, 0x16a95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
//1930-1939
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
//1940-1949
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,
//1950-1959
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
//1960-1969
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
//1970-1979
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
//1980-1989
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0,
//1990-1999
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
//2000-2009
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
//2010-2019
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
//2020-2029
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
//2030-2039
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
//2040-2049
/**Add By JJonline@JJonline.Cn**/
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,
//2050-2059
0x092e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
//2060-2069
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
//2070-2079
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
//2080-2089
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
//2090-2099
0x0d520,
]; //2100
const solarMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const Gan = [
'\u7532',
'\u4E59',
'\u4E19',
'\u4E01',
'\u620A',
'\u5DF1',
'\u5E9A',
'\u8F9B',
'\u58EC',
'\u7678',
];
const Zhi = [
'\u5B50',
'\u4E11',
'\u5BC5',
'\u536F',
'\u8FB0',
'\u5DF3',
'\u5348',
'\u672A',
'\u7533',
'\u9149',
'\u620C',
'\u4EA5',
];
const ChineseZodiac = [
'\u9F20',
'\u725B',
'\u864E',
'\u5154',
'\u9F99',
'\u86C7',
'\u9A6C',
'\u7F8A',
'\u7334',
'\u9E21',
'\u72D7',
'\u732A',
];
const festival = {
'1-1': {
title: '元旦节',
},
'2-14': {
title: '情人节',
},
'5-1': {
title: '劳动节',
},
'5-4': {
title: '青年节',
},
'6-1': {
title: '儿童节',
},
'9-10': {
title: '教师节',
},
'10-1': {
title: '国庆节',
},
'12-25': {
title: '圣诞节',
},
'3-8': {
title: '妇女节',
},
'3-12': {
title: '植树节',
},
'4-1': {
title: '愚人节',
},
'5-12': {
title: '护士节',
},
'7-1': {
title: '建党节',
},
'8-1': {
title: '建军节',
},
'12-24': {
title: '平安夜',
},
};
const lFestival = {
'12-30': {
title: '除夕',
},
'1-1': {
title: '春节',
},
'1-15': {
title: '元宵节',
},
'2-2': {
title: '龙抬头',
},
'5-5': {
title: '端午节',
},
'7-7': {
title: '七夕节',
},
'7-15': {
title: '中元节',
},
'8-15': {
title: '中秋节',
},
'9-9': {
title: '重阳节',
},
'10-1': {
title: '寒衣节',
},
'10-15': {
title: '下元节',
},
'12-8': {
title: '腊八节',
},
'12-23': {
title: '北方小年',
},
'12-24': {
title: '南方小年',
},
};
const solarTerm = [
'\u5C0F\u5BD2',
'\u5927\u5BD2',
'\u7ACB\u6625',
'\u96E8\u6C34',
'\u60CA\u86F0',
'\u6625\u5206',
'\u6E05\u660E',
'\u8C37\u96E8',
'\u7ACB\u590F',
'\u5C0F\u6EE1',
'\u8292\u79CD',
'\u590F\u81F3',
'\u5C0F\u6691',
'\u5927\u6691',
'\u7ACB\u79CB',
'\u5904\u6691',
'\u767D\u9732',
'\u79CB\u5206',
'\u5BD2\u9732',
'\u971C\u964D',
'\u7ACB\u51AC',
'\u5C0F\u96EA',
'\u5927\u96EA',
'\u51AC\u81F3',
];
const sTermInfo = [
'9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf97c3598082c95f8c965cc920f',
'97bd0b06bdb0722c965ce1cfcc920f',
'b027097bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f',
'97bd0b06bdb0722c965ce1cfcc920f',
'b027097bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f',
'97bd0b06bdb0722c965ce1cfcc920f',
'b027097bd097c36b0b6fc9274c91aa',
'9778397bd19801ec9210c965cc920e',
'97b6b97bd19801ec95f8c965cc920f',
'97bd09801d98082c95f8e1cfcc920f',
'97bd097bd097c36b0b6fc9210c8dc2',
'9778397bd197c36c9210c9274c91aa',
'97b6b97bd19801ec95f8c965cc920e',
'97bd09801d98082c95f8e1cfcc920f',
'97bd097bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa',
'97b6b97bd19801ec95f8c965cc920e',
'97bcf97c3598082c95f8e1cfcc920f',
'97bd097bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf97c3598082c95f8c965cc920f',
'97bd097bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf97c3598082c95f8c965cc920f',
'97bd097bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9210c8dc2',
'97b6b97bd19801ec95f8c965cc920f',
'97bd07f5307f595b0b0bc920fb0722',
'7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c920e',
'97b6b97bd19801ec95f8c965cc920f',
'97bd07f5307f595b0b0bc920fb0722',
'7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c9274c920e',
'97bcf7f0e47f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c8dc2',
'97b6b97bd197c36c9210c9274c920e',
'97bcf7f0e47f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c920e',
'97b6b7f0e47f531b0723b0b6fb0722',
'7f0e37f5307f595b0b0bc920fb0722',
'7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36b0b70c9274c91aa',
'97b6b7f0e47f531b0723b0b6fb0721',
'7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0787b0721',
'7f0e27f0e47f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa',
'97b6b7f0e47f149b0723b0787b0721',
'7f0e27f0e47f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c8dc2',
'977837f0e37f149b0723b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722',
'7f0e37f5307f595b0b0bc920fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2',
'977837f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2',
'977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722',
'977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722',
'977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722',
'977837f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722',
'7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e37f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722',
'7f0e37f1487f531b0b0bb0b6fb0722',
'7f0e37f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e37f1487f531b0b0bb0b6fb0722',
'7f0e37f0e37f14898082b072297c35',
'7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e37f0e37f14898082b072297c35',
'7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e37f0e366aa89801eb072297c35',
'7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f149b0723b0787b0721',
'7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e37f0e366aa89801eb072297c35',
'7ec967f0e37f14998082b0723b06bd',
'7f07e7f0e47f149b0723b0787b0721',
'7f0e27f0e47f531b0723b0b6fb0722',
'7f0e37f0e366aa89801eb072297c35',
'7ec967f0e37f14998082b0723b06bd',
'7f07e7f0e37f14998083b0787b0721',
'7f0e27f0e47f531b0723b0b6fb0722',
'7f0e37f0e366aa89801eb072297c35',
'7ec967f0e37f14898082b0723b02d5',
'7f07e7f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722',
'7f0e36665b66aa89801e9808297c35',
'665f67f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722',
'7f0e36665b66a449801e9808297c35',
'665f67f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e36665b66a449801e9808297c35',
'665f67f0e37f14898082b072297c35',
'7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e26665b66a449801e9808297c35',
'665f67f0e37f1489801eb072297c35',
'7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722',
];
const nStr1 = [
'\u65E5',
'\u4E00',
'\u4E8C',
'\u4E09',
'\u56DB',
'\u4E94',
'\u516D',
'\u4E03',
'\u516B',
'\u4E5D',
'\u5341',
];
const nStr2 = ['\u521D', '\u5341', '\u5EFF', '\u5345'];
const nStr3 = [
'\u6B63',
'\u4E8C',
'\u4E09',
'\u56DB',
'\u4E94',
'\u516D',
'\u4E03',
'\u516B',
'\u4E5D',
'\u5341',
'\u51AC',
'\u814A',
];
// 直接定义并导出日历对象
const solarLunar = {
lunarInfo,
solarMonth,
Gan,
Zhi,
Animals: ChineseZodiac,
festival,
lFestival,
solarTerm,
sTermInfo,
nStr1,
nStr2,
nStr3,
getFestival() {
return this.festival;
},
getLunarFestival() {
return this.lFestival;
},
setFestival(param = {}) {
this.festival = param;
},
setLunarFestival(param = {}) {
this.lFestival = param;
},
lYearDays(y) {
var i,
sum = 348;
for (i = 0x8000; i > 0x8; i >>= 1) {
sum += this.lunarInfo[y - 1900] & i ? 1 : 0;
}
return sum + this.leapDays(y);
},
leapMonth(y) {
//闰字编码 \u95f0
return this.lunarInfo[y - 1900] & 0xf;
},
leapDays(y) {
if (this.leapMonth(y)) {
return this.lunarInfo[y - 1900] & 0x10000 ? 30 : 29;
}
return 0;
},
monthDays(y, m) {
if (m > 12 || m < 1) {
return -1;
} //月份参数从1至12,参数错误返回-1
return this.lunarInfo[y - 1900] & (0x10000 >> m) ? 30 : 29;
},
solarDays(y, m) {
if (m > 12 || m < 1) {
return -1;
} //若参数错误 返回-1
var ms = m - 1;
if (ms === 1) {
//2月份的闰平规律测算后确认返回28或29
return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0 ? 29 : 28;
} else {
return this.solarMonth[ms];
}
},
toGanZhiYear(lYear) {
var ganKey = (lYear - 3) % 10;
var zhiKey = (lYear - 3) % 12;
if (ganKey === 0) ganKey = 10; //如果余数为0则为最后一个天干
if (zhiKey === 0) zhiKey = 12; //如果余数为0则为最后一个地支
return this.Gan[ganKey - 1] + this.Zhi[zhiKey - 1];
},
toAstro(cMonth, cDay) {
var s =
'\u6469\u7FAF\u6C34\u74F6\u53CC\u9C7C\u767D\u7F8A\u91D1\u725B\u53CC\u5B50\u5DE8\u87F9\u72EE\u5B50\u5904\u5973\u5929\u79E4\u5929\u874E\u5C04\u624B\u6469\u7FAF';
var arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22];
return s.substr(cMonth * 2 - (cDay < arr[cMonth - 1] ? 2 : 0), 2) + '\u5EA7'; //座
},
toGanZhi(offset) {
return this.Gan[offset % 10] + this.Zhi[offset % 12];
},
getTerm(y, n) {
if (y < 1900 || y > 2100 || n < 1 || n > 24) {
return -1;
}
var _table = this.sTermInfo[y - 1900];
var _calcDay = [];
for (var index = 0; index < _table.length; index += 5) {
var chunk = parseInt('0x' + _table.substr(index, 5)).toString();
_calcDay.push(chunk[0], chunk.substr(1, 2), chunk[3], chunk.substr(4, 2));
}
return parseInt(_calcDay[n - 1]);
},
toChinaMonth(m) {
// 月 => \u6708
if (m > 12 || m < 1) {
return -1;
} //若参数错误 返回-1
var s = this.nStr3[m - 1];
s += '\u6708'; //加上月字
return s;
},
toChinaDay(d) {
//日 => \u65e5
var s;
switch (d) {
case 10:
s = '\u521D\u5341';
break;
case 20:
s = '\u4E8C\u5341';
break;
case 30:
s = '\u4E09\u5341';
break;
default:
s = this.nStr2[Math.floor(d / 10)];
s += this.nStr1[d % 10];
}
return s;
},
getAnimal(y) {
return this.Animals[(y - 4) % 12];
},
solar2lunar(yPara, mPara, dPara) {
var y = parseInt(yPara);
var m = parseInt(mPara);
var d = parseInt(dPara);
//年份限定、上限
if (y < 1900 || y > 2100) {
return -1; // undefined转换为数字变为NaN
}
//公历传参最下限
if (y === 1900 && m === 1 && d < 31) {
return -1;
}
//未传参 获得当天
var objDate;
if (!y) {
objDate = new Date();
} else {
objDate = new Date(y, parseInt(m) - 1, d);
}
var i,
leap = 0,
temp = 0;
//修正ymd参数
y = objDate.getFullYear();
m = objDate.getMonth() + 1;
d = objDate.getDate();
var offset =
(Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) -
Date.UTC(1900, 0, 31)) /
86400000;
for (i = 1900; i < 2101 && offset > 0; i++) {
temp = this.lYearDays(i);
offset -= temp;
}
if (offset < 0) {
offset += temp;
i--;
}
//是否今天
var isTodayObj = new Date(),
isToday = false;
if (
isTodayObj.getFullYear() === y &&
isTodayObj.getMonth() + 1 === m &&
isTodayObj.getDate() === d
) {
isToday = true;
}
//星期几
var nWeek = objDate.getDay(),
cWeek = this.nStr1[nWeek];
//数字表示周几顺应天朝周一开始的惯例
if (nWeek === 0) {
nWeek = 7;
}
//农历年
var year = i;
leap = this.leapMonth(i); //闰哪个月
var isLeap = false;
//效验闰月
for (i = 1; i < 13 && offset > 0; i++) {
//闰月
if (leap > 0 && i === leap + 1 && isLeap === false) {
--i;
isLeap = true;
temp = this.leapDays(year); //计算农历闰月天数
} else {
temp = this.monthDays(year, i); //计算农历普通月天数
}
//解除闰月
if (isLeap === true && i === leap + 1) {
isLeap = false;
}
offset -= temp;
}
// 闰月导致数组下标重叠取反
if (offset === 0 && leap > 0 && i === leap + 1) {
if (isLeap) {
isLeap = false;
} else {
isLeap = true;
--i;
}
}
if (offset < 0) {
offset += temp;
--i;
}
//农历月
var month = i;
//农历日
var day = offset + 1;
//天干地支处理
var sm = m - 1;
var gzY = this.toGanZhiYear(year);
// 当月的两个节气
// bugfix-2017-7-24 11:03:38 use lunar Year Param `y` Not `year`
var firstNode = this.getTerm(y, m * 2 - 1); //返回当月「节」为几日开始
var secondNode = this.getTerm(y, m * 2); //返回当月「节」为几日开始
// 依据12节气修正干支月
var gzM = this.toGanZhi((y - 1900) * 12 + m + 11);
if (d >= firstNode) {
gzM = this.toGanZhi((y - 1900) * 12 + m + 12);
}
//传入的日期的节气与否
var isTerm = false;
var Term = null;
if (firstNode === d) {
isTerm = true;
Term = this.solarTerm[m * 2 - 2];
}
if (secondNode === d) {
isTerm = true;
Term = this.solarTerm[m * 2 - 1];
}
//日柱 当月一日与 1900/1/1 相差天数
var dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10;
var gzD = this.toGanZhi(dayCyclical + d - 1);
//该日期所属的星座
var astro = this.toAstro(m, d);
var solarDate = y + '/' + m + '/' + d;
var lunarDate = year + '/' + month + '/' + day;
var festival = this.festival;
var lFestival = this.lFestival;
var festivalDate = m + '/' + d;
var lunarFestivalDate = month + '/' + day;
// bugfix https://github.com/jjonline/calendar.js/issues/29
// 农历节日修正:农历12月小月则29号除夕,大月则30号除夕
// 此处取巧修正:当前为农历12月29号时增加一次判断并且把lunarFestivalDate设置为12-30以正确取得除夕
// 天朝农历节日遇闰月过前不过后的原则,此处取农历12月天数不考虑闰月
// 农历润12月在本工具支持的200年区间内仅1574年出现
if (month === 12 && day === 29 && this.monthDays(year, month) === 29) {
lunarFestivalDate = '12/30';
}
return {
date: solarDate,
lunarDate: lunarDate,
festival: festival[festivalDate] ? festival[festivalDate].title : null,
lunarFestival: lFestival[lunarFestivalDate] ? lFestival[lunarFestivalDate].title : null,
lYear: year,
lMonth: month,
lDay: day,
Animal: this.getAnimal(year),
IMonthCn: (isLeap ? '\u95F0' : '') + this.toChinaMonth(month),
IDayCn: this.toChinaDay(day),
cYear: y,
cMonth: m,
cDay: d,
gzYear: gzY,
gzMonth: gzM,
gzDay: gzD,
isToday: isToday,
isLeap: isLeap,
nWeek: nWeek,
ncWeek: '\u661F\u671F' + cWeek,
isTerm: isTerm,
Term: Term,
astro: astro,
};
},
lunar2solar(y, m, d, isLeapMonth) {
y = parseInt(y);
m = parseInt(m);
d = parseInt(d);
isLeapMonth = !!isLeapMonth;
var leapMonth = this.leapMonth(y);
this.leapDays(y);
if (isLeapMonth && leapMonth !== m) {
return -1;
} //传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同
if ((y === 2100 && m === 12 && d > 1) || (y === 1900 && m === 1 && d < 31)) {
return -1;
} //超出了最大极限值
var day = this.monthDays(y, m);
var _day = day;
//bugFix 2016-9-25
//if month is leap, _day use leapDays method
if (isLeapMonth) {
_day = this.leapDays(y, m);
}
if (y < 1900 || y > 2100 || d > _day) {
return -1;
} //参数合法性效验
//计算农历的时间差
var offset = 0;
var i;
for (i = 1900; i < y; i++) {
offset += this.lYearDays(i);
}
var leap = 0,
isAdd = false;
for (i = 1; i < m; i++) {
leap = this.leapMonth(y);
if (!isAdd) {
//处理闰月
if (leap <= i && leap > 0) {
offset += this.leapDays(y);
isAdd = true;
}
}
offset += this.monthDays(y, i);
}
//转换闰月农历 需补充该年闰月的前一个月的时差
if (isLeapMonth) {
offset += day;
}
//1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点)
var strap = Date.UTC(1900, 1, 30, 0, 0, 0);
var calObj = new Date((offset + d - 31) * 86400000 + strap);
var cY = calObj.getUTCFullYear();
var cM = calObj.getUTCMonth() + 1;
var cD = calObj.getUTCDate();
return this.solar2lunar(cY, cM, cD);
},
/**
* 传入时间字符串转换为农历
* @param dateStr 时间字符串,支持 2025/01/07 或 2025-01-07 格式
* @returns 农历信息对象
*/
solarStringToLunar(dateStr) {
if (!dateStr) return null;
// 处理日期字符串,支持 / 或 - 分隔
const parts = dateStr.split(/[-/]/);
if (parts.length !== 3) return null;
const [year, month, day] = parts.map((part) => parseInt(part));
return this.solar2lunar(year, month, day);
},
/**
* 传入农历时间字符串转换为公历
* @param dateStr 时间字符串,支持 2025/01/07 或 2025-01-07 格式
* @param isLeapMonth 是否闰月
* @returns 公历信息对象
*/
lunarStringToSolar(dateStr, isLeapMonth = false) {
if (!dateStr) return null;
// 处理日期字符串,支持 / 或 - 分隔
const parts = dateStr.split(/[-/]/);
if (parts.length !== 3) return null;
const [year, month, day] = parts.map((part) => parseInt(part));
return this.lunar2solar(year, month, day, isLeapMonth);
},
};
export default solarLunar;
页面使用:
import solarLunar from './solarLunar.js';
console.log(solarLunar.solarStringToLunar('2025/01/07'));
console.log(solarLunar.lunarStringToSolar('2025/01/07'));