手摸手教你用 VUE 封裝日歷組件
寫(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)及樣式
const dataArr = Array(40).fill(0, 0, 40)
父元素設(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ě)



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

// 獲取當(dāng)前日期
getCurrentDate() {
this.selectData = {
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
day: new Date().getDate(),
}
}
當(dāng)前月份的天數(shù) 當(dāng)前月份第一天應(yīng)該顯示在什么位置

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
}
const { year, month } = this.selectData
const monthStartWeekDay = new Date(year, month - 1, 1).getDay()


日期切換及月份切換
// 切換點(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)
}
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)
},

滑動(dòng)切月

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

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

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





切換周視圖

isWeekView: false, // 周視圖還是月視圖
itemHeight: 50, // 日歷行高
lineNum: 0, // 當(dāng)前視圖總行數(shù)
this.lineNum = Math.ceil(this.dataArr.length / 7)
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ǔ)全視圖信息

const nextInfo = this.getNextMonth()
let nextObj = {
type: 'next',
day: i + 1,
month: nextInfo.month,
year: nextInfo.year,
}
const preInfo = this.getPreMonth(date)
let preObj = {
type: 'pre',
day: daysInMonth[preInfo.month - 1] - (monthStartWeekDay - i - 1),
month: preInfo.month,
year: preInfo.year,
}
滑動(dòng)切換星期

貍貓 - lastWeek
太子 - 平行世界的當(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)化代碼
一些蹩腳的動(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條, 使其更美觀些
完整代碼


評(píng)論
圖片
表情
