1. 项目概述
这个基于Vue2的日历组件实现了一个完整的日期选择器,核心功能包括:
- 显示当前月份的日历网格
- 支持年份和月份的前后切换
- 高亮标记当天日期
- 允许用户点击选择特定日期
- 通过事件机制通知父组件日期变更
组件采用dayjs进行日期处理,相比原生Date API,dayjs提供了更简洁的链式调用和格式化能力。整个组件约200行代码,但实现了完整的日历交互逻辑。
提示:dayjs是一个轻量级的日期处理库(仅2KB),API设计参考了Moment.js但性能更优,非常适合前端日期操作场景。
2. 核心设计解析
2.1 数据结构设计
组件内部维护两个核心状态:
currentDate- 当前显示的月份(用于控制日历视图)selectedDate- 用户选中的日期(用于标记选中状态)
javascript复制data() {
return {
currentDate: dayjs(), // 默认当前月份
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
selectedDate: dayjs() // 默认选中当天
}
}
2.2 日历数据生成逻辑
通过计算属性calendarDays动态生成42天的日历数据(6周×7天),包含:
- 上个月的部分日期(用于填充第一周空白)
- 当前月的所有日期
- 下个月的部分日期(用于填充最后一周空白)
javascript复制computed: {
calendarDays() {
const year = this.currentDate.year()
const month = this.currentDate.month()
const today = dayjs()
// 关键算法:找到日历起始日(当月1号所在周的周日)
const firstDay = dayjs(new Date(year, month, 1))
const startDate = firstDay.subtract(firstDay.day(), 'day')
// 生成42天的日历数据
const days = []
for (let i = 0; i < 42; i++) {
const date = startDate.add(i, 'day')
days.push({
date: date.date(), // 日期数字(1-31)
fullDate: date.format('YYYY-MM-DD'), // 完整日期字符串
otherMonth: date.month() !== month, // 是否非当月
today: date.format('YYYY-MM-DD') === today.format('YYYY-MM-DD'),
selected: date.format('YYYY-MM-DD') === this.selectedDate.format('YYYY-MM-DD')
})
}
return days
}
}
2.3 交互逻辑实现
2.3.1 年月切换
通过简单的加减运算实现年月切换:
javascript复制methods: {
changeYear(step) {
this.currentDate = this.currentDate.add(step, 'year')
},
changeMonth(step) {
this.currentDate = this.currentDate.add(step, 'month')
}
}
2.3.2 日期选择
点击日期时更新选中状态并触发事件:
javascript复制selectDate(day) {
this.selectedDate = dayjs(day.fullDate)
this.currentDate = dayjs(day.fullDate) // 同步更新当前视图月份
this.$emit('input', day.fullDate) // 支持v-model
this.$emit('select', day.fullDate) // 自定义事件
}
3. 样式设计与优化
3.1 基础布局结构
采用Flex布局实现响应式日历网格:
scss复制.calendar {
width: 100%;
max-width: 350px;
border-radius: 4px;
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.calendar-weekdays {
display: flex;
}
.calendar-days {
display: flex;
flex-wrap: wrap;
}
.calendar-day {
width: calc(100% / 7); // 7天一周
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
}
3.2 状态样式设计
通过动态class实现不同状态的视觉反馈:
scss复制.calendar-day {
// 基础样式...
&.other-month {
color: #c0c4cc; // 非当月日期灰色显示
}
&.today {
background-color: #409eff; // 当天蓝色背景
color: #fff;
}
&.selected {
background-color: #67c23a; // 选中绿色背景
color: #fff;
}
&:hover {
background-color: #ecf5ff; // 悬停浅蓝色
}
}
4. 功能扩展与实践建议
4.1 禁用日期功能
实际项目中常需要禁用特定日期(如已约满日期):
javascript复制// 在calendarDays计算属性中添加
disabled: this.disabledDates.includes(date.format('YYYY-MM-DD'))
// 使用示例
<calendar :disabled-dates="['2023-12-25', '2024-01-01']" />
4.2 国际化支持
如需支持多语言,可以动态设置weekdays:
javascript复制data() {
return {
weekdays: this.$t('calendar.weekdays')
// 根据i18n返回['Sun', 'Mon',...]或['日', '一',...]
}
}
4.3 性能优化建议
- 避免重复计算:对于静态数据如weekdays,应在created钩子中初始化而非每次渲染计算
- 日期格式化缓存:频繁使用的日期格式可以预先计算存储
- 虚拟滚动:如需显示多年日历,考虑只渲染可视区域日期
5. 常见问题与解决方案
5.1 日期显示错位
现象:日历第一行显示不完整的上周日期
原因:时区问题导致firstDay.day()计算偏差
解决:确保dayjs使用统一时区配置
javascript复制import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
const firstDay = dayjs.utc(new Date(year, month, 1))
5.2 选中状态不更新
现象:点击日期后UI未更新
排查步骤:
- 检查dayjs对象是否被正确创建
- 确认selectedDate被响应式更新
- 检查template中class绑定是否正确
5.3 移动端适配问题
现象:触摸反馈不明显
优化方案:
scss复制.calendar-day {
// 增加触摸反馈
&:active {
transform: scale(0.95);
}
// 增大点击区域
padding: 10px 0;
}
6. 组件使用示例
6.1 基础用法
html复制<template>
<div>
<calendar v-model="selectedDate" @select="handleDateSelect" />
<p>当前选择:{{ selectedDate }}</p>
</div>
</template>
<script>
export default {
data() {
return {
selectedDate: ''
}
},
methods: {
handleDateSelect(date) {
console.log('选中日期:', date)
}
}
}
</script>
6.2 带禁用日期
html复制<calendar
:disabled-dates="disabledDates"
:value="bookingDate"
/>
6.3 自定义样式
通过穿透scoped样式实现深度定制:
scss复制::v-deep .calendar {
.calendar-day.selected {
background-color: #ff0000; // 红色选中状态
}
}
在实现过程中,我发现日历组件的核心难点在于日期数据的生成算法。特别是处理跨月的日期显示时,需要精确计算当前月第一天是星期几,然后向前补全上周的日期,向后补全下月的日期,确保日历表总是显示完整的6行42天。这个算法经过多次调试才达到稳定状态。
另一个值得注意的细节是dayjs的不可变性。所有日期操作都会返回新对象而非修改原对象,这与Vue的响应式系统配合得很好。例如this.currentDate.add(1, 'month')必须赋值给this.currentDate才能触发视图更新。