寫一個構建複雜資料的日曆元件 Kalendar

LoadChange發表於2018-09-04

需求:我們經常會在一些旅遊、訂票、酒店的頁面中看到一些日曆,這些日曆有公曆、農曆、節假日、非節假日(調休)的標註,同時還有產品業務上的資訊,比如 票價、餘票等等。那現在開始造造輪子了,首先這個元件應該可以生成一個基本的日曆,並且掛入一些擴充套件的資訊在每一天的物件上。它可能看上去應該像這樣⤵,或者更復雜。


寫一個構建複雜資料的日曆元件 Kalendar


GitHub | NPM | Demo

注:這是一個無關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() 函式用於抹平小時及以下的時間

生成一個月的表格

這個物件是二維陣列的形式,一個月中有若干周,每週的陣列中存放每天的物件

寫一個構建複雜資料的日曆元件 Kalendar

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 至少產生一個月的資料。

寫一個構建複雜資料的日曆元件 Kalendar

打包與釋出

為了獲得更小的體積,我使用 rollup 構建元件程式碼,並使用 MIT 協議 npm publish

GitHub | NPM | Demo

相關文章