基於Vue開發一個日曆元件

mykurisu發表於2018-02-22

最近在做一個類似課程表的需求,需要自制一個日曆來支援功能及展現,就順便研究一下應該怎麼開發日曆元件。

更新

  • 2.23修復了2026年2月份會渲染多一行的bug,謝謝@深藍一人童鞋提出的bug,解決方案是給二月份的日曆做特殊處理,new Date(year, month+1, 0).getDay() === 6時不會再渲染後面的日期。
  • 下班更新一哈,更科學的邏輯
    // if (total_calendar_list.length > 35) {
    //   nextNum = 42 - total_calendar_list.length;
    // } else {
    //   nextNum = 35 - total_calendar_list.length;
    // }
    
    // if (month === 1 && new Date(year, month, 0).getDay() === 6) {
    //   nextNum = 0
    // }
    
    nextNum = 6 - new Date(year, month+1, 0).getDay()
    複製程式碼

本文主要涉及以下內容:

  • 怎麼開發一套日曆皮膚?
  • 怎麼計算年月日?
  • 怎麼開發日曆相關的功能?
  • 總結&DEMO原始碼

怎麼開發一套日曆皮膚?

層層分離,塊塊獨立

在梳理日曆邏輯之前我想先記錄一下日曆樣式相關的問題:

下面是借鑑px2rem模式,寫的基於vw為主單位的自適應轉化。簡單來說,就是在我們的設計稿是iPhone8一倍圖的情況下,計算出某元素寬度與375(iPhone8最大寬度)的比例再與100vw相乘就得到了,該元素的vw值。因為vw是相對於螢幕的百分比單位,所以就能達到我們想要的自適應效果啦,不同的螢幕裡,同一元素的展現比例是一致的。

// 借鑑了Rem佈局
@function pxWithVw($n) {
  @return 100vw * $n / 375;
}
// 規定極限寬度,避免PC上觀感太差
@function pxWithVwMax($n) {
  @return 480px * $n / 375;
}
複製程式碼

有了上面這段SCSS的函式,我們就基本可以不用考慮螢幕適配的問題了,可以盡情的敲樣式啦。關於日曆的樣式,其實說複雜也還好,我們只需要在做之前好好的分一下層級就好了。

基於Vue開發一個日曆元件

如同上圖,每一個框表示一層元素,最後會有這樣的佈局--

<!--最外層的div限定整個日曆的寬度以及一些圓角陰影等樣式-->
<div class="calendar">
  <!--header則為上圖中綠色框的內容,包含上下月切換以及日曆title-->
  <div class="calendar__header"></div>
  <!--顧名思義main則是整個日曆的核心內容,也就是日期的展示區域-->
  <div class="calendar__main">
    <!--星期一~星期日的展示頭,列表渲染固定的7個block-->
    <div class="main__block-head"></div>
    <!--相應月份的日期展示區域,列表渲染-->
    <div class="main__block"></div>
  </div>
</div>
複製程式碼

也許大家看完之後比較奇怪calendar__main裡面的佈局,為什麼沒有把固定的展示頭分離開來,當你實際寫到這裡的時候會發現其實沒有這個必要。

因為我們用了pxWithVw去規定calendar__main的寬度以及每個block的寬度,也就確保了每7塊元素必定會佔滿我們一行,再利用justify-content: space-around確保我們每塊元素的間隙一致即可。

好了,層層分離說完了,什麼是塊塊獨立呢?

主要指的是日期的展示塊,我們是每塊獨立的,這樣在我們渲染的時候可以很方便的決定應該以什麼樣式去展示他,或是應該給他繫結怎麼樣的事件,給我們精密控制每個日期的展示提供了便利。

怎麼計算年月日?

本小節的內容總結起來其實就一句話--

我們只需要知道,某個月的1號是星期幾,就能把整個日曆渲染出來

關於年月日的計算,我這邊有兩種模式,一種是隻計算當月日期,另一種則是將整年的日期都計算出來。在本篇文章裡我想著重記錄第一種寫法,大家想了解第二種的話可以到我的github裡看看這個日曆的demo。

基於Vue開發一個日曆元件

我們先來個看圖說話,這個二月份有28天,1號是星期四。那是不是說,我們只要從週四開始,按順序渲染出28個'main__block'就好了呢?其實就是這樣,關鍵是怎麼把我們的1號定位到週四,只要這個能夠準確定位到,我們的日曆自然就出來了。

// 定義每個月的天數,如果是閏年第二月改為29天
// year=2018;month=1(js--month=0~11)
let daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
if ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0) {
    daysInMonth[1] = 29;
}
// 獲得指定年月的1號是星期幾
let targetDay = new Date(year, month, 1).getDay();
// 將要在calendar__main中渲染的列表
let total_calendar_list = [];
let preNum = targetDay;
// 首先先說一下,我們的日期是(日--六)這個順序也就是(0--6)
// 有了上述的前提我們可以認為targetDay為多少,我們就只需要在total_calendar_list的陣列中push幾個content為''的obj作為佔位
if (targetDay > 0) {
    for (let i = 0; i < preNum; i++) {
        let obj = {
            type: "pre",
            content: ""
        };
        total_calendar_list.push(obj);
    }
}
複製程式碼

這樣一來,1號的位置自然而然就到了我們需要的星期四了,接下來就只需要按順序渲染就ok啦。下面是剩下日期陣列填充,填充完畢之後return出來供我們view層使用。

for (let i = 0; i < daysInMonth[month]; i++) {
    let obj = {
        type: "normal",
        content: i + 1
    };
    total_calendar_list.push(obj);
}
nextNum = 6 - new Date(year, month+1, 0).getDay()
// 與上面的type=pre同理
for (let i = 0; i < nextNum; i++) {
    let obj = {
        type: "next",
        content: ""
    };
    total_calendar_list.push(obj);
}
return total_calendar_list;
複製程式碼

怎麼開發日曆相關的功能?

如何選擇上一個月或下一個月?

data() {
    return {
        // ...
        selectedYear: new Date().getFullYear(),
        selectedMonth: new Date().getMonth(),
        selectedDate: new Date().getDate()
    };
}

handlePreMonth() {
    if (this.selectedMonth === 0) {
        this.selectedYear = this.selectedYear - 1
        this.selectedMonth = 11
        this.selectedDate = 1
    } else {
        this.selectedMonth = this.selectedMonth - 1
        this.selectedDate = 1
    }
}

handleNextMonth() {
    if (this.selectedMonth === 11) {
        this.selectedYear = this.selectedYear + 1
        this.selectedMonth = 0
        this.selectedDate = 1
    } else {
        this.selectedMonth = this.selectedMonth + 1
        this.selectedDate = 1
    }
}
複製程式碼

就是這麼簡單,需要注意的點是跨年的時間轉換,我們需要在變更月份的同時把年份也改變,這樣才能渲染出正確的日期。

也許大家會有疑問,怎麼變更了月份或年份之後不需要重新計算一次日期呢?其實是有計算的,不知大家是否還記得,vue可是資料驅動變更的,我們只需要關注資料的變更即可,其他東西vue都會幫我們解決。

如果選中某一天?

handleDayClick(item) {
    if (item.type === 'normal') {
        // do anything...
        this.selectedDate = Number(item.content)
    }
}
複製程式碼

在渲染列表的時候我就給每一個block繫結了click事件,這樣做的好處就是呼叫十分方便,點選每一個block的時候,可以獲取該block的內容然後do anything you like

當然我們也可以給外層的父級元素繫結事件監聽,通過事件流來解決每個block的點選事件,這裡看個人習慣~畢竟元素數量不是特別多

總結

一個移動端日曆貌似也有驚無險的完成啦,總體來說日曆這活還是偏樣式方面的,對邏輯的要求不是特別高,對樣式的要求倒是挺高的需要對flexbox佈局有一定理解,才能迅速的吧日曆的骨架搭起來,雖然也不一定說必須用flex,不過個人認為用flex的效率會稍高一些。

基於Vue寫的日曆DEMO--Github

囉嗦一下,為什麼想起來寫日曆?當然是業務需求啦,所以說這個日曆元件一開始是react寫的,後面想在vue裡也嘗試一下就改成了vue。其實在react裡面寫也是大同小異啦,只不過我會把日期的block抽離成無狀態元件,也不為啥就感覺比較好看:)

基於Vue開發一個日曆元件

相關文章