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

Hello, 各位勇敢的小夥伴, 你們好, 我是大家的嘴強王者小五, 身體健康, 腦子沒病.
 

本人有豐富的脫髮技巧, 能讓你一躍成爲資深大咖.vue

一看就會一寫就廢是本人的主旨, 菜到摳腳是本人的特色, 卑微中透着一絲絲剛強, 傻人有傻福是對我最大的安慰.git

歡迎來到小五隨筆系列手摸手教你用VUE封裝日曆組件.github

寫在前面

雙手奉上代碼連接: 傳送門 - ajun568數組

雙腳奉上最終效果圖:函數

image

需求分析

需求分析無非是一個想要什麼並逐步細化的過程, 畢竟誰都不能一口吃掉一張大餅, 因此咱們先把餅切開, 一點一點吃. 如下基於特定場景來實現一個基本的日曆組件. 小生不才, 還望各位看官輕噴, 歡迎各路大神留言指教.工具

image

場景: 在移動端中經過切換日期來切換收益數據, 展示形式爲上面日曆, 下面對應數據, 只顯示日數據.佈局

基於此場景, 咱們對該日曆功能進行需求分析性能

  • 廣泛場景下, 咱們更傾向當天的數據狀況. 因此基於此, 首次進入應展現當前月份且選中日期爲今日
  • 點選日期, 應能夠準確切換, 不然作它何用, 當🌹瓶嗎
  • 切換月份, 以查看更多數據. 場景基於移動端, 交互方式選擇體驗更好的滑動切換, 左滑切換至上一月, 右滑切換至下一月
  • 滑動切換月份後, 選中該月1號
  • 移動端的展現區域很是寶貴, 減小佔用空間顯得極爲重要, 這時候周視圖就有了用武之地. 交互上可上滑切換至周視圖, 下拉切換回月視圖.
  • 明確月視圖滑動切月, 周視圖滑動切周
  • 滑動切換星期後, 選中該星期的第一天, 若左滑切換後存在1號, 選中1號

image

結構及樣式

先拆分一下日曆, 可將其上下拆分紅兩部分, 上面的 星期 部分, 和下面的 數據 部分, 一週7天限定了列數爲7列, 行數會隨當月天數1號所在位置而有所不一樣.flex

移動端亦應根據屏幕寬度自適應佈局, flex佈局就是一個很好的選擇, 咱們對數據部分進行下模擬, 先造一個長度爲40數據都爲0的數組以下:優化

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

如今, 咱們想要每排顯示7個, 順次下移, 不妨想一下, 若是是你, 你會怎麼作?

  • 父元素設置

    • flex-direction : 用於定義主軸方向
    • flex-wrap : 用於定義是否換行
    • flex-flow : 同時定義flex-directionflex-wrap
  • 子元素設置

    • flex-basis : 用於設置伸縮基準值,可設置具體寬度或百分比,默認值是auto
    • flex-grow : 用於設置放大比例,默認爲0,若是存在剩餘空間,該元素也不會被放大
    • flex-shrink : 用於設置縮小比例,默認爲1,若是空間不足,將等比例縮小。若是設置爲0,則它不會被縮小
    • flex : flex-growflex-shrinkflex-basis的縮寫

綜上, 咱們能夠設置樣式爲 👉🏼     flex: row wrap     flex: 0 0 14.285% (1/7 ≈ 14.285%)

效果圖 👇

image

代碼片斷 👇

image

此時, 能夠加一層結構, 讓子元素寬高固定爲40✖️40, 方便對選中後的樣式進行處理

咱們來隨意勾勒兩筆樣式, 呈現以下 👇

image

展現當前月份及選中當天日期

憑空想象哪有直接上圖片來的直觀, 就像老闆畫的餅哪有money來的實在😏, 接下來咱們結合下面圖片進行進一步的分析, 圖片爲我截取的手機日曆圖

image

首先, 既然是默認選中今天, 咱們就先來獲取下當前日期

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

咱們來看下這張圖片, 不考慮藍框中的部分, 要顯示出當月日期, 咱們只需知道如下兩個點, 而後作for循環就能夠了.

  1. 當前月份的天數
  2. 當前月份第一天應該顯示在什麼位置

這麼一看, 是否是 so easy! 不要太簡單有木有.

image

當月天數

「一三五七八十臘, 三十一天永不差」, 每一年除了二月分平年閏年之外, 其他月份的天數都是固定的, 這麼一看, 這不是區分下二月就完事了嗎

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

🧟‍♂️ Image

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)
},

至此, 一個基本的月視圖就實現完畢了

image

滑動切月

接下來咱們來對月視圖進行優化, 增長滑動切月的功能. 咱們先來看一下實現的效果👇

image

以左滑爲例:

  • 滑動過程當中, 咱們能夠看到部分下個月的數據
  • 滑動距離太小, 自動回彈到當前視圖
  • 滑動超過必定距離, 自動滑至下一個月

touch

做案是須要工具的, 想要觸發滑動事件, 得先找到對應的工具

image

  • touchstart : 手指觸摸屏幕時觸發
  • touchmove : 手指在屏幕中拖動時觸發
  • touchend : 手指離開屏幕時觸發

光靠這個事件, 在滑動過程當中是沒法看到下個月的部分數據的, 想要在滑動過程當中看到數據, 這就是典型的輪播場景. 本質上就是一次transform的過程.

image

此時, 咱們調整下頁面結構, 由對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]中任意值變化時, 而prenext的變化都依附於current的變化, Wow, interesting! watch watch watch !!!

isSelectedCurrentDate - 是否點選的當月日期

❓ 在點選切換數據時, 由於isSelected的變化, watch監聽並執行賦值操做, 但此時並無必要從新生成prenext

translateIndex - 輪播所在位置

用於控制pre, current, next位置, 當觸發滑動切月時, 經過更改translateIndex來更改位置. 在從新賦值時還原到初始值.

touchStartPositionX, touchStartPositionY, touch

這三個是爲了肯定滑動方向及距離的, 向什麼方向滑動? (不要和我說你任性, 就想斜着滑動) 滑動多遠? 鬆手後, 滑動距離小作回彈處理, 滑動距離大作切換處理 (結合translateIndex, 我知道你懂得)

needAnimation - 左右滑動是否須要動畫

image

咱們看圖說話(👆), 是否是感受這個動畫怪怪的, 但又說不清楚哪裏怪, 那是由於在動畫進行中時候, 咱們就對allDataArr進行了賦值操做, 咱們在定時器中延遲下這個賦值操做, 效果以下(👇):

image

是否是有一個明顯的反覆橫跳的過程, 由於咱們滑動過去時候在next, 但最後回到的是current. 這點小問題怎麼能限制住咱們的聰明大腦, 將回到current的動畫去掉, 不就完美解決問題了嗎.

image

賦部分代碼片斷:

image

image

image

切換周視圖

仍是看圖說話, 文字哪有圖片直觀, 咱們來分析下切換周的過程:

image

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的空白處填上對應日期.

image

年和月的填充就不說了, 簡單說下日的填充

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號

滑動切換星期

在視圖切換的過程當中, 與咱們一同上下摩擦的, 仍是陪着咱們不離不棄的preArrnextArr. 既然甩不掉, 何不將它們的價值榨乾到極致, 這樣才符合利益最大化嘛, 咱們對同一橫行的先後數據作狸貓換太子的操做, 將其分別換成當前數據的前一週和後一週, 畢竟破壞纔是更好的創造.

image

要想狸貓換太子, 得先找到那隻狸貓, 在找到太子, 才能進行二者的對調. 咱們以切換至上一週爲例, 來具體找一下狸貓和太子.

  • 狸貓 - 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條, 使其更美觀些

完整代碼

長圖預警, 此處請單擊點開大圖觀看, 也可直接去個人github上查看, 傳送門 - ajun568

image

image

參考🔗連接

Github - 基於 vue 2.0 開發的輕量,高性能日曆組件

相關文章
相關標籤/搜索