Skip to main content

日历

逻辑预设

如果我们想要搭建这样一个日历组件,我们首先要解决三个问题:

  • 如何解决不同月份的翻页问题,怎样保证翻页流畅?
  • 日历每个单元渲染具体数据的算法?
  • 日历单元的特殊样式渲染?

如何获取日历需要渲染的月份跨度?

根据开始日期和结束日期判断需要渲染的内容是否跨年 如果日期年份没变,则跨越了 x 个 tab 否则跨越了-x+12 个 tab

// 获取开始年、月数,当前年、月数,及其间的月数跨度 startDate/nowDate:"YYYY-MM-DD"
export const getMonthsRange = (props: {
startDate: string,
nowDate: string,
}) => {
const { startDate, nowDate } = props;
const startArr = startDate.split("-").map((item) => Number(item));
const nowArr = nowDate.split("-").map((item) => Number(item));
if (startArr[0] === nowArr[0]) {
return {
start: { year: startArr[0], month: startArr[1] },
now: { year: nowArr[0], month: nowArr[1] },
range: nowArr[1] + 1 - startArr[1],
};
} else {
return {
start: { year: startArr[0], month: startArr[1] },
now: { year: nowArr[0], month: nowArr[1] },
range: 12 + 1 - startArr[1] + nowArr[1],
};
}
};

怎样保证翻页流畅?

对于翻页操作,我们可以手写滑动 tabs 或者依赖一些现成的库,因为手写滑动 tabs 的动画时间开销会比较大,所以我们最后选择使用一个现成的 tabs 库——rmc-tabs 我们会用到以下三个属性 初始页面 index,我们取月份跨度的最大值-1,也就是当前月的 index 需要渲染的 tabs 标题和 key,可以在 onChange 回调中拿到,一个小函数可以搞定(本需求年份跨度不超过两年,如果需要渲染更多年份这里需要改逻辑)

tabs={Array.from(new Array(months_range.range).keys()).map((item) => {
const month =
(months_range.start.month + item) % 12 === 0
? 12
: (months_range.start.month + item) % 12; // 边界情况:%12===0会转成0
const year =
months_range.start.month + item > 12
// 跨年了,肯定用上nowYear
? months_range.now.year
// 没跨年,直接用startYear就行
: months_range.start.year;
return {
key: `${year}-${month > 9 ? month : `0${month}`}`,
title: `${month}`,
};
})}
//months_range.range is a number 24

效果如下

tabs={[{key:2024-01,title:'01月'}],......}

具体日期算法

ps.日期格式转换我们使用业界主流的日期库 dayjs

日历月份行数有几种格式?

如果我们的日历兼容不同的日历行数,就需要根据一个月的全部天数和一个月的第一日是这星期的第几天把前后的空位补齐至 x 行,效果如下: [0,0,0,0,0,1,2,3,4,5.....0,0,0,0,0]

const fullDayShort = 28; // 日历四行
const fullDaysNormal = 35; // 日历五行
const fullDaysLong = 42; // 日历六行
const monthfullDay = {
full31Days: 31,
full30Days: 30,
full28Days: 28,
};

// 获取一个月的全部天数及其前后填充空白天
export const getMonthEveryday = (props: {
// 这个月有多少天
numOfDays: number,
// 这个月的第一天是星期几
firstDayInweek: number,
}) => {
const { numOfDays, firstDayInweek } = props;
const rawDay = Array.from(new Array(numOfDays).keys()).map(
(item) => item + 1
);
const frontDay = Array.from(new Array(firstDayInweek));
let behindDay = [];
if (
(numOfDays === monthfullDay.full31Days &&
(firstDayInweek === 5 || firstDayInweek === 6)) ||
(numOfDays === monthfullDay.full30Days && firstDayInweek === 6)
) {
behindDay = Array.from(
new Array(fullDaysLong - firstDayInweek - numOfDays)
);
} else if (numOfDays === monthfullDay.full28Days && firstDayInweek === 0) {
behindDay = Array.from(
new Array(fullDayShort - firstDayInweek - numOfDays)
);
} else {
behindDay = Array.from(
new Array(fullDaysNormal - firstDayInweek - numOfDays)
);
}
return [...frontDay, ...rawDay, ...behindDay];
};

特殊日期样式

如何实现日期元素特殊样式?

{
getMonthEveryday({
numOfDays: dayjs(`${dateTabs}-1`).daysInMonth(),
firstDayInweek: dayjs(`${dateTabs}-1`).day(),
}).map((item) => {
let className = ""; // 开始日期tag样式
let classNameBg = ""; // 日期背景圆 完成日:绿/当前日:灰
if (
achieveDays?.some((day) => day === dayjsFormatAddZero(dateTabs, item))
) {
classNameBg = "achieve";
} else if (
dayjs().format("YYYY-MM-DD") ===
dayjs(dayjsFormat(dateTabs, item)).format("YYYY-MM-DD")
) {
classNameBg = "now";
}
if (
start_time !== end_time &&
start_time === dayjsFormatAddZero(dateTabs, item)
) {
if (dayjs(dayjsFormatAddZero(dateTabs, item)).day() === 0) {
// 如果开始日期是周日,需要翻转样式,否则ui显示不全
className = "start-monday";
} else {
className = "start";
}
}
return (
<div key={item}>
<div className={className}>
<div className={classNameBg}>{item}</div>
</div>
</div>
);
});
}