[Vue] 如何实现一个日历组件?

  |   0 评论   |   0 浏览   |   Erioifpud

效果预览

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 的细节需要提一下:

  1. 第二个参数 month,表示月份,Date 的月份不管是传入的还是使用 getMonth() 获取的,均是从 0 开始计算的,也就是说 0 表示一月,1 表示二月,以此类推。

若传的数据大于 11,那么就会推算到下一年,比方说 12,就是第二年的一月。

  1. 第三个参数 date,他表示“一个月中的第几天”,正常来说是从 1 开始的,但传入 0 的时候也不会报错,只是会往前一个月推算,也就是上个月的最后一天。
  2. getDate()Date 对象中有 getDategetDay 两个函数,功能不一样,分别是获取当天的日期(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,会往前推一天。

ryJDne.png

接着拿到本月第一天是星期几

const firstDay = new Date(year, month - 1).getDay()

第一天肯定是在周一到周日之间的,取值范围是 0-6,星期天对应 0,星期一对应 1,如图:

ryJr0H.md.png

因为 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,所以会往前推一天。

ryJDne.png

现在通过 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),所以他能恰好选中最后一列的所有单元格

ryJ0XD.png

那么对于第一列的单元格,是不是能通过 nth-child(1n) 选择呢?答案是否,因为 1n 对应的结果是 0(不存在)、1、2...等位置的单元格,相当于全选。正确的选择方式应该是 7n - 6,先定位到每一行的最后一个元素,再减去 6 定位到每行的第一个元素。


标题:[Vue] 如何实现一个日历组件?
作者:Erioifpud
地址:https://blog.doiduoyi.com/articles/1608693931056.html

评论

发表评论