Vue3 实现一个简单的日历组件

2,828 阅读2分钟

需求简述:公司项目需要有一个日历,用来统计作者每个月写了哪些天发布了文章,然后显示对应的字数,今天之前显示一个颜色,今天以后显示一个颜色,有发布文章的一个颜色,大概查阅了一下主要的几个ui库的日历功能,发现使用功能不是很满足,所以自己写了一个 先看下最终的效果如下

1630994994161.jpg

核心代码

  • 面板区域数据
import { format, addMonths, startOfMonth, endOfMonth, addDays } from 'date-fns'
const getCurrentDays = (now, formatType) => {
  const monthStart = startOfMonth(now)
  const monthEnd = endOfMonth(now)
  const monthStartDay = new Date(monthStart).getDay()  // 月份开始第一天是周几
  const monthDays = new Date(monthEnd).getDate() // 这个月有都少天
  const itemDays = []
  for (let i = 0; i < monthDays; i++) {
    // days 0 - 6 周日 - 周六
    const _format = (type) => {
      return format(addDays(monthStart, i), type)
    }
    itemDays.push({
      currentMonth: 0, // 是否当前月 0 -1 1
      day: (monthStartDay + i) % 7, // 周几
      date: _format(formatType), // 日期
      month: _format('M'), // 月
      days: _format('d'), // 天
      valueOf: addDays(monthStart, i).valueOf() // 时间戳
    })
  }
  return itemDays
}
// 月份面板补充前面的
const getPrefixDays = (now, formatType, day) => {
  if (day === 0) {
    return []
  }
  const prevDays = getCurrentDays(addMonths(now, -1), formatType)
  const ret = [...prevDays].map(item => {
    return { ...item, currentMonth: -1 }
  })
  return ret.slice(-day)
}

// 月份面板补充后面的
const getSubfixDays = (now, formatType, day) => {
  if (day === 0) {
    return []
  }
  const nextDays = getCurrentDays(addMonths(now, 1), formatType)
  let ret = [...nextDays].map(item => {
    return { ...item, currentMonth: 1 }
  })
  ret.splice(day)
  return ret
}
// 根据月初和月末 计算 前后需要补几天
const supNumber = (day, type) => {
  // type 月前 -1 还是 月后 1
  if (type === -1) {
    return day === 0 ? 6 : day - 1
  }
  return day === 0 ? day : 7 - day
}
const getItemDays = (now = new Date(), formatType) => {
  const _getCurrentDays = getCurrentDays(now, formatType)
  const firstDay = _getCurrentDays[0].day
  const endDay = _getCurrentDays[_getCurrentDays.length - 1].day
  return [
    ...getPrefixDays(now, formatType, supNumber(firstDay, -1)),
    ..._getCurrentDays,
    ...getSubfixDays(now, formatType, supNumber(endDay, 1))]
}

export {
  getItemDays
}

主要思想就是通过当前月第一天、最后一天去获取整个月的数据,在根据当前月第一天、最后一天是周几、去补齐前面和后面的数据,这样我们通过getItemDays就可以获得一个完整的月份面板数据如下:

1630996575799.jpg 有了这份数据,其他的就简单了,currentMonth 主要表示是否是当前显示的月份, 0 为当前显示的月份,-1表示显示的是上个月补齐的天,1表示是下个月补齐的天,day 是周几,days 是多少号。

剩余的主要就是 渲染和事件

<template>
  <div :class="`${cp}-box`">
    <div :class="`${cp}-header`">
      <div :class="`${cp}-title`">{{title}}</div>
      <div :class="`${cp}-editor`">
        <div>
          <i class="el-icon-arrow-left" :class="{disabled: isFirstMonth}" @click="changeMonth(-1)"></i>
          <span>{{ currentYearAndMonth }}</span>
          <i class="el-icon-arrow-right" @click="changeMonth(1)"></i>
        </div>
      </div>
    </div>
    <div :class="`${cp}-th`">
      <div v-for="item in dayTexts" :key="item">{{item}}</div>
    </div>
    <div :class="`${cp}-body`">
      <div :class="`${cp}-row`" v-for="i in itemDays.length / 7" :key="i">
        <div :class="`${cp}-td ${getClass(getDayItem(i, j))}`" v-for="j in 7" :key="`${i}-${j}`">
          <span :class="`${cp}-day`">{{(getDayItem(i, j).currentMonth === 0) ? getDayItem(i, j).days.toString().padStart(2, 0) : ''}}</span>
          <slot :item="getDayItem(i, j)"></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, ref, getCurrentInstance } from 'vue'
import { getItemDays } from './utils'
import { format as _format } from 'date-fns'
const instance = getCurrentInstance()
const now = new Date()
const month = ref(+now.getMonth())
const year = ref(+(now.getFullYear()))
const cp = 'tosn-calendar'
defineEmits(['changeMonth'])
const props = defineProps({
  title: {
    type: String,
    default: 'Calendar'
  },
  dayTexts: {
    type: Array,
    default: () => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
  },
  monthTexts: {
    type: Array,
    default: () => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
  },
  dataList: {
    type: Array,
    default: () => []
  },
  format: {
    type: String,
    default: 'yyyy-MM-dd'
  },
  start: { // 格式需要和format保持一致
    type: String,
    default: ''
  }
})
// 当前显示的年月
const currentYearAndMonth = computed(() => {
  return `${props.monthTexts[month.value]} ${year.value}`
})
// 当前年月字符串 2020-05
const formatYearAndMonth = computed(() => {
  return `${year.value}-${(month.value + 1).toString().padStart(2, 0)}`
})
// 是否开始的时间
const isFirstMonth = computed(() => {
  // 这个地方需要注意 和 format的根式保持一致
  return formatYearAndMonth.value === props.start
})
const itemDays = computed(() => {
  // 添加/1代表日,否则手机ios识别不出来
  return getItemDays(new Date(`${year.value}/${month.value + 1}/1`), props.format)
})
// 更改月份
const changeMonth = (mon) => {
  // 月份从0开始 到 11结束
  if (mon === 1) {
    if (month.value < 11) {
      month.value += 1
    } else {
      month.value = 1
      year.value += 1
    }
  } else {
    if (isFirstMonth.value) {  // 如果已经是初始月份,点击无效
      return false
    }
    if (month.value === 0) {
      month.value = 11
      year.value -= 1
    } else {
      month.value -= 1
    }
  }
  instance.emit('changeMonth', formatYearAndMonth.value)
}
// 获取每一天的信息
const getDayItem = (i, j) => {
  return itemDays.value[(i - 1) * 7 + (j - 1)]
} 
// 获取当前class的值
const getClass = (db) => {
  if (db.currentMonth !== 0) return '' // 不是当前月份 直接return
  const className = ['color-grey'] // 如果是当月 默认有灰色
  const _today = _format(new Date(), props.format)
  if (db.date <= _today) {
    className.push('color-purple') // 已经过去的
  }
  if (props.dataList.some(r => r.date === db.date)) {
    className.push('color-green') // 有数据的
  }
  if (db.date === _today) { // 今天
    className.push('color-today')
  }
  return className.join(' ')
}
// 重置
const resetDate = () => {
  month.value = +now.getMonth()
  year.value = +now.getFullYear()
}
// 对外暴露
defineExpose({
  resetDate
})
</script>
<span :class="`${cp}-day`">{{(getDayItem(i, j).currentMonth === 0) ? getDayItem(i, j).days.toString().padStart(2, 0) : ''}}</span>
这边因为ui设计的只显示当前月,所以简单做了个判断,然后几号做了个补0,这种修改尽量不要去修改数据模型,因为这边只是展示不一样,可能不同需求展示的不一定一样
<slot :item="getDayItem(i, j)"></slot>
这个插槽主要是暴露给外面,使用作用域插槽实现每天的更新字数

功能目前比较粗糙,主要是项目使用,如果有其他需求,可以完全自主的定制化

github地址

演示地址