Hello, 各位勇敢的小夥伴, 你們好, 我是大家的嘴強王者小五, 身體健康, 腦子沒病.
本人有豐富的脫髮技巧, 能讓你一躍成爲資深大咖.vue
一看就會一寫就廢是本人的主旨, 菜到摳腳是本人的特色, 卑微中透着一絲絲剛強, 傻人有傻福是對我最大的安慰.git
歡迎來到
小五
的隨筆系列
之手摸手教你用VUE封裝日曆組件
.github
雙手奉上代碼連接: 傳送門 - ajun568數組
雙腳奉上最終效果圖:函數
需求分析無非是一個想要什麼並逐步細化的過程, 畢竟誰都不能一口吃掉一張大餅, 因此咱們先把餅切開, 一點一點吃. 如下基於特定場景來實現一個基本的日曆組件. 小生不才, 還望各位看官輕噴, 歡迎各路大神留言指教.工具
場景: 在移動端
中經過切換日期
來切換收益數據, 展示形式爲上面日曆, 下面對應數據, 只顯示日數據
.佈局
基於此場景, 咱們對該日曆功能進行需求分析性能
先拆分一下日曆, 可將其上下拆分紅兩部分, 上面的 星期
部分, 和下面的 數據
部分, 一週7天限定了列數爲7列, 行數會隨當月天數及1號所在位置而有所不一樣.flex
移動端亦應根據屏幕寬度自適應佈局, flex
佈局就是一個很好的選擇, 咱們對數據部分進行下模擬, 先造一個長度爲40數據都爲0的數組以下:優化
const dataArr = Array(40).fill(0, 0, 40)
如今, 咱們想要每排顯示7個, 順次下移, 不妨想一下, 若是是你, 你會怎麼作?
父元素設置
flex-direction
: 用於定義主軸方向flex-wrap
: 用於定義是否換行flex-flow
: 同時定義flex-direction
和flex-wrap
子元素設置
flex-basis
: 用於設置伸縮基準值,可設置具體寬度或百分比,默認值是autoflex-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, },
allDataArr
- 輪播數組
❓ 何時對這個數組進行賦值
🅰️ 當[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
致使的, 因此要想好何時須要它, 何時果斷捨棄它長圖預警, 此處請單擊點開大圖觀看, 也可直接去個人github
上查看, 傳送門 - ajun568