最近在做一個類似課程表的需求,需要自制一個日曆來支援功能及展現,就順便研究一下應該怎麼開發日曆元件。
更新
- 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的函式,我們就基本可以不用考慮螢幕適配的問題了,可以盡情的敲樣式啦。關於日曆的樣式,其實說複雜也還好,我們只需要在做之前好好的分一下層級就好了。
如同上圖,每一個框表示一層元素,最後會有這樣的佈局--
<!--最外層的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。
我們先來個看圖說話,這個二月份有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的效率會稍高一些。
囉嗦一下,為什麼想起來寫日曆?當然是業務需求啦,所以說這個日曆元件一開始是react寫的,後面想在vue裡也嘗試一下就改成了vue。其實在react裡面寫也是大同小異啦,只不過我會把日期的block抽離成無狀態元件,也不為啥就感覺比較好看:)