写一个构建复杂数据的日历组件 Kalendar

需求:我们经常会在一些旅游、订票、酒店的页面中看到一些日历,这些日历有公历、农历、节假日、非节假日(调休)的标注,同时还有产品业务上的信息,比如 票价、余票等等。那现在开始造造轮子了,首先这个组件应该可以生成一个基本的日历,并且挂入一些扩展的信息在每一天的对象上。它可能看上去应该像这样⤵,或者更复杂。

注:这是一个无关UI的组件,我们要做的只是如何去构建一个这样的日历对象。

设计组件

日历的展现一般是一个表格,我们需要将每一天的信息放入表格,生成一个月或几个月的完整日历。

  1. 查询当前月份有多少天
  2. 计算这个月的第一天前面空了几天(需要考虑日历起点,周一开始或周日开始)
  3. 如果有空天,放入需要跳过的天数
  4. 生成有数据的 每一天 的数据
  5. 放有数据的对象到月份表格中

查询月份总天数

这个函数非常简单,它接收一个参数 date ,可变天数为 2 月,这里进行闰年判断即可。

export function getMonthDays(date) {
    const year = date.getFullYear()
    const month = date.getMonth()
    const leapYear = !(year % 4) && ((year % 100) || !(year % 400))
    const days = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    return days[month]
}
复制代码

四年一闰,百年不闰,四百年再闰。

请注意这里参数的 date , 很有可能是 new Date() ,比如当天为 2018-08-31 , 当用户获得正常的本月数据后,再直接设置月份,如向上翻到 6 月时就会出现问题,因为 6 月没有 31 天,此时的 date 会得到 7 月 1 号的日期,与预期不符。 所以在操作完 date 后一定要注意将日期设置为 1 号。

const days = utils.getMonthDays(date)
date.setDate(1)
复制代码

创建一天的信息

每天的信息应该包含一些公共的属性,这些属性来自于 date 对象中,还有一些源于用户的扩展字段。这里我用一个类来构造这个对象。

class Day {
    constructor(dateObj, extension = {}) {
        this.year = dateObj.getFullYear()
        this.month = dateObj.getMonth()
        this.date = dateObj.getDate()
        this.day = dateObj.getDay()
        this.dateText = utils.getChinaStandard(dateObj)
        this.past = this.toDay.getTime() > dateObj.getTime()
        this.today = utils.getChinaStandard(new Date()) === this.dateText
        this.timestamp = dateObj.getTime()
        const _self = this
        Object.keys(extension).forEach(key => {
            _self[key] = extension[key]
        })
    }

    get toDay() {
        const date = new Date()
        date.setHours(0)
        date.setMinutes(0)
        date.setSeconds(0)
        date.setMilliseconds(0)
        return date
    }
}
复制代码

注释:

  • past 属性可能会出现在买票的场景中,过去的时间不能购票
  • today 判断是否是今天
  • dateText 生成一个字符串格式的日期
  • toDay() 函数用于抹平小时及以下的时间

生成一个月的表格

这个对象是二维数组的形式,一个月中有若干周,每周的数组中存放每天的对象

monthly 方法设计为静态方法,方便调用者通过 Kalendar.monthly() 快速构建某一个月的数据对象,

参数

  • date 生成月份的 date 对象
  • mount 需要扩展那些信息
  • weekStart 设置一周的开始为周几,默认周日
  • unifiedMount 所有日期都需要扩展的信息
static monthly({date, mount = {}, weekStart = 0, unifiedMount = {}}) {
        const monthTable = []
        const days = utils.getMonthDays(date)
        date.setDate(1)
        const day = date.getDay()
        let skip = 0
        if (day !== weekStart) skip = day - weekStart
        for (let i = 0; i < days + skip; i += 7) {
            const week = []
            let num = 7
            if (!i && skip) {
                for (let k = 0; k < skip; k++) week.push(null)
                num -= skip
            }
            for (let j = 0; j < num; j++) {
                const dateText = utils.getChinaStandard(date)
                week.push(new Day(date, Object.assign({}, unifiedMount, mount[dateText])))
                if (date.getDate() >= days) break
                date.setDate(date.getDate() + 1)
            }
            monthTable.push(week)
        }
        return monthTable
    }
复制代码

unifiedMount 所有日期都要添加的可能是票价,或者一些默认数据

    var unifiedMount = {
        total: 1000,
        price: 550,
        bg: 'info'
    }
复制代码

mount 是一些具体日期的数据,比如某一天票已售罄,应该禁用这一天的选择

    var mount = {
        '2018-08-30': {
            total: 0,
            price: 550
        },
        '2018-08-31': {
            total: 10,
            price: 750
        },
    }
复制代码

构建长期的日历对象

在一些移动端的场景,往往是一次性获取三个月或更长的时间,滑动显示的效果。

我在 Kalendar 类中的构造函数中 返回了 _create() 函数,_create() 函数用户生成多个月的信息。

class Kalendar {
    constructor({start, end, unifiedMount = {}, mount = {}, weekStart = 0} = {}) {
        this.startTime = start
        this.endTime = end
        this.unifiedMount = unifiedMount
        this.mount = mount
        this.weekStart = weekStart
        return this._create()
    }
    
    ......
    
    _create() {
        const {mount, weekStart, unifiedMount} = this
        const table = {}
        let count = (this.endDate.getFullYear() * 12 + this.endDate.getMonth() + 1)
            - (this.startDate.getFullYear() * 12 + this.startDate.getMonth() + 1)
        if (count < 0) return null
        let idx = 0
        do {
            const date = this.startDate
            date.setMonth(date.getMonth() + idx)
            const monthTable = Kalendar.monthly({date, mount, weekStart, unifiedMount})
            table[utils.getChinaStandard(date, true)] = monthTable
            count--
            idx++
        } while (count > 0)
        return table
    }
    
    static monthly({date, mount = {}, weekStart = 0, unifiedMount = {}}) {
        ......
    }
}
复制代码

相比生成一个月的数据,构造函数需要设置 日期起止值,在计算相差月份后,循环调用 monthly 函数,避免 startTimeendTime 是同一个月,没有月份差,这里使用 do ... while 至少产生一个月的数据。

打包与发布

为了获得更小的体积,我使用 rollup 构建组件代码,并使用 MIT 协议 npm publish

GitHub | NPM | Demo

猜你喜欢

转载自juejin.im/post/5b8e45bfe51d4538c77ab90e
今日推荐