1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        手摸手教你用 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)的直觀(guān), 就像老板畫(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ō)話(huà)(??), 是不是感覺(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ō)話(huà), 文字哪有圖片直觀(guān), 我們來(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就好了

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

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




        完整代碼


        此處請(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)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            韩国黄色1级片 | 一边揉着胸一边舌吻拉丝 | 亲子伦视频一区二区 | 《韩国激情合集无删减在线观看》 | 成人高清无码视频在线播放观看 | 一级毛在线观看 | 激情综合五月天 | 99热官方网站 | 亚洲婷婷视频 | 亚洲中文无码影视 |