手摸手教你用 VUE 封装日历组件
写在前面
双手奉上代码链接:
https://github.com/ajun568/vue-calendar
双脚奉上最终效果图:
需求分析
需求分析无非是一个想要什么并逐步细化的过程, 毕竟谁都不能一口吃掉一张大饼, 所以我们先把饼切开, 一点一点吃. 以下基于特定场景来实现一个基本的日历组件. 小生不才, 还望各位看官轻喷, 欢迎各路大神留言指教.
场景: 在移动端中通过切换日期来切换收益数据, 展现形式为上面日历, 下面对应数据, 只显示日数据.
基于此场景, 我们对该日历功能进行需求分析
普遍场景下, 我们更倾向当天的数据情况. 所以基于此, 首次进入应展示当前月份且选中日期为今日 点选日期, 应可以准确切换, 否则做它何用, 当🌹瓶吗 切换月份, 以查看更多数据. 场景基于移动端, 交互方式选择体验更好的滑动切换, 左滑切换至上一月, 右滑切换至下一月 滑动切换月份后, 选中该月1号 移动端的展示区域非常宝贵, 减少占用空间显得极为重要, 这时候周视图就有了用武之地. 交互上可上滑切换至周视图, 下拉切换回月视图. 明确月视图滑动切月, 周视图滑动切周 滑动切换星期后, 选中该星期的第一天, 若左滑切换后存在1号, 选中1号
结构及样式
const dataArr = Array(40).fill(0, 0, 40)
父元素设置 flex-direction : 用于定义主轴方向 flex-wrap : 用于定义是否换行 flex-flow : 同时定义flex-direction和flex-wrap 子元素设置 flex-basis : 用于设置伸缩基准值,可设置具体宽度或百分比,默认值是auto flex-grow : 用于设置放大比例,默认为0,如果存在剩余空间,该元素也不会被放大 flex-shrink : 用于设置缩小比例,默认为1,如果空间不足,将等比例缩小。如果设置为0,则它不会被缩小 flex : flex-grow、flex-shrink和flex-basis的缩写
展示当前月份及选中当天日期
// 获取当前日期
getCurrentDate() {
this.selectData = {
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
day: new Date().getDate(),
}
}
当前月份的天数 当前月份第一天应该显示在什么位置
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()
日期切换及月份切换
// 切换点选日期
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)
}
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)
},
滑动切月
滑动过程中, 我们可以看到部分下个月的数据 滑动距离过小, 自动回弹到当前视图 滑动超过一定距离, 自动滑至下一个月
touchstart : 手指触摸屏幕时触发 touchmove : 手指在屏幕中拖动时触发 touchend : 手指离开屏幕时触发
allDataArr: [], // 轮播数组
isSelectedCurrentDate: false, // 是否点选的当月日期
translateIndex: 0, // 轮播所在位置
transitionDuration: 0.3, // 动画持续时间
needAnimation: true, // 左右滑动是否需要动画
isTouching: false, // 是否为滑动状态
touchStartPositionX: null, // 初始滑动X的值
touchStartPositionY: null, // 初始滑动Y的值
touch: { // 本次touch事件,横向,纵向滑动的距离的百分比
x: 0,
y: 0,
},
切换周视图
isWeekView: false, // 周视图还是月视图
itemHeight: 50, // 日历行高
lineNum: 0, // 当前视图总行数
this.lineNum = Math.ceil(this.dataArr.length / 7)
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)
},
补全视图信息
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,
}
滑动切换星期
狸猫 - lastWeek
太子 - 平行世界的当前行
// 获取处理周视图所需的位置信息
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条, 使其更美观些
完整代码
评论