关于前端:手摸手教你用VUE封装日历组件

47次阅读

共计 7796 个字符,预计需要花费 20 分钟才能阅读完成。

Hello, 各位怯懦的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.
 

自己有丰盛的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是自己的宗旨, 菜到抠脚是自己的特点, 低微中透着一丝丝坚强, 傻人有傻福是对我最大的刺激.

欢送来到 小五 随笔系列 手摸手教你用 VUE 封装日历组件.

写在后面

双手奉上代码链接: 传送门 – ajun568

双脚奉上最终效果图:

需要剖析

需要剖析无非是一个想要什么并逐渐细化的过程, 毕竟谁都不能一口吃掉一张大饼, 所以咱们先把饼切开, 一点一点吃. 以下基于特定场景来实现一个根本的日历组件. 小生不才, 还望各位看官轻喷, 欢送各路大神留言指教.

场景: 在 挪动端 中通过 切换日期 来切换收益数据, 展示模式为下面日历, 上面对应数据, 只显示 日数据.

基于此场景, 咱们对该日历性能进行需要剖析

  • 广泛场景下, 咱们更偏向当天的数据状况. 所以基于此, 首次进入应展现以后月份且选中日期为今日
  • 点选日期, 应能够精确切换, 否则做它何用, 当???? 瓶吗
  • 切换月份, 以查看更多数据. 场景基于挪动端, 交互方式抉择体验更好的滑动切换, 左滑切换至上一月, 右滑切换至下一月
  • 滑动切换月份后, 选中该月 1 号
  • 挪动端的展现区域十分贵重, 缩小占用空间显得极为重要, 这时候周视图就有了用武之地. 交互上可上滑切换至周视图, 下拉切换回月视图.
  • 明确月视图滑动切月, 周视图滑动切周
  • 滑动切换星期后, 选中该星期的第一天, 若左滑切换后存在 1 号, 选中 1 号

构造及款式

先拆分一下日历, 可将其高低拆分成两局部, 下面的 星期 局部, 和上面的 数据 局部, 一周 7 天限定了列数为 7 列, 行数会随 当月天数 1 号所在位置 而有所不同.

挪动端亦应依据屏幕宽度自适应布局, 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%)

效果图 ????

代码片段 ????

此时, 能够加一层构造, 让子元素宽高固定为 40✖️40, 不便对选中后的款式进行解决

咱们来随便勾画两笔款式, 出现如下 ????

展现以后月份及选中当天日期

凭空想象哪有间接上图片来的直观, 就像老板画的饼哪有 money 来的切实????, 接下来咱们联合上面图片进行进一步的剖析, 图片为我截取的手机日历图

首先, 既然是默认选中明天, 咱们就先来获取下以后日期

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

咱们来看下这张图片, 不思考蓝框中的局部, 要显示出当月日期, 咱们只需晓得以下两个点, 而后做 for 循环就能够了.

  1. 以后月份的天数
  2. 以后月份第一天应该显示在什么地位

这么一看, 是不是 so easy! 不要太简略有木有.

当月天数

“一三五七八十腊, 三十一天永不差”, 每年除了二月分平年平年以外, 其余月份的天数都是固定的, 这么一看, 这不是辨别下二月就完事了吗

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

日期切换及月份切换

日期切换 = 更改以后数组中子元素的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)
},

至此, 一个根本的月视图就实现结束了

滑动切月

接下来咱们来对月视图进行优化, 减少滑动切月的性能. 咱们先来看一下实现的成果????

以左滑为例:

  • 滑动过程中, 咱们能够看到局部下个月的数据
  • 滑动间隔过小, 主动回弹到以后视图
  • 滑动超过肯定间隔, 主动滑至下一个月

touch

作案是须要工具的, 想要触发滑动事件, 得先找到对应的工具

  • touchstart : 手指触摸屏幕时触发
  • touchmove : 手指在屏幕中拖动时触发
  • touchend : 手指来到屏幕时触发

光靠这个事件, 在滑动过程中是无奈看到下个月的局部数据的, 想要在滑动过程中看到数据, 这就是典型的轮播场景. 实质上就是一次 transform 的过程.

此时, 咱们调整下页面构造, 由对 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 – 左右滑动是否须要动画

咱们看图谈话 (????), 是不是感觉这个动画怪怪的, 但又说不清楚哪里怪, 那是因为在动画进行中时候, 咱们就对allDataArr 进行了赋值操作, 咱们在定时器中提早下这个赋值操作, 成果如下(????):

是不是有一个显著的重复横跳的过程, 因为咱们滑动过来时候在 next, 但最初回到的是current. 这点小问题怎么能限制住咱们的聪慧大脑, 将回到current 的动画去掉, 不就完满解决问题了吗.

赋局部代码片段:

切换周视图

还是看图谈话, 文字哪有图片直观, 咱们来剖析下切换周的过程:

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 的空白处填上对应日期.

年和月的填充就不说了, 简略说下日的填充

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. 既然甩不掉, 何不将它们的价值榨干到极致, 这样才合乎利益最大化嘛, 咱们对同一横行的前后数据做狸猫换太子的操作, 将其别离换成以后数据的前一周和后一周, 毕竟毁坏才是更好的发明.

要想狸猫换太子, 得先找到那只狸猫, 在找到太子, 能力进行两者的对调. 咱们以切换至上一周为例, 来具体找一下狸猫和太子.

  • 狸猫 – 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

参考???? 链接

Github – 基于 vue 2.0 开发的轻量,高性能日历组件

正文完
 0