说明
本文语法使用的是组合式,需要vue3或者 vue2.7
组件样式使用的是 tailwindcss,这里只是简单介绍下我的日历组件是如何构成的。
本来打算开源的,后来想想也不好看,也只有一个组件,就算了。
日历数据来源用 dayjs 得到一个日历的数据
思路
日历头
html
<div class="calendar" :style="{ 'background-color': backgroundColor }">
<div v-if="showHeader" class="header flex justify-between items-center px-3 py-2 border-b">
<div class="title">
<span v-if="showHeaderYear">{{ selectDate.year() }} 年 </span>
{{ selectDate.month() + 1 }} 月
</div>
<div class="button-group flex items-center justify-end">
<button
class="button border rounded-l py-1 px-4 text-center text-xs lg:hover:text-sky-600 lg:hover:bg-sky-100 lg:hover:border-sky-200"
style="margin-right: -1px" @click="changeMonth(-1)">上个月</button>
<button
class="button border py-1 px-4 text-center text-xs lg:hover:text-sky-600 lg:hover:bg-sky-100 lg:hover:border-sky-200"
@click="today">今天</button>
<button
class="button border rounded-r py-1 px-4 text-center text-xs lg:hover:text-sky-600 lg:hover:bg-sky-100 lg:hover:border-sky-200"
style="margin-left: -1px" @click="changeMonth(1)">下个月</button>
</div>
</div>
日历头很简单,只有左边一个标题显示了当前的年和月,右边是三个控制跳转的按钮,分别是上个月 ,今天 ,下个月
其中有几个是传入的可选 props
go
// 日历背景颜色
backgroundColor: {
type: String,
default: '#fff',
},
// 是否显示日历头
showHeader: {
type: Boolean,
default: true
},
这里面的selectDate 是当前选择的日期经过 dayjs 格式化后的对象
js
// 传入的日期,使用时通常传入今天
date: {
type: String,
default: dayjs().format('YYYY-MM-DD')
},
// 传入日期的 dayjs 格式化对象
const selectDate = computed(() => dayjs(props.date))
此处的 changeMonth和 today 方法稍后再说
日历 body 部分
背景显示月份
其实这个有没有都行,也很少有需求会点名要这个功能
html
<div class="body relative p-2">
<div v-if="showMonthOnBackground"
:class="['month-shadow', 'absolute', 'text-9xl', 'top-1/2', 'left-1/2', 'text-gray-100/80', '-translate-x-2/4', '-translate-y-2/4', 'pointer-events-none']">
{{ month.split('-')[1] }}
</div>
它的作用就是在日历的背景中显示一个月份的虚影
js
// 是否背景显示月份虚影
showMonthOnBackground: {
type: Boolean,
default: false
},
// 当前选中日期的年-月
const month = computed(() => {
return `${selectDate.value.year()}-${selectDate.value.month() + 1}`
})
日历周标题
考虑到可能会有定制周标题的需求,于是开放成了 props
js
// 星期的显示文字
weekDays: {
type: Array,
default: () => ['一', '二', '三', '四', '五', '六', '日']
},
// 一周是否开始于周一
weekStartsOnMonday: {
type: Boolean,
default: true
},
// 星期的样式
weekDayStyle: {
type: Object,
default: () => ({})
},
const _weekDays = computed(() => {
let weekDays = [...props.weekDays];
if (!props.weekStartsOnMonday) {
const lastDay = weekDays.pop()
weekDays.unshift(lastDay)
}
return weekDays;
})
配置 weekDays 时不用管一周开始于周几,按周一至周日排,如果将 weekStartsOnMonday 配置成 false 时会自动将周日放到第一位的
html
<div class="weekdays grid grid-cols-7">
<div v-for="weekday in _weekDays" :key="weekday" class="weekday text-center text-base py-3"
:style="weekDayStyle">{{ weekday }}
</div>
</div>
日历日期部分
html
<div class="week relative grid grid-cols-7" v-for="(week, weekIndex) in calendar">
<div v-for="(day, dayIndex) in week" :key="day.fullDate" :class="[
'day',
'text-center',
'text-base',
'cursor-pointer',
'lg:hover:bg-blue-200',
'transition-colors',
{
'border-r': bordered,
'border-b': bordered,
'border-t': !weekIndex && bordered,
'border-l': !dayIndex && bordered,
'pointer-events-none': isCellDisabled(day),
'opacity-50': isCellDisabled(day),
'invisible': isHiddenCell(day),
}]" @click="onSelect(day)" :style="dateCellStyle">
<div
:class="['day-inner', 'py-2', { 'text-gray-400': !day.isCurrentMonth, 'text-sky-500': day.isToday || selectDate.isSame(day.fullDate, 'day'), 'bg-blue-200': selectDate.isSame(day.fullDate, 'day') }]">
<slot :day="day" name="date-cell"><span>{{ day.date }}</span></slot>
</div>
</div>
</div>
这里要说明的比较多,首先日历数据 calendar
js
const calendar = ref([])
// 每当选中日期的月份变化了之后,就重新计算日历数据
watch(month, () => {
calendar.value = generateCalendar(selectDate.value, {
weekStartsOnMonday: props.weekStartsOnMonday
})
}, { immediate: true })
得到的是一个二维数组,所以在画UI 时要循环两次,一次是周,一次是日期。
样式部分没什么好说的,只有一点要解释一下,为什么在加 hover 时要加上个前缀 lg,是因为这个组件我是移动端和桌面端同时使用的,在移动端时会有 hover 残留的现象,所以为了解决就加上了lg。
isCellDisabled 是一个判断该日期是否禁止的方法
js
// 是否禁止选择非当前月份的日期
disableNonCurrentMonth: {
type: Boolean,
default: false
},
// 禁止选中的日期
disabledDate: {
type: Function,
default: () => false
},
function isCellDisabled(day) {
if (props.disableNonCurrentMonth && !day.isCurrentMonth) {
return true
}
return props.disabledDate(day)
}
除了可以配置禁止选择当前月份的日期外,还可以自行配置禁止的日期,例如:
js
<Calendar :disabledDate="handleDisabledDate" />
function handleDisabledDate (day) {
if ([2, 3, 4, 5, 6].includes(day.date)){
return true
}
return false
}
isHiddenCell 是一个判断日期是否隐藏的方法,用于判断这一天是否需要隐藏不显示
js
// 是否显示非本月的日期
showOtherMonth: {
type: Boolean,
default: true
},
// 是否显示上个月的日期
showPrevMonth: {
type: Boolean,
default: true
},
// 是否显示下个月的日期
showNextMonth: {
type: Boolean,
default: true
},
function isHiddenCell(day) {
if (day.isCurrentMonth) return false;
if (!props.showOtherMonth) return true;
if (!props.showPrevMonth && day.isPrevMonth) return true;
if (!props.showNextMonth && day.isNextMonth) return true;
}
显示日期
因为有自定义日期内容的需求,所以日期部分写成了个插槽,将日期的数据传递出去,如何显示自己决定,默认只是一个日期
html
<Calendar>
<template #date-cell="{ day }">
<span>自定义内容{{ day.fullDate }}</span>
</template>
</Calendar>
切换日期
下面说下在点击切换日期时 还有之前提到的上个月 今天 下个月三个按钮都做了什么
其实他们做的都是同一个事,就是触发一个 onSelect 的 emit,由父组件来改变当前选中的日期
js
// 点击上/下个月选择的日期,first: 上/下个月第一天,last: 上/下个月最后一天,none: 当前选择日期的上/下个月
monthSelectType: {
type: String,
default: 'none',
},
const emit = defineEmits(['onSelect'])
const FORMAT = 'YYYY-MM-DD';
function today() {
const today = dayjs();
emit('onSelect', today.format(FORMAT), {
from: 'today',
isOverMonth: !selectDate.value.isSame(today, 'month')
})
}
function onSelect(newDate) {
emit('onSelect', newDate.fullDate, {
from: 'select',
isOverMonth: !selectDate.value.isSame(newDate.fullDate, 'month')
})
}
function changeMonth(delta) {
const newMonthDay = selectDate.value.add(delta, 'month');
let targetDate = null;
if (props.monthSelectType === 'first') {
targetDate = newMonthDay.startOf('month');
} else if (props.monthSelectType === 'last') {
targetDate = newMonthDay.endOf('month');
} else {
targetDate = newMonthDay;
}
emit('onSelect', targetDate.format(FORMAT), {
from: delta > 0 ? 'next' : 'prev',
isOverMonth: true,
})
}
除了向父组件传递了新的日期之外,还传了一个数据,实际上目前的使用过程中只有 isOverMonth 这个参数用到了
向外暴露选择上下月份的方法
有时候并不想使用自带的 header,而是在页面上其他位置自定义了上下月份的按钮, 所以组件要将这两个方法暴露出去
js
function prevMonth() {
changeMonth(-1)
}
function nextMonth() {
changeMonth(1)
}
defineExpose({
prevMonth,
nextMonth,
})
使用时:
js
<Calendar ref="calendarRef" />
const calendarRef = ref(null)
function handleNextMonth() {
calendarRef.value.nextMonth()
}
使用
使用这个日历组件时要传入选择的日期和改变日期的方法
js
<Calendar :date="date" @onSelect="handleSelectDate" />
const date = ref(dayjs().format('YYYY-MM-DD'))
function handleSelectDate(newDate, { isOverMonth }) {
date.value = newDate;
// 可以在这获取这日的数据
if (isOverMonth) {
// 可以在这当跨月时获取月份所需的数据
}
}