[Vue] 如何实现一个日历组件?
效果预览
See the Pen Calendar by Erioifpud (@erioifpud) on CodePen.
准备
本文会对日历数据方面的构造做解析,不会过多涉及样式,因为样式会跟着实际的业务需求做调整。
首先要确定我们要做的日历是什么样的,比如说每周是从星期几开始(周一还是周日)?要不要显示前后两个月的日子?要不要高亮当天的日期?这些都会影响到日历数据的构造。
数据
我这里要做的日历是从周日开始的,并且一个页面内要显示 42 天的数据,包括了前后两个月,也就是月末与月初,也需要高亮当天的日期。
首先我们得有年份 year
和月份 month
两个 props
,根据这两个值算出 42 天分别是几月几号,由于一页的数据是由最多 3 个月份组成,所以我会分成“上月”、“本月”与“下月” 3 部分来讲解。
每一天的数据结构如下:
{
year: 2020,
month: 12,
date: 22,
type
}
其中 type
表示这个日期的类型,比如说本月 current
、上月 prev
、当天 today
,在样式上可以做一些区分。
初始化
// props
// year: 当前年份
// month: 当前月份(这里的范围是 1-12,方便使用)
// 本月第一天是星期几(0-6)
const firstDay = new Date(year, month - 1).getDay()
// 本月有多少天(本月最后一天是多少号,1-31)
const dates = new Date(year, month, 0).getDate()
// 上个月有多少天
const prevDates = new Date(year, month - 1, 0).getDate()
// 一页显示 42 天,6 行 7 列
const CALENDAR_TOTAL_DATES = 42
// 日历的日期列表
const dateList = []
这里有几个关于 Date
的细节需要提一下:
- 第二个参数
month
,表示月份,Date
的月份不管是传入的还是使用getMonth()
获取的,均是从0
开始计算的,也就是说 0 表示一月,1 表示二月,以此类推。
若传的数据大于 11,那么就会推算到下一年,比方说 12,就是第二年的一月。
- 第三个参数
date
,他表示“一个月中的第几天”,正常来说是从 1 开始的,但传入 0 的时候也不会报错,只是会往前一个月推算,也就是上个月的最后一天。 getDate()
,Date
对象中有getDate
与getDay
两个函数,功能不一样,分别是获取当天的日期(1-31)与当天的星期(0-6)。
上月
首先我们得拿到上一个月有多少天:
const prevDates = new Date(year, month - 1, 0).getDate()
假设现在要显示 2020 年 12 月的日历,实参为 2020 与 11,那么通过 new Date(2020, 11, 0)
拿到的日期实际上是 2020 年 11 月 30 号,因为第二个参数 month - 1
为 11,所以是 12 月,但第三个参数是 0,会往前推一天。
接着拿到本月第一天是星期几:
const firstDay = new Date(year, month - 1).getDay()
第一天肯定是在周一到周日之间的,取值范围是 0-6,星期天对应 0,星期一对应 1,如图:
因为 12 月的第一天是周二,那么 firstDay
也就是 2,所以这一页的日历会显示上个月的最后两天,接下来开始填数字:
for (let index = 0; index < firstDay; index++) {
dateList.push({
type: 'prev',
// 按上个月的倒数第 firstDay 天开始算
date: (prevDates - firstDay) + index + 1,
year: month === 1 ? year - 1 : year,
month: month === 1 ? 12 : month - 1
})
}
因为上月、下月可能会涉及到跨年,所以需要处理一下年份、月份。
本月
本月的数据是最容易获取的:
const dates = new Date(year, month, 0).getDate()
假设现在要显示 2020 年 12 月的日历,实参为 2020 与 12,那么通过 new Date(2020, 12, 0)
拿到的日期实际上是 2020 年 12 月 31 号,因为第二个参数 month
为 12,所以会往后推一个月到 2021 年第一天,但第三个参数 date
为 0,所以会往前推一天。
现在通过 getDate()
拿到的就是 12 月 31 号这天的日期(31)了,我们用他来表示本月有多少天。
拿到了本月的天数,每一天的日期就只剩下填数字这一个步骤了:
for (let index = 0; index < dates; index++) {
const date = index + 1
dateList.push({
type: this.isToday(date, month, year) ? 'today' : 'current',
date,
year,
month
})
}
下月
这和其他两个月份有些不一样,我们并不需要拿到下个月的日期信息,因为日期是正序的,第一天肯定是 1 号,我们只需要算出这一页的日历上会显示下个月的多少天就行了。
由于一页上有 6 行 7 列,一共 42 天,只需要用它减去本月的天数与上月的天数就行了:
CALENDAR_TOTAL_DATES - dates - firstDay
剩下的就是填数字:
const nextDates = CALENDAR_TOTAL_DATES - dates - firstDay
for (let index = 0; index < nextDates; index++) {
const date = index + 1
dateList.push({
type: 'next',
date,
year: month === 12 ? year + 1 : year,
month: month === 12 ? 1 : month + 1
})
}
因为上月、下月可能会涉及到跨年,所以需要处理一下年份、月份。
样式
调整网格布局的某行或某列的样式,而且这一行、一列上的所有单元格都是 1x1 的,这时候问题就变成了“如何选中这一行、一列上的所有单元格”。
在这个日历里,有两处遇到了这个问题的地方,一是周末两列背景置灰,二是取消周六那一列的右边界。
为了分别选中这两列,我使用了 nth-child
这个选择器,他能接受变量 n
表示“一组兄弟元素中的每一个元素”,比方说 nth-child(7n)
,他包含了 0(不存在)、7、14...等位置的单元格,日历中有 7 列(并且单元格是 1x1),所以他能恰好选中最后一列的所有单元格。
那么对于第一列的单元格,是不是能通过 nth-child(1n)
选择呢?答案是否,因为 1n
对应的结果是 0(不存在)、1、2...等位置的单元格,相当于全选。正确的选择方式应该是 7n - 6
,先定位到每一行的最后一个元素,再减去 6 定位到每行的第一个元素。