<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手摸手教你用 VUE 封裝日歷組件

          共 12175字,需瀏覽 25分鐘

           ·

          2021-03-14 13:31

          作者:黃刀小五
          來(lái)源:SegmentFault 思否社區(qū)




          寫(xiě)在前面


          雙手奉上代碼鏈接: 

          https://github.com/ajun568/vue-calendar


          雙腳奉上最終效果圖:





          需求分析


          需求分析無(wú)非是一個(gè)想要什么并逐步細(xì)化的過(guò)程, 畢竟誰(shuí)都不能一口吃掉一張大餅, 所以我們先把餅切開(kāi), 一點(diǎn)一點(diǎn)吃. 以下基于特定場(chǎng)景來(lái)實(shí)現(xiàn)一個(gè)基本的日歷組件. 小生不才, 還望各位看官輕噴, 歡迎各路大神留言指教.



          場(chǎng)景: 在移動(dòng)端中通過(guò)切換日期來(lái)切換收益數(shù)據(jù), 展現(xiàn)形式為上面日歷, 下面對(duì)應(yīng)數(shù)據(jù), 只顯示日數(shù)據(jù).


          基于此場(chǎng)景, 我們對(duì)該日歷功能進(jìn)行需求分析


          • 普遍場(chǎng)景下, 我們更傾向當(dāng)天的數(shù)據(jù)情況. 所以基于此, 首次進(jìn)入應(yīng)展示當(dāng)前月份且選中日期為今日
          • 點(diǎn)選日期, 應(yīng)可以準(zhǔn)確切換, 否則做它何用, 當(dāng)??瓶嗎
          • 切換月份, 以查看更多數(shù)據(jù). 場(chǎng)景基于移動(dòng)端, 交互方式選擇體驗(yàn)更好的滑動(dòng)切換, 左滑切換至上一月, 右滑切換至下一月
          • 滑動(dòng)切換月份后, 選中該月1號(hào)
          • 移動(dòng)端的展示區(qū)域非常寶貴, 減少占用空間顯得極為重要, 這時(shí)候周視圖就有了用武之地. 交互上可上滑切換至周視圖, 下拉切換回月視圖.
          • 明確月視圖滑動(dòng)切月, 周視圖滑動(dòng)切周
          • 滑動(dòng)切換星期后, 選中該星期的第一天, 若左滑切換后存在1號(hào), 選中1號(hào)




          結(jié)構(gòu)及樣式


          先拆分一下日歷, 可將其上下拆分成兩部分, 上面的 星期 部分, 和下面的 數(shù)據(jù) 部分, 一周7天限定了列數(shù)為7列, 行數(shù)會(huì)隨當(dāng)月天數(shù)及1號(hào)所在位置而有所不同.

          移動(dòng)端亦應(yīng)根據(jù)屏幕寬度自適應(yīng)布局, flex布局就是一個(gè)很好的選擇, 我們對(duì)數(shù)據(jù)部分進(jìn)行下模擬, 先造一個(gè)長(zhǎng)度為40數(shù)據(jù)都為0的數(shù)組如下:

          const dataArr = Array(40).fill(0, 0, 40)

          現(xiàn)在, 我們想要每排顯示7個(gè), 順次下移, 不妨想一下, 如果是你, 你會(huì)怎么做?

          • 父元素設(shè)置
            • flex-direction : 用于定義主軸方向
            • flex-wrap : 用于定義是否換行
            • flex-flow : 同時(shí)定義flex-direction和flex-wrap
          • 子元素設(shè)置
            • flex-basis : 用于設(shè)置伸縮基準(zhǔn)值,可設(shè)置具體寬度或百分比,默認(rèn)值是auto
            • flex-grow : 用于設(shè)置放大比例,默認(rèn)為0,如果存在剩余空間,該元素也不會(huì)被放大
            • flex-shrink : 用于設(shè)置縮小比例,默認(rèn)為1,如果空間不足,將等比例縮小。如果設(shè)置為0,則它不會(huì)被縮小
            • flex : flex-grow、flex-shrink和flex-basis的縮寫(xiě)

          綜上, 我們可以設(shè)置樣式為 ????   父   flex: row wrap   子   flex: 0 0 14.285% (1/7 ≈ 14.285%)

          效果圖 ??


          代碼片段 ??


          此時(shí), 可以加一層結(jié)構(gòu), 讓子元素寬高固定為40??40, 方便對(duì)選中后的樣式進(jìn)行處理

          我們來(lái)隨意勾勒兩筆樣式, 呈現(xiàn)如下 ??




          展示當(dāng)前月份及選中當(dāng)天日期


          憑空想象哪有直接上圖片來(lái)的直觀, 就像老板畫(huà)的餅?zāi)挠衜oney來(lái)的實(shí)在??, 接下來(lái)我們結(jié)合下面圖片進(jìn)行進(jìn)一步的分析, 圖片為我截取的手機(jī)日歷圖


          首先, 既然是默認(rèn)選中今天, 我們就先來(lái)獲取下當(dāng)前日期

          // 獲取當(dāng)前日期
          getCurrentDate() {
            this.selectData = {
              year: new Date().getFullYear(),
              month: new Date().getMonth() + 1,
              day: new Date().getDate(),
            }
          }

          我們來(lái)看下這張圖片, 不考慮藍(lán)框中的部分, 要顯示出當(dāng)月日期, 我們只需知道以下兩個(gè)點(diǎn), 然后做for循環(huán)就可以了.

          1. 當(dāng)前月份的天數(shù)
          2. 當(dāng)前月份第一天應(yīng)該顯示在什么位置

          這么一看, 是不是 so easy! 不要太簡(jiǎn)單有木有.


          當(dāng)月天數(shù)

          “一三五七八十臘, 三十一天永不差”, 每年除了二月分平年閏年以外, 其余月份的天數(shù)都是固定的, 這么一看, 這不是區(qū)分下二月就完事了嗎

          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
          }

          當(dāng)月第一天的位置

          想知道當(dāng)月第一天的位置, 換個(gè)思路想, 其實(shí)就是想知道當(dāng)月第一天是星期幾, 誒, 這不是巧了嗎, 拿當(dāng)月第一天的日期 getDay() 這不就完事了嗎

          const { year, month } = this.selectData
          const monthStartWeekDay = new Date(year, month - 1, 1).getDay()


          接下來(lái)我們填充下數(shù)據(jù), 前后做留白處理, 代碼及效果如下:

          ???♂? Code


          ???♂? Image




          日期切換及月份切換


          日期切換 = 更改當(dāng)前數(shù)組中子元素的isSelected

          // 切換點(diǎn)選日期
          checkoutDate(selectData) {
            if (selectData.type !== 'normal'return // 非有效日期不可點(diǎn)選

            this.selectData.day = selectData.day // 對(duì)選中日期賦值

             // 查找當(dāng)前選中日期的索引
            const oldSelectIndex = this.dataArr.findIndex(item => item.isSelected && item.type === 'normal')
            // 查找新切換日期的索引 (tips: 這里也可以直接把索引值傳過(guò)來(lái) -> 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)
          }

          月份切換 = 重新生成新月份所對(duì)應(yīng)的dataArr, 并選中當(dāng)月1號(hào)

          tips: 這里需要注意的點(diǎn)是, 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)
          },

          至此, 一個(gè)基本的月視圖就實(shí)現(xiàn)完畢了




          滑動(dòng)切月


          接下來(lái)我們來(lái)對(duì)月視圖進(jìn)行優(yōu)化, 增加滑動(dòng)切月的功能. 我們先來(lái)看一下實(shí)現(xiàn)的效果??


          以左滑為例:

          • 滑動(dòng)過(guò)程中, 我們可以看到部分下個(gè)月的數(shù)據(jù)
          • 滑動(dòng)距離過(guò)小, 自動(dòng)回彈到當(dāng)前視圖
          • 滑動(dòng)超過(guò)一定距離, 自動(dòng)滑至下一個(gè)月

          touch

          作案是需要工具的, 想要觸發(fā)滑動(dòng)事件, 得先找到對(duì)應(yīng)的工具


          • touchstart : 手指觸摸屏幕時(shí)觸發(fā)
          • touchmove : 手指在屏幕中拖動(dòng)時(shí)觸發(fā)
          • touchend : 手指離開(kāi)屏幕時(shí)觸發(fā)

          光靠這個(gè)事件, 在滑動(dòng)過(guò)程中是無(wú)法看到下個(gè)月的部分?jǐn)?shù)據(jù)的, 想要在滑動(dòng)過(guò)程中看到數(shù)據(jù), 這就是典型的輪播場(chǎng)景. 本質(zhì)上就是一次transform的過(guò)程.


          此時(shí), 我們調(diào)整下頁(yè)面結(jié)構(gòu), 由對(duì)dataArr的單層循環(huán)改為雙層循環(huán)模式, 其本質(zhì)就是上圖所示的[pre, current, next]數(shù)組

          此步驟涉及的代碼改動(dòng)較多, 接下來(lái)主要通過(guò)新引入的變量來(lái)捋清思路, 思路清晰了, 代碼順其自然就好, ?? Let's go, come on baby!

          allDataArr: [], // 輪播數(shù)組
          isSelectedCurrentDate: false, // 是否點(diǎn)選的當(dāng)月日期
          translateIndex: 0, // 輪播所在位置
          transitionDuration: 0.3, // 動(dòng)畫(huà)持續(xù)時(shí)間
          needAnimation: true, // 左右滑動(dòng)是否需要?jiǎng)赢?huà)
          isTouching: false, // 是否為滑動(dòng)狀態(tài)
          touchStartPositionX: null, // 初始滑動(dòng)X的值
          touchStartPositionY: null, // 初始滑動(dòng)Y的值
          touch: { // 本次touch事件,橫向,縱向滑動(dòng)的距離的百分比
            x: 0,
            y: 0,
          },

          allDataArr - 輪播數(shù)組

          ? 什么時(shí)候?qū)@個(gè)數(shù)組進(jìn)行賦值

          ??? 當(dāng)[pre, current, next]中任意值變化時(shí), 而pre和next的變化都依附于current的變化, Wow, interesting! watch watch watch !!!

          isSelectedCurrentDate - 是否點(diǎn)選的當(dāng)月日期

          ? 在點(diǎn)選切換數(shù)據(jù)時(shí), 因?yàn)閕sSelected的變化, watch監(jiān)聽(tīng)并執(zhí)行賦值操作, 但此時(shí)并沒(méi)有必要重新生成pre和next

          translateIndex - 輪播所在位置

          用于控制pre, current, next位置, 當(dāng)觸發(fā)滑動(dòng)切月時(shí), 通過(guò)更改translateIndex來(lái)更改位置. 在重新賦值時(shí)還原到初始值.

          touchStartPositionX, touchStartPositionY, touch

          這三個(gè)是為了確定滑動(dòng)方向及距離的, 向什么方向滑動(dòng)? (不要和我說(shuō)你任性, 就想斜著滑動(dòng)) 滑動(dòng)多遠(yuǎn)? 松手后, 滑動(dòng)距離小做回彈處理, 滑動(dòng)距離大做切換處理 (結(jié)合translateIndex, 我知道你懂得)

          needAnimation - 左右滑動(dòng)是否需要?jiǎng)赢?huà)


          我們看圖說(shuō)話(??), 是不是感覺(jué)這個(gè)動(dòng)畫(huà)怪怪的, 但又說(shuō)不清楚哪里怪, 那是因?yàn)樵趧?dòng)畫(huà)進(jìn)行中時(shí)候, 我們就對(duì)allDataArr進(jìn)行了賦值操作, 我們?cè)诙〞r(shí)器中延遲下這個(gè)賦值操作, 效果如下(??):


          是不是有一個(gè)明顯的反復(fù)橫跳的過(guò)程, 因?yàn)槲覀兓瑒?dòng)過(guò)去時(shí)候在next, 但最后回到的是current. 這點(diǎn)小問(wèn)題怎么能限制住我們的聰明大腦, 將回到current的動(dòng)畫(huà)去掉, 不就完美解決問(wèn)題了嗎.

          賦部分代碼片段:




          切換周視圖


          還是看圖說(shuō)話, 文字哪有圖片直觀, 我們來(lái)分析下切換周的過(guò)程:


          Bingo, 就是一個(gè)transformY+height的過(guò)程

          ?? 對(duì)于height, 無(wú)非是總高度到單行高度反復(fù)橫跳的過(guò)程, 每行高度是固定的, 總高度=單行高度*總行數(shù)

          isWeekView: false, // 周視圖還是月視圖
          itemHeight: 50, // 日歷行高
          lineNum: 0, // 當(dāng)前視圖總行數(shù)

          this.lineNum = Math.ceil(this.dataArr.length / 7)

          ?? 對(duì)于transformY, 其移動(dòng)距離=(當(dāng)前所在行數(shù)-1)*單行高度

          offsetY: 0, // 周視圖 Y軸偏移量

          // 處理周視圖的數(shù)據(jù)變化
          dealWeekViewData() {
            const selectedIndex = this.dataArr.findIndex(item => item.isSelected)
            const indexOfLine = Math.ceil((selectedIndex + 1) / 7)
            this.offsetY = -((indexOfLine - 1) * this.itemHeight)
          },



          補(bǔ)全視圖信息


          在做周視圖的滑動(dòng)切換之前, 我們來(lái)補(bǔ)全一下視圖信息, 將daraArr的空白處填上對(duì)應(yīng)日期.


          年和月的填充就不說(shuō)了, 簡(jiǎn)單說(shuō)下日的填充

          next比較簡(jiǎn)單, 循環(huán)次數(shù)=7-最后一行天數(shù)=7-次月1日的星期索引 (tip: 需要注意的是, 若次月1日索引為0, 代表無(wú)空白處可填充, 自然也無(wú)需循環(huán)), day的賦值從1號(hào)順次增加即可.

          const nextInfo = this.getNextMonth()

          let nextObj = {
            type'next',
            day: i + 1,
            month: nextInfo.month,
            year: nextInfo.year,
          }

          再來(lái)說(shuō)說(shuō)pre, 循環(huán)次數(shù)=7-第一行天數(shù)=當(dāng)月1號(hào)的星期索引, day的賦值等于上月日期的倒序 => 上月天數(shù) - (當(dāng)月1號(hào)星期索引 - (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()函數(shù)傳date的原因

          ??? 說(shuō)白了, date就是參照物唄, 對(duì)誰(shuí)取上個(gè)月就傳誰(shuí); 而getNextMonth()為什么不傳呢, 單純的無(wú)所謂, 傳與不傳它都是從1遞增, 誰(shuí)又會(huì)在一個(gè)無(wú)關(guān)緊要的事上浪費(fèi)感情呢.

          點(diǎn)選非本月日期時(shí), 對(duì)應(yīng)做切換月份的處理即可, 此時(shí)切換后的日期為點(diǎn)選日期, 而非1號(hào)



          滑動(dòng)切換星期


          在視圖切換的過(guò)程中, 與我們一同上下摩擦的, 還是陪著我們不離不棄的preArr和nextArr. 既然甩不掉, 何不將它們的價(jià)值榨干到極致, 這樣才符合利益最大化嘛, 我們對(duì)同一橫行的前后數(shù)據(jù)做貍貓換太子的操作, 將其分別換成當(dāng)前數(shù)據(jù)的前一周和后一周, 畢竟破壞才是更好的創(chuàng)造.


          要想貍貓換太子, 得先找到那只貍貓, 在找到太子, 才能進(jìn)行兩者的對(duì)調(diào). 我們以切換至上一周為例, 來(lái)具體找一下貍貓和太子.

          • 貍貓 - lastWeek

          No.1 如果非首行數(shù)據(jù), 上周=上一行. 通過(guò)當(dāng)前行數(shù), 拿到兩端數(shù)據(jù)的索引, 分別減7獲取上一周兩端數(shù)據(jù)的索引, 進(jìn)而拿到上一周的數(shù)據(jù).

          No.2 如果當(dāng)前為首行, 又可進(jìn)一步劃分為: 首個(gè)數(shù)據(jù)項(xiàng)是否為1號(hào), 若是, 則取上個(gè)月最后一行數(shù)據(jù); 若否, 則取上個(gè)月倒數(shù)第二行數(shù)據(jù)(tips: 此時(shí)上個(gè)月最后一行等同于當(dāng)前首行); 以上兩點(diǎn), 也可考慮成查找特定日期在上個(gè)月的所在行.

          • 太子 - 平行世界的當(dāng)前行

          // 獲取處理周視圖所需的位置信息
          getInfoOfWeekView(selectedIndex, length) {
            const indexOfLine = Math.ceil((selectedIndex + 1) / 7) // 當(dāng)前行數(shù)
            const totalLine = Math.ceil(length / 7) // 總行數(shù)
            const sliceStart = (indexOfLine - 1) * 7 // 當(dāng)前行左端索引
            const sliceEnd = sliceStart + 7 // 當(dāng)前行右端索引

            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)

            // 前一周數(shù)據(jù)
            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)
            }

            // 后一周數(shù)據(jù)
            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)
          },



          優(yōu)化代碼


          到這里基本就大功告成了, 我們總結(jié)下剩下的問(wèn)題并加以處理, 阿拉霍洞開(kāi)

          • 一些蹩腳的動(dòng)畫(huà): 此場(chǎng)景下, 一切奇怪的動(dòng)畫(huà)都是由transitionDuration導(dǎo)致的, 所以我們要想清楚什么時(shí)候需要?jiǎng)赢?huà), 什么時(shí)候不需要, 不需要時(shí)候賦值為0就好了

          • 類似卡頓的效果: 此場(chǎng)景下, 幾乎所有的卡頓、延遲, 都是那個(gè)萬(wàn)惡的setTimeout導(dǎo)致的, 所以要想好什么時(shí)候需要它, 什么時(shí)候果斷舍棄它

          • 最后加個(gè)底部的touch條, 使其更美觀些




          完整代碼


          此處請(qǐng)直接去我的github上查看, 傳送門(mén):
          https://github.com/ajun568/vue-calendar




          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開(kāi)更多互動(dòng)和交流,掃描下方”二維碼“或在“公眾號(hào)后臺(tái)回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 29
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  影音先锋青青草视频 | 五月天Av成人在线播放 | 女人高潮在线看91 | 国产日产久久高清欧美 | 久操视频在线免费观看 |