手摸手教你用 VUE 封裝日曆組件
寫在前面
雙腳奉上最終效果圖:
需求分析
需求分析無非是一個想要什麼並逐步細化的過程, 畢竟誰都不能一口喫掉一張大餅, 所以我們先把餅切開, 一點一點喫. 以下基於特定場景來實現一個基本的日曆組件. 小生不才, 還望各位看官輕噴, 歡迎各路大神留言指教.
場景: 在移動端
中通過切換日期
來切換收益數據, 展現形式爲上面日曆, 下面對應數據, 只顯示日數據
.
基於此場景, 我們對該日曆功能進行需求分析
-
普遍場景下, 我們更傾向當天的數據情況. 所以基於此, 首次進入應展示當前月份且選中日期爲今日
-
點選日期, 應可以準確切換, 否則做它何用, 當🌹瓶嗎
-
切換月份, 以查看更多數據. 場景基於移動端, 交互方式選擇體驗更好的滑動切換, 左滑切換至上一月, 右滑切換至下一月
-
滑動切換月份後, 選中該月 1 號
-
移動端的展示區域非常寶貴, 減少佔用空間顯得極爲重要, 這時候周視圖就有了用武之地. 交互上可上滑切換至周視圖, 下拉切換回月視圖.
-
明確月視圖滑動切月, 周視圖滑動切周
-
滑動切換星期後, 選中該星期的第一天, 若左滑切換後存在 1 號, 選中 1 號
結構及樣式
先拆分一下日曆, 可將其上下拆分成兩部分, 上面的 星期
部分, 和下面的 數據
部分, 一週 7 天限定了列數爲 7 列, 行數會隨當月天數及 1 號所在位置而有所不同.
移動端亦應根據屏幕寬度自適應佈局, flex
佈局就是一個很好的選擇, 我們對數據部分進行下模擬, 先造一個長度爲 40 數據都爲 0 的數組如下:
現在, 我們想要每排顯示 7 個, 順次下移, 不妨想一下, 如果是你, 你會怎麼做?
-
父元素設置
-
flex-direction
: 用於定義主軸方向 -
flex-wrap
: 用於定義是否換行 -
flex-flow
: 同時定義flex-direction
和flex-wrap
-
子元素設置
-
flex-basis
: 用於設置伸縮基準值,可設置具體寬度或百分比,默認值是 auto -
flex-grow
: 用於設置放大比例,默認爲 0,如果存在剩餘空間,該元素也不會被放大 -
flex-shrink
: 用於設置縮小比例,默認爲 1,如果空間不足,將等比例縮小。如果設置爲 0,則它不會被縮小 -
flex
:flex-grow
、flex-shrink
和flex-basis
的縮寫
綜上, 我們可以設置樣式爲 👉🏼 父 flex: row wrap
子 flex: 0 0 14.285%
(1/7 ≈ 14.285%)
效果圖 👇
代碼片段 👇
此時, 可以加一層結構, 讓子元素寬高固定爲 40✖️40, 方便對選中後的樣式進行處理
我們來隨意勾勒兩筆樣式, 呈現如下 👇
展示當前月份及選中當天日期
憑空想象哪有直接上圖片來的直觀, 就像老闆畫的餅哪有 money 來的實在😏, 接下來我們結合下面圖片進行進一步的分析, 圖片爲我截取的手機日曆圖
首先, 既然是默認選中今天, 我們就先來獲取下當前日期
// 獲取當前日期
getCurrentDate() {
this.selectData = {
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
day: new Date().getDate(),
}
}
我們來看下這張圖片, 不考慮藍框中的部分, 要顯示出當月日期, 我們只需知道以下兩個點, 然後做 for 循環就可以了.
-
當前月份的天數
-
當前月份第一天應該顯示在什麼位置
這麼一看, 是不是 so easy! 不要太簡單有木有.
當月天數
“一三五七八十臘, 三十一天永不差”, 每年除了二月分平年閏年以外, 其餘月份的天數都是固定的, 這麼一看, 這不是區分下二月就完事了嗎
const { year } = this.selectData
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
}
當月第一天的位置
想知道當月第一天的位置, 換個思路想, 其實就是想知道當月第一天是星期幾, 誒, 這不是巧了嗎, 拿當月第一天的日期 getDay()
這不就完事了嗎
const { year, month } = this.selectData
const monthStartWeekDay = new Date(year, month - 1, 1).getDay()
接下來我們填充下數據, 前後做留白處理, 代碼及效果如下:
🧟♂️ Code
🧟♂️ Image
日期切換及月份切換
日期切換 = 更改當前數組中子元素的isSelected
// 切換點選日期
checkoutDate(selectData) {
if (selectData.type !== 'normal') return // 非有效日期不可點選
this.selectData.day = selectData.day // 對選中日期賦值
// 查找當前選中日期的索引
const oldSelectIndex = this.dataArr.findIndex(item => item.isSelected && item.type === 'normal')
// 查找新切換日期的索引 (tips: 這裏也可以直接把索引值傳過來 -> index)
const newSelectIndex = this.dataArr.findIndex(item => item.day === selectData.day && item.type === 'normal')
// 更改isSelected值
if (this.dataArr[oldSelectIndex]) this.$set(this.dataArr[oldSelectIndex], 'isSelected', false)
if (this.dataArr[newSelectIndex]) this.$set(this.dataArr[newSelectIndex], 'isSelected', true)
}
月份切換 = 重新生成新月份所對應的dataArr
, 並選中當月 1 號
tips: 這裏需要注意的點是, 1 月的上一月和 12 月的下一月, 以上一月舉例:
checkoutPreMonth() {
let { year, month, day } = this.selectData
if (month === 1) {
year -= 1
month = 12
} else {
month -= 1
}
this.selectData = { year, month, day: 1 }
this.dataArr = this.getMonthData(this.selectData)
},
今日
checkoutCurrentDate() {
this.getCurrentDate()
this.dataArr = this.getMonthData(this.selectData)
},
至此, 一個基本的月視圖就實現完畢了
滑動切月
接下來我們來對月視圖進行優化, 增加滑動切月的功能. 我們先來看一下實現的效果👇
以左滑爲例:
-
滑動過程中, 我們可以看到部分下個月的數據
-
滑動距離過小, 自動回彈到當前視圖
-
滑動超過一定距離, 自動滑至下一個月
touch
作案是需要工具的, 想要觸發滑動事件, 得先找到對應的工具
-
touchstart
: 手指觸摸屏幕時觸發 -
touchmove
: 手指在屏幕中拖動時觸發 -
touchend
: 手指離開屏幕時觸發
光靠這個事件, 在滑動過程中是無法看到下個月的部分數據的, 想要在滑動過程中看到數據, 這就是典型的輪播場景. 本質上就是一次transform
的過程.
此時, 我們調整下頁面結構, 由對dataArr
的單層循環改爲雙層循環模式, 其本質就是上圖所示的[pre, current, next]
數組
此步驟涉及的代碼改動較多, 接下來主要通過新引入的變量來捋清思路, 思路清晰了, 代碼順其自然就好, 👀 Let's go, come on baby!
allDataArr: [], // 輪播數組
isSelectedCurrentDate: false, // 是否點選的當月日期
translateIndex: 0, // 輪播所在位置
transitionDuration: 0.3, // 動畫持續時間
needAnimation: true, // 左右滑動是否需要動畫
isTouching: false, // 是否爲滑動狀態
touchStartPositionX: null, // 初始滑動X的值
touchStartPositionY: null, // 初始滑動Y的值
touch: { // 本次touch事件,橫向,縱向滑動的距離的百分比
x: 0,
y: 0,
},
❓ 什麼時候對這個數組進行賦值
🅰️ 當[pre, current, next]
中任意值變化時, 而pre
和next
的變化都依附於current
的變化, Wow, interesting! watch watch watch !!!
isSelectedCurrentDate
- 是否點選的當月日期
❓ 在點選切換數據時, 因爲isSelected
的變化, watch
監聽並執行賦值操作, 但此時並沒有必要重新生成pre
和next
translateIndex
- 輪播所在位置
用於控制pre, current, next
位置, 當觸發滑動切月時, 通過更改translateIndex
來更改位置. 在重新賦值時還原到初始值.
touchStartPositionX
, touchStartPositionY
, touch
這三個是爲了確定滑動方向及距離的, 向什麼方向滑動? (不要和我說你任性, 就想斜着滑動) 滑動多遠? 鬆手後, 滑動距離小做回彈處理, 滑動距離大做切換處理 (結合translateIndex
, 我知道你懂得)
needAnimation
- 左右滑動是否需要動畫
我們看圖說話 (👆), 是不是感覺這個動畫怪怪的, 但又說不清楚哪裏怪, 那是因爲在動畫進行中時候, 我們就對allDataArr
進行了賦值操作, 我們在定時器中延遲下這個賦值操作, 效果如下 (👇):
是不是有一個明顯的反覆橫跳的過程, 因爲我們滑動過去時候在next
, 但最後回到的是current
. 這點小問題怎麼能限制住我們的聰明大腦, 將回到current
的動畫去掉, 不就完美解決問題了嗎.
賦部分代碼片段:
切換周視圖
還是看圖說話, 文字哪有圖片直觀, 我們來分析下切換周的過程:
Bingo, 就是一個transformY
+height
的過程
👉 對於height
, 無非是總高度到單行高度反覆橫跳的過程, 每行高度是固定的, 總高度 = 單行高度 * 總行數
isWeekView: false, // 周視圖還是月視圖
itemHeight: 50, // 日曆行高
lineNum: 0, // 當前視圖總行數
this.lineNum = Math.ceil(this.dataArr.length / 7)
👉 對於transformY
, 其移動距離 =(當前所在行數 - 1)* 單行高度
offsetY: 0, // 周視圖 Y軸偏移量
// 處理周視圖的數據變化
dealWeekViewData() {
const selectedIndex = this.dataArr.findIndex(item => item.isSelected)
const indexOfLine = Math.ceil((selectedIndex + 1) / 7)
this.offsetY = -((indexOfLine - 1) * this.itemHeight)
},
補全視圖信息
在做周視圖的滑動切換之前, 我們來補全一下視圖信息, 將daraArr
的空白處填上對應日期.
年和月的填充就不說了, 簡單說下日的填充
next
比較簡單, 循環次數 = 7 - 最後一行天數 = 7 - 次月 1 日的星期索引 (tip: 需要注意的是, 若次月 1 日索引爲 0, 代表無空白處可填充, 自然也無需循環), day
的賦值從 1 號順次增加即可.
const nextInfo = this.getNextMonth()
let nextObj = {
type: 'next',
day: i + 1,
month: nextInfo.month,
year: nextInfo.year,
}
再來說說pre
, 循環次數 = 7 - 第一行天數 = 當月 1 號的星期索引, day
的賦值等於上月日期的倒序 => 上月天數 - (當月 1 號星期索引 - (index + 1))
const preInfo = this.getPreMonth(date)
let preObj = {
type: 'pre',
day: daysInMonth[preInfo.month - 1] - (monthStartWeekDay - i - 1),
month: preInfo.month,
year: preInfo.year,
}
❓ 這裏getPreMonth()
函數傳date
的原因
🅰️ 說白了, date 就是參照物唄, 對誰取上個月就傳誰; 而getNextMonth()
爲什麼不傳呢, 單純的無所謂, 傳與不傳它都是從 1 遞增, 誰又會在一個無關緊要的事上浪費感情呢.
點選非本月日期時, 對應做切換月份的處理即可, 此時切換後的日期爲點選日期, 而非 1 號
滑動切換星期
在視圖切換的過程中, 與我們一同上下摩擦的, 還是陪着我們不離不棄的preArr
和nextArr
. 既然甩不掉, 何不將它們的價值榨乾到極致, 這樣才符合利益最大化嘛, 我們對同一橫行的前後數據做狸貓換太子的操作, 將其分別換成當前數據的前一週和後一週, 畢竟破壞纔是更好的創造.
要想狸貓換太子, 得先找到那隻狸貓, 在找到太子, 才能進行兩者的對調. 我們以切換至上一週爲例, 來具體找一下狸貓和太子.
- 狸貓 -
lastWeek
No.1 如果非首行數據, 上週 = 上一行. 通過當前行數, 拿到兩端數據的索引, 分別減 7 獲取上一週兩端數據的索引, 進而拿到上一週的數據.
No.2 如果當前爲首行, 又可進一步劃分爲: 首個數據項是否爲 1 號, 若是, 則取上個月最後一行數據; 若否, 則取上個月倒數第二行數據(tips: 此時上個月最後一行等同於當前首行)
; 以上兩點, 也可考慮成查找特定日期在上個月的所在行.
- 太子 - 平行世界的當前行
// 獲取處理周視圖所需的位置信息
getInfoOfWeekView(selectedIndex, length) {
const indexOfLine = Math.ceil((selectedIndex + 1) / 7) // 當前行數
const totalLine = Math.ceil(length / 7) // 總行數
const sliceStart = (indexOfLine - 1) * 7 // 當前行左端索引
const sliceEnd = sliceStart + 7 // 當前行右端索引
return { indexOfLine, totalLine, sliceStart, sliceEnd }
},
// 處理lastWeek、nextWeek, 並返回替換行索引
dealWeekViewSliceStart() {
const selectedIndex = this.dataArr.findIndex(item => item.isSelected)
const {
indexOfLine,
totalLine,
sliceStart,
sliceEnd
} = this.getInfoOfWeekView(selectedIndex, this.dataArr.length)
this.offsetY = -((indexOfLine - 1) * this.itemHeight)
// 前一週數據
if (indexOfLine === 1) {
const preDataArr = this.getMonthData(this.getPreMonth(), true)
const preDay = this.dataArr[0].day - 1 || preDataArr[preDataArr.length - 1].day
const preIndex = preDataArr.findIndex(item => item.day === preDay && item.type === 'normal')
const { sliceStart: preSliceStart, sliceEnd: preSliceEnd } = this.getInfoOfWeekView(preIndex, preDataArr.length)
this.lastWeek = preDataArr.slice(preSliceStart, preSliceEnd)
} else {
this.lastWeek = this.dataArr.slice(sliceStart - 7, sliceEnd - 7)
}
// 後一週數據
if (indexOfLine >= totalLine) {
const nextDataArr = this.getMonthData(this.getNextMonth(), true)
const nextDay = this.dataArr[this.dataArr.length - 1].type === 'normal' ? 1 : this.dataArr[this.dataArr.length - 1].day + 1
const nextIndex = nextDataArr.findIndex(item => item.day === nextDay)
const { sliceStart: nextSliceStart, sliceEnd: nextSliceEnd } = this.getInfoOfWeekView(nextIndex, nextDataArr.length)
this.nextWeek = nextDataArr.slice(nextSliceStart, nextSliceEnd)
} else {
this.nextWeek = this.dataArr.slice(sliceStart + 7, sliceEnd + 7)
}
return sliceStart
},
dealWeekViewData() {
const sliceStart = this.dealWeekViewSliceStart()
this.allDataArr[0].splice(sliceStart, 7, ...this.lastWeek)
this.allDataArr[2].splice(sliceStart, 7, ...this.nextWeek)
},
優化代碼
到這裏基本就大功告成了, 我們總結下剩下的問題並加以處理, 阿拉霍洞開
-
一些蹩腳的動畫: 此場景下, 一切奇怪的動畫都是由
transitionDuration
導致的, 所以我們要想清楚什麼時候需要動畫, 什麼時候不需要, 不需要時候賦值爲 0 就好了 -
類似卡頓的效果: 此場景下, 幾乎所有的卡頓、延遲, 都是那個萬惡的
setTimeout
導致的, 所以要想好什麼時候需要它, 什麼時候果斷捨棄它 -
最後加個底部的 touch 條, 使其更美觀些
源自:https://segmentfault.com/a/1190000039370231
聲明:文章著作權歸作者所有,如有侵權,請聯繫小編刪除。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/4LFzKxXzV-EZX3UBvcGBdQ