寫在前面
雙手奉上程式碼連結: 傳送門 - ajun568
雙腳奉上最終效果圖:
需求分析
需求分析無非是一個想要什麼並逐步細化的過程, 畢竟誰都不能一口吃掉一張大餅, 所以我們先把餅切開, 一點一點吃. 以下基於特定場景來實現一個基本的日曆套件. 小生不才, 還望各位看官輕噴, 歡迎各路大神留言指教.
「我自己是一名從事了6年web前端開發的老程式設計師(我的微信:webxxq),今年年初我花了一個月整理了一份最適合2021年自學的web前端全套培訓教學(影片+原始碼+筆記+專案實作),從最基礎的HTML+CSS+JS到行動裝置HTML5以及各種框架和新技術都有整理,打包給每一位前端小夥伴,這裡是前端學習者聚集地,歡迎初學和進階中的小夥伴(所有前端教學關注我的微信公眾號:web前端學習圈,關注後回復「web」即可領取)。
場景: 在
基於此場景, 我們對該日曆功能進行需求分析
- 普遍場景下, 我們更傾向當天的資料情況. 所以基於此, 首次進入應展示當前月份且選中日期為今日
- 點選日期, 應可以準確切換, 否則做它何用, 當??瓶嗎
- 切換月份, 以檢視更多資料. 場景基於行動裝置, 互動方式選擇體驗更好的滑動切換, 左滑切換至上一月, 右滑切換至下一月
- 滑動切換月份後, 選中該月1號
- 行動裝置的展示區域非常寶貴, 減少佔用空間顯得極為重要, 這時候周檢視表就有了用武之地. 互動上可上滑切換至周檢視表, 下拉切換回月檢視表.
- 明確月檢視表滑動切月, 周檢視表滑動切周
- 滑動切換星期後, 選中該星期的第一天, 若左滑切換後存在1號, 選中1號
結構及樣式
先拆解一下日曆, 可將其上下拆解成兩部分, 上面的
行動裝置亦應根據螢幕寬度自適應佈局,
1 | 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 的縮寫
綜上, 我們可以設定樣式為 ???? 父
效果圖 ??
程式碼片段 ??
此時, 可以加一層結構, 讓子元素寬高固定為40??40, 方便對選中後的樣式進行處理
我們來隨意勾勒兩筆樣式, 呈現如下 ??
展示當前月份及選中當天日期
憑空想像哪有直接上圖像來的直觀, 就像老闆畫的餅哪有money來的實在??, 接下來我們結合下面圖像進行進一步的分析, 圖像為我擷取的行動電話日曆圖
首先, 既然是預設選中今天, 我們就先來取得下當前日期
1 2 3 4 5 6 7 8 | // 取得當前日期 getCurrentDate() { this.selectData = { year: new Date().getFullYear(), month: new Date().getMonth() + 1, day: new Date().getDate(), } } |
我們來看下這張圖像, 不考慮藍框中的部分, 要顯示出當月日期, 我們只需知道以下兩個點, 然後做for迴圈就可以了.
- 當前月份的天數
- 當前月份第一天應該顯示在什麼位置
這麼一看, 是不是 so easy! 不要太簡單有木有.
當月天數
「一三五七八十臘, 三十一天永不差」, 每年除了二月分平年閏年以外, 其餘月份的天數都是固定的, 這麼一看, 這不是區分下二月就完事了嗎
1 2 3 4 5 6 | 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 } |
當月第一天的位置
想知道當月第一天的位置, 換個思路想, 其實就是想知道當月第一天是星期幾, 誒, 這不是巧了嗎, 拿當月第一天的日期
1 2 | const { year, month } = this.selectData const monthStartWeekDay = new Date(year, month - 1, 1).getDay() |
接下來我們填充下資料, 前後做留白處理, 程式碼及效果如下:
???♂? Code
???♂? Image
日期切換及月份切換
日期切換 = 更改當前陣列中子元素的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 切換點選日期 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) } |
月份切換 = 重新生成新月份所對應的
tips: 這裡需要注意的點是, 1月的上一月和12月的下一月, 以上一月舉例:
1 2 3 4 5 6 7 8 9 10 11 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) }, |
今日
1 2 3 4 | checkoutCurrentDate() { this.getCurrentDate() this.dataArr = this.getMonthData(this.selectData) }, |
至此, 一個基本的月檢視表就實現完畢了
滑動切月
接下來我們來對月檢視表進行調校, 增加滑動切月的功能. 我們先來看一下實現的效果??
以左滑為例:
- 滑動過程中, 我們可以看到部分下個月的資料
- 滑動距離過小, 自動回彈到當前檢視表
- 滑動超過一定距離, 自動滑至下一個月
touch
作案是需要工具的, 想要觸發滑動事件, 得先找到對應的工具
touchstart : 手指觸控螢幕時觸發touchmove : 手指在螢幕中拖動時觸發touchend : 手指離開螢幕時觸發
光靠這個事件, 在滑動過程中是無法看到下個月的部分資料的, 想要在滑動過程中看到資料, 這就是典型的輪播場景. 本質上就是一次
此時, 我們調整下頁面結構, 由對
此步驟涉及的程式碼改動較多, 接下來主要透過新引入的變數來捋清思路, 思路清晰了, 程式碼順其自然就好, ?? Let's go, come on baby!
1 2 3 4 5 6 7 8 9 10 11 12 | allDataArr: [], // 輪播陣列 isSelectedCurrentDate: false, // 是否點選的當月日期 translateIndex: 0, // 輪播所在位置 transitionDuration: 0.3, // 影片持續時間 needAnimation: true, // 左右滑動是否需要影片 isTouching: false, // 是否為滑動狀態 touchStartPositionX: null, // 初始滑動X的值 touchStartPositionY: null, // 初始滑動Y的值 touch: { // 本次touch事件,橫向,縱向滑動的距離的百分比 x: 0, y: 0, }, |
? 什麼時候對這個陣列進行賦值
??? 當
? 在點選切換資料時, 因為
用於控制
這三個是為了確定滑動方向及距離的, 向什麼方向滑動? (不要和我說你任性, 就想斜著滑動) 滑動多遠? 鬆手後, 滑動距離小做回彈處理, 滑動距離大做切換處理 (結合
我們看圖說話(??), 是不是感覺這個影片怪怪的, 但又說不清楚哪裡怪, 那是因為在影片進行中時候, 我們就對
是不是有一個明顯的反覆橫跳的過程, 因為我們滑動過去時候在
賦部分程式碼片段:
切換周檢視表
還是看圖說話, 文字哪有圖像直觀, 我們來分析下切換周的過程:
Bingo, 就是一個
?? 對於
1 2 3 4 5 | isWeekView: false, // 周檢視表還是月檢視表 itemHeight: 50, // 日曆行高 lineNum: 0, // 當前檢視表總行數 this.lineNum = Math.ceil(this.dataArr.length / 7) |
?? 對於
1 2 3 4 5 6 7 8 | 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) }, |
補全檢視表訊息
在做周檢視表的滑動切換之前, 我們來補全一下檢視表訊息, 將
年和月的填充就不說了, 簡單說下日的填充
1 2 3 4 5 6 7 8 | const nextInfo = this.getNextMonth() let nextObj = { type: 'next', day: i + 1, month: nextInfo.month, year: nextInfo.year, } |
再來說說
1 2 3 4 5 6 7 8 | const preInfo = this.getPreMonth(date) let preObj = { type: 'pre', day: daysInMonth[preInfo.month - 1] - (monthStartWeekDay - i - 1), month: preInfo.month, year: preInfo.year, } |
? 這裡
??? 說白了, date就是參照物唄, 對誰取上個月就傳誰; 而
點選非本月日期時, 對應做切換月份的處理即可, 此時切換後的日期為點選日期, 而非1號
滑動切換星期
在檢視表切換的過程中, 與我們一同上下摩擦的, 還是陪著我們不離不棄的
要想狸貓換太子, 得先找到那隻狸貓, 在找到太子, 才能進行兩者的對調. 我們以切換至上一周為例, 來具體找一下狸貓和太子.
- 狸貓 -
lastWeek
No.1 如果非首行資料, 上周=上一行. 透過當前行數, 拿到兩端資料的索引, 分別減7取得上一周兩端資料的索引, 進而拿到上一周的資料.
No.2 如果當前為首行, 又可進一步劃分為: 首個資料項是否為1號, 若是, 則取上個月最後一行資料; 若否, 則取上個月倒數第二行資料
- 太子 - 平行世界的當前行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // 取得處理周檢視表所需的位置訊息 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條, 使其更美觀些
完整程式碼
長圖預警, 此處請單擊點開大圖觀看, 也可直接去我的