最详细的Vue实现日历组件Calendar(日期点击多选,滑动多选)

1. 成果演示

1.1 日期的切换

最详细的Vue实现日历组件Calendar(日期点击多选,滑动多选)_第1张图片

1.2 点击多选

最详细的Vue实现日历组件Calendar(日期点击多选,滑动多选)_第2张图片

1.3 滑动多选

最详细的Vue实现日历组件Calendar(日期点击多选,滑动多选)_第3张图片

2. 实现基本的日期渲染

2.1 思路

2.1.1 要用到的Date对象方法

  1. new Date(2020, 4, 1).getDay() 计算传入的日期是星期几,返回值0表示星期天1表示星期一,以此类推…
  2. new Date(2020, 4, 0).getDate()计算传入月份的该月总天数,第三个参数为0才是计算总天数!!

2.1.2 剖析日历日期结构

  1. 一个日期表格六行七列,共42个日期,这个是固定的(重要!)
  2. 一般情况要展示上个月,本月,下个月共三个月的日期,但有种特殊情况是当本月第一天是星期一,那么就只展示本月和下个月的日期。
  3. 还有注意在每年的1月和12月,它们分别的上一月和下一月的年份都应该-1+1

2.1.3 如何计算上个月的日期

当本月第一天不是星期一,那么必然会展示上月的日期

  1. 计算该月第一天是星期几(n0 <= n <= 6),那么上一个月就会展示n-1天,注意当n为0时,n要赋值为7
  2. 计算上一个月有多少天
  3. 循环添加日期 - 循环起始: 上个月天数 - n + 2循环结束:<= 上个月天数

2.1.4 如何计算下个月的日期

  1. 当前日期总共42个减去该月天数再减去n-1,就得到下个月要展示的天数
  2. 循环添加日期:循环起始:1,循环结束:<= 第一步结果

2.1.5 二维数组渲染生成日历

上面的步骤生成的上个月日期以及下个月日期本月日期,三个数组组合到一起刚好是一个长度42的一维数组。但我们的table表格是tr -> td这样的结构,所以我们必须要把一维数组转成二维数组,这样才能遍历生成基础的日历样式。

2.2 实现代码

2.2.1 Calendar.vue

<template>
  <div class="calendar">
    <table class="calendar-table">
      <thead>
        <tr>
          <th v-for="(item, i) in weeks" :key="i">{{ item }}th>
        tr>
      thead>
      <tbody>
        <tr v-for="(dates, i) in res" :key="i">
          <td
            v-for="(item, index) in dates"
            :key="index"
            :class="{notCurMonth: !item.isCurMonth, currentDay: item.date === curDate}"
          >
            
            <span>{{ item.date }}span>
            <slot :data="item" />
          td>
        tr>
      tbody>
    table>
  div>
template>
<script>
	data() {
	  return {
	    weeks: ['一', '二', '三', '四', '五', '六', '日'],
	    curYear: new Date().getFullYear(), // 当前年
	    curMonth: new Date().getMonth(), // 当前月
	    days: 0, // 当前月总共天数
	    curDate: parseTime(new Date().getTime()), // 当前日期 yyyy-MM-dd 格式,用来匹配是否是当前日期
	    prevDays: [], // 非当前月的上一月展示的日期
	    rearDays: [], // 非当前月的下一月展示的日期
	    curDays: [], // 当前月的日期
	    showDays: [], // 总共展示的42个日期
	    res: [], // 二维数组
	  }
	},
	created() {
		// 默认渲染当前月
		this.handleGetDays(this.curYear, this.curMonth)
	},
	methods() {
		handleGetDays(year, month) {
	      this.showDays = []
	      this.days = getDaysInMonth(year, month)
	      let firstDayOfWeek = new Date(`${year}-${month + 1}-01`).getDay()

	      if (firstDayOfWeek === 0) { // 星期天为0 星期一为1 ,以此类推
	        firstDayOfWeek = 7
	      }
	      this.prevDays = handleCrateDate(year, month, 1, firstDayOfWeek, 'prev')
	      this.rearDays = handleCrateDate(year, month, 1, 42 - this.days - (firstDayOfWeek - 1), 'rear') 
	
	      this.curDays = handleCrateDate(year, month, 1, this.days)
	      this.showDays.unshift(...this.prevDays)
	      this.showDays.push(...this.curDays)
	      this.showDays.push(...this.rearDays)
	      // console.log(this.showDays)
	      this.res = this.handleFormatDates(this.showDays)
	   },
	   handleFormatDates(arr, size = 7) { // 传入长度42的原数组,最终转换成二维数组
	      const arr2 = []
	      for (let i = 0; i < size; i++) {
	        const temp = arr.slice(i * size, i * size + size)
	        arr2.push(temp)
	      }
	      console.log(arr2)
	      return arr2
	  },
	}
</script>

2.2.2 index.js

// 获取该月的天数
export const getDaysInMonth = (year, month) => {
  const day = new Date(year, month + 1, 0)
  return day.getDate()
}

// 创建日期 yyyy-MM-dd 格式, 用于创建非当前月的日期
export const handleCrateDate = (year, month, start, end, type) => {
  const arr = []
  if (type === 'prev') { // 上一月
    if (start === end) return []
    const daysInLastMonth = getDaysInMonth(year, month - 1) // 获取上一个月有多少天
    for (let i = daysInLastMonth - end + 2; i <= daysInLastMonth; i++) {
      arr.push({
        date: parseTime(new Date(year, month - 1, i)),
        isCurMonth: false
      })
    }
  } else if (type === 'rear') { // 下一月
    for (let i = start; i <= end; i++) {
      arr.push({
        date: parseTime(new Date(year, month + 1, i)),
        isCurMonth: false
      })
    }
  } else { // 本月
    for (let i = start; i <= end; i++) {
      arr.push({
        date: parseTime(new Date(year, month, i)),
        isCurMonth: true
      })
    }
  }
  return arr
}
export function parseTime(time, cFormat) {
  if (arguments.length === 0 || !time) {
    return null
  }
  const format = cFormat || '{y}-{m}-{d}'
  let date
  if (typeof time === 'object') {
    date = time
  } else {
    if ((typeof time === 'string')) {
      if ((/^[0-9]+$/.test(time))) {
        // support "1548221490638"
        time = parseInt(time)
      } else {
        // support safari
        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
        time = time.replace(new RegExp(/-/gm), '/')
      }
    }

    if ((typeof time === 'number') && (time.toString().length === 10)) {
      time = time * 1000
    }
    date = new Date(time)
  }
  const formatObj = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay()
  }
  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
    const value = formatObj[key]
    // Note: getDay() returns 0 on Sunday
    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
    return value.toString().padStart(2, '0')
  })
  return time_str
}

2.2.3 生成最基本的日历

最详细的Vue实现日历组件Calendar(日期点击多选,滑动多选)_第4张图片
ps: notCurMonthcurrentDay这两个类名自己写,示例没有贴出来,不然就全是一个颜色。

3. 实现年月的切换

3.1 思路

就是监听下拉框的值变化。然后调用handleGetDays方法。
最后帖完整代码

4. 实现日期的选中(点击,滑动)

4.1 点击选中

4.1.1 思路

  1. td绑定点击事件,传入三个参数: item -> 值、i -> 所在行、j -> 所在列
  2. 通过res[i][j]可以拿到当前点击的某一项,先把它的isSelected属性取反,然后判断它的isSelected是否为true,如果是,把当前项push进selectedDates数组,进行去重操作(Array.from(new Set(this.selectedDates)))。如果不是,找到该项索引,并且在selectedDates数组里删掉它(this.selectedDates.splice(this.selectedDates.indexOf(item.date), 1)
  3. 最后贴完整代码

4.2 滑动选中

4.2.1 思路

  1. 滑动模式下,第一次点击是开始,第二次点击是结束,存在moveIndex数组里面
  2. 建立一个canMove状态,第一次点击之后,可以滑动,第二次点击之后,不能滑动
  3. 滑动用mouseover监听,因为触发频率较低,也是传入item,i,j三个参数,并且鼠标停留到的元素的索引计算方法为 i * 7 + j
  4. 总共选中的数组为 this.selectedDates = this.showDays.slice(this.moveIndex[0], this.moveIndex[1] + 1)
  5. 遍历循环在第一次点击的索引和第二次点击的索引之间, 给这些元素添加一个isRangeSelected状态,用以加颜色。第一次点击和最后一次点击的索引是moveIndex[0]i * 7 + j,可以给他们单独加样式,用以区分。

5. 周起始日的改变

5.1 切换表头的中文

一开始定义的表头是固定的:weeks: ['一', '二', '三', '四', '五', '六', '日']

当我们拿到传入的周起始日,我们可以使用数组的spliceunshift进行重新排序。

最终实现:
this.weeks.unshift(...this.weeks.splice(this.startOfWeek - 1))

5.2 切换表格内容

5.2.1 思路

  1. 定义一个对象列举出日期的罗马数字和中文数字
const obj = {
    1: '一',
    2: '二',
    3: '三',
    4: '四',
    5: '五',
    6: '六',
    0: '日'
  }
  1. 获取到月起始日的中文(‘一’,…)
  2. 再用indexOf方法拿到该月起始日中文在weeks数组里的索引
  3. 获取出来的索引即使上一个月要展示的日期天数,传入handleGetDay()函数即可

代码一并贴在后面

6. 父组件中调用

6.1 Attributes

参数 说明 类型 可选值 默认值
can-select 是否开启选择日期项 Boolean true/false false
selectMode 选择模式:点击/滑动 String click/move click
startOfWeek 周起始日 Number [1,7] 1
width 整个日历的宽度 String - 70%
tbodyHeight 日期的高度 String - 60px

6.2 Events

事件名 说明 参数
dateSelected 当用户开启选择日期触发 selection

7. 完整代码

7.1 Calendar.vue

<template>
  <div class="calendar">
    <div class="select">
      <el-form inline>
        <el-form-item>
          <el-select v-model="curYear" placeholder="请选择">
            <el-option v-for="item in yearOptions" :key="item.key" :value="item.value" :label="item.label" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-select v-model="curMonth" placeholder="请选择">
            <el-option v-for="item in monthOptions" :key="item.key" :value="item.value" :label="item.label" />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuickChange('prev')">上一月</el-button>
          <el-button type="primary" @click="handleQuickChange('next')">下一月</el-button>
        </el-form-item>
      </el-form>
    </div>

    <table class="calendar-table" :style="{width}">
      <thead>
        <tr>
          <th v-for="(item, i) in weeks" :key="i">{{ item }}</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(dates, i) in res" :key="i" :style="{height: tbodyHeight}">
          <td
            v-for="(item, index) in dates"
            :key="index"
            :class="{notCurMonth: !item.isCurMonth, currentDay: item.date === curDate, selectDay: item.isSelected, rangeSelectd: item.isRangeSelected, weekend: item.isWeekend}"
            @click="handleItemClick(item, i, index)"
            @mouseover="handleItemMove(item, i, index)"
          >
            <!-- <span>{{ item.date.split('-').slice(1).join('-') }}</span> -->
            <span>{{ item.date }}</span>
            <slot :data="item" />
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
import { getDaysInMonth, handleCrateDate, handleCreateDatePicker, parseTime } from '../utils/index'
export default {
  components: {

  },
  props: {
    'selectMode': {
      type: String,
      default: 'click'
    },
    'startOfWeek': {
      type: Number,
      default: 1
    },
    canSelect: {
      type: Boolean,
      default: false
    },
    width: {
      type: String,
      default: '70%'
    },
    tbodyHeight: {
      type: String,
      default: '60px'
    }
  },
  data() {
    return {
      monthOptions: [],
      yearOptions: [],
      weeks: ['一', '二', '三', '四', '五', '六', '日'],
      curYear: new Date().getFullYear(), // 当前年
      curMonth: new Date().getMonth(), // 当前月
      days: 0, // 当前月总共天数
      curDate: parseTime(new Date().getTime()), // 当前日期 yyyy-MM-dd 格式,用来匹配是否是当前日期
      prevDays: [], // 非当前月的上一月展示的日期
      rearDays: [], // 非当前月的下一月展示的日期
      curDays: [], // 当前月的日期
      showDays: [], // 总共展示的42个日期
      res: [], // 二维数组
      selectedDates: [], // 选中的日期
      selectedMode: false, // true表示点击, false表示滑动
      moveIndex: [], // 两个,第一个是起始,第二个是结束
      canMove: false // 当moveIndex数组有一个值时,可以触发滑动
    }
  },
  computed: {

  },
  watch: {
    curMonth: {
      handler(val) {
        this.handleGetDays(this.curYear, val, this.startOfWeek)
      }
    },
    curYear: {
      handler(val) {
        this.handleGetDays(val, this.curMonth, this.startOfWeek)
      }
    }
  },
  created() {
    this.weeks.unshift(...this.weeks.splice(this.startOfWeek - 1))
    this.handleGetDays(this.curYear, this.curMonth, this.startOfWeek)
    this.selectedMode = this.selectMode === 'click'
  },
  mounted() {
    this.monthOptions = handleCreateDatePicker().months
    this.yearOptions = handleCreateDatePicker().years
    if (localStorage.selectedDates) this.selectedDates = JSON.parse(localStorage.selectedDates)
  },
  methods: {
    handleGetDays(year, month, startOfWeek) {
      this.showDays = []
      this.days = getDaysInMonth(year, month)
      let firstDayOfWeek = new Date(`${year}-${month + 1}-01`).getDay()
        
      // 处理周起始日
      const obj = {
        1: '一',
        2: '二',
        3: '三',
        4: '四',
        5: '五',
        6: '六',
        0: '日'
      }
      const firstDayInCN = obj[firstDayOfWeek]
      const index = this.weeks.indexOf(firstDayInCN)
      console.log(firstDayOfWeek, index)

      if (firstDayOfWeek === 0) { // 星期天为0 星期一为1 ,以此类推
        firstDayOfWeek = 7
      }

      this.prevDays = handleCrateDate(year, month, 1, index + 1, 'prev')
      this.rearDays = handleCrateDate(year, month, 1, 42 - this.days - (index), 'rear')

      this.curDays = handleCrateDate(year, month, 1, this.days)
      this.showDays.unshift(...this.prevDays)
      this.showDays.push(...this.curDays)
      this.showDays.push(...this.rearDays)
      this.res = this.handleFormatDates(this.showDays)
    },
    handleFormatDates(arr, size = 7) { // 传入长度42的原数组,最终转换成二维数组
      const arr2 = []
      for (let i = 0; i < size; i++) {
        const temp = arr.slice(i * size, i * size + size)
        arr2.push(temp)
      }
      // console.log(arr2)
      return arr2
    },
    handleTableHead(start) {
      const sliceDates = this.weeks.splice(start - 1)
      this.weeks.unshift(...sliceDates)
    },
    handleItemClick(item, i, j) {
      if (!this.canSelect) return
      if (this.selectedMode) {
        this.$nextTick(() => {
        // this.$set(this.res[i][j], 'isSelected', )
          this.res[i][j].isSelected = !this.res[i][j].isSelected
          if (this.res[i][j].isSelected) {
            this.selectedDates.push(this.res[i][j].date)
            this.selectedDates = Array.from(new Set(this.selectedDates))
          } else {
            this.selectedDates.splice(this.selectedDates.indexOf(item.date), 1)
          }
          this.$emit('dateSelected', this.selectedDates)
        })
      } else {
        // 滑动模式下,第一次点击是起始,第二次点击是结束
        const index = i * 7 + j
        this.canMove = true
        if (this.moveIndex.length === 1) {
          this.canMove = false
        }
        if (this.moveIndex.length === 2) {
          this.showDays.forEach(item => {
            item.isSelected = false
            item.isRangeSelected = false
          })
          this.canMove = true
          this.moveIndex.length = 0
        }
        this.moveIndex.push(index)
        this.moveIndex.sort((a, b) => a - b)
        this.selectedDates = this.showDays.slice(this.moveIndex[0], this.moveIndex[1] + 1)
        this.selectedDates.length !== 0 && this.$emit('dateSelected', this.selectedDates)
      }
    },
    handleItemMove(data, i, j) {
      if (this.canMove && !this.selectedMode) {
        const index = i * 7 + j
        this.showDays.forEach(item => {
          item.isSelected = false
          item.isRangeSelected = false
        })
        // 让第一个日期和最后一个日期显示蓝色高亮
        this.showDays[index].isSelected = true
        this.showDays[this.moveIndex[0]].isSelected = true

        // 不同情况的判断,当用户的鼠标滑动进日期的索引小于起始日期的索引,要做if else处理
        if (this.moveIndex[0] < index) {
          for (let i = this.moveIndex[0] + 1; i < index; i++) {
            this.showDays[i].isRangeSelected = true
          }
        } else {
          for (let i = index + 1; i < this.moveIndex[0]; i++) {
            this.showDays[i].isRangeSelected = true
          }
        }
      }
    },
    handleQuickChange(type) {
      if (type === 'prev') {
        this.curMonth--
        console.log(this.curMonth)
        if (this.curMonth === -1) {
          this.curMonth = 11
          this.curYear -= 1
        }
      } else if (type === 'next') {
        this.curMonth++
        if (this.curMonth === 12) {
          this.curMonth = 0
          this.curYear += 1
        }
      }
    }
  }
}
</script>

<style scoped lang="scss">
.calendar{
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}
.calendar-table{
  table-layout: fixed;
  border-collapse: collapse;
  transition: .3s;
  thead tr{
    height: 50px;
  }
  tbody tr {
    &:first-child td{
      border-top: 1px solid #08a8a0;
    }
    td{
      cursor: pointer;
      border-right: 1px solid #08a8a0;
      border-bottom: 1px solid #08a8a0;
      &:first-child{
        border-left: 1px solid #08a8a0;
      }
    }
  }
}

.notCurMonth{
  color: #C0C4CC;
}
.currentDay{
  color: #fff;
  background-color: #08a8a0;
}
.selectDay{
  color: #fff;
  background-color: #409EFF;
}
.rangeSelectd{
  color: #606266;
  background-color: #dee2e9;
}
.weekend{
  color: #F73131;
}
</style>

7.2 utils/index.js

/* eslint-disable camelcase */
/* eslint-disable no-unused-vars */

// 获取该月的天数
export const getDaysInMonth = (year, month) => {
  const day = new Date(year, month + 1, 0)
  return day.getDate()
}

// 创建日期 yyyy-MM-dd 格式, 用于创建非当前月的日期
export const handleCrateDate = (year, month, start, end, type) => {
  const arr = []
  if (type === 'prev') { // 上一月
    if (start === end) return []
    const daysInLastMonth = getDaysInMonth(year, month - 1) // 获取上一个月有多少天
    console.log(`当前月是${month + 1}月, 上一月${month}月的天数是${daysInLastMonth}天`)
    for (let i = daysInLastMonth - end + 2; i <= daysInLastMonth; i++) {
      arr.push({
        // date: `${month === 0 ? year - 1 : year}-${(month + 1) < 10 ? month === 0 ? 12 : `0${month}` : month}-${i < 10 ? `0${i}` : i}`,
        date: parseTime(new Date(year, month - 1, i)),
        isCurMonth: false,
        isSelected: false,
        isRangeSelected: false
      })
    }
  } else if (type === 'rear') { // 下一月
    for (let i = start; i <= end; i++) {
      arr.push({
        // date: `${month === 11 ? year + 1 : year}-${(month + 1) < 9 ? `0${month + 2}` : month + 2 <= 12 ? month + 2 : (month + 2) % 12 < 10 ? `0${(month + 2) % 12}` : (month + 2) % 12}-${i < 10 ? `0${i}` : i}`,
        date: parseTime(new Date(year, month + 1, i)),
        isCurMonth: false,
        isSelected: false,
        isRangeSelected: false
      })
    }
  } else { // 本月
    for (let i = start; i <= end; i++) {
      arr.push({
        // date: `${year}-${(month + 1) < 10 ? `0${month + 1}` : month + 1}-${i < 10 ? `0${i}` : i}`,
        date: parseTime(new Date(year, month, i)),
        isCurMonth: true,
        isSelected: false,
        isRangeSelected: false
      })
    }
  }
  // console.log(arr)
  return arr
}

export const handleCreateDatePicker = () => {
  const years = []
  const months = []
  for (let i = 1970; i <= 2099; i++) {
    years.push({
      label: `${i}年`,
      value: i
    })
  }
  for (let i = 0; i <= 11; i++) {
    months.push({
      label: `${i + 1}月`,
      value: i
    })
  }
  return {
    years,
    months
  }
}

/**
 * Parse the time to string
 * @param {(Object|string|number)} time
 * @param {string} cFormat
 * @returns {string | null}
 */
export function parseTime(time, cFormat) {
  if (arguments.length === 0 || !time) {
    return null
  }
  const format = cFormat || '{y}-{m}-{d}'
  let date
  if (typeof time === 'object') {
    date = time
  } else {
    if ((typeof time === 'string')) {
      if ((/^[0-9]+$/.test(time))) {
        // support "1548221490638"
        time = parseInt(time)
      } else {
        // support safari
        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari
        time = time.replace(new RegExp(/-/gm), '/')
      }
    }

    if ((typeof time === 'number') && (time.toString().length === 10)) {
      time = time * 1000
    }
    date = new Date(time)
  }
  const formatObj = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay()
  }
  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
    const value = formatObj[key]
    // Note: getDay() returns 0 on Sunday
    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
    return value.toString().padStart(2, '0')
  })
  return time_str
}

你可能感兴趣的:(vue实现日历,js实现日历组件,vue,前端)