需求简述:公司项目需要有一个日历,用来统计作者每个月写了哪些天发布了文章,然后显示对应的字数,今天之前显示一个颜色,今天以后显示一个颜色,有发布文章的一个颜色,大概查阅了一下主要的几个ui库的日历功能,发现使用功能不是很满足,所以自己写了一个 先看下最终的效果如下
核心代码
- 面板区域数据
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就可以获得一个完整的月份面板数据如下:
有了这份数据,其他的就简单了,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>
这个插槽主要是暴露给外面,使用作用域插槽实现每天的更新字数
功能目前比较粗糙,主要是项目使用,如果有其他需求,可以完全自主的定制化