以下是使用 Vue3 开发手机端日历组件的详细指南,包含常见场景、问题及解决方案:
一、基础日历组件实现
1. 组件结构
<template>
<div class="calendar-container">
<!-- 头部控制栏 -->
<div class="header">
<button @click="prevMonth">←</button>
<h2>{{ currentMonth }}</h2>
<button @click="nextMonth">→</button>
</div>
<!-- 星期显示 -->
<div class="weekdays">
<div v-for="day in weekdayNames" :key="day">{{ day }}</div>
</div>
<!-- 日期网格 -->
<div class="days-grid">
<div
v-for="(day, index) in visibleDays"
:key="index"
:class="[
'day-cell',
{
'current-month': day.isCurrentMonth,
'selected': isSelected(day.date),
'today': isToday(day.date)
}
]"
@click="selectDate(day)"
>
{{ day.date.getDate() }}
<div v-if="hasEvent(day.date)" class="event-dot"></div>
</div>
</div>
</div>
</template>
2. 核心逻辑
import { ref, computed } from 'vue';
export default {
props: {
// 事件日期数组
events: {
type: Array,
default: () => []
},
// 周起始日(0-周日,1-周一)
weekStartsOn: {
type: Number,
default: 0
}
},
setup(props) {
const currentDate = ref(new Date());
const selectedDate = ref(null);
// 生成可见日期数组
const visibleDays = computed(() => {
const year = currentDate.value.getFullYear();
const month = currentDate.value.getMonth();
const days = [];
// 获取本月第一天
const firstDay = new Date(year, month, 1);
// 获取上月最后一天
const lastDayOfPrevMonth = new Date(year, month, 0);
// 填充上月日期
const prevMonthDays = firstDay.getDay() - props.weekStartsOn;
for (let i = prevMonthDays < 0 ? 6 : prevMonthDays; i > 0; i--) {
days.push(createDayObj(lastDayOfPrevMonth.getDate() - i + 1, false));
}
// 填充本月日期
const daysInMonth = new Date(year, month + 1, 0).getDate();
for (let i = 1; i <= daysInMonth; i++) {
days.push(createDayObj(i, true));
}
// 填充下月日期
const nextMonthDays = 42 - days.length; // 保持6行
for (let i = 1; i <= nextMonthDays; i++) {
days.push(createDayObj(i, false));
}
return days;
});
// 辅助方法:创建日期对象
const createDayObj = (dayNumber, isCurrentMonth) => {
const date = new Date(currentDate.value);
date.setMonth(date.getMonth() + (isCurrentMonth ? 0 : dayNumber < 15 ? 1 : -1));
date.setDate(dayNumber);
return { date, isCurrentMonth };
};
// 日期选择
const selectDate = (day) => {
if (!day.isCurrentMonth) return;
selectedDate.value = day.date;
emit('select', day.date);
};
return { visibleDays, selectDate, /* 其他需要返回的属性和方法 */ };
}
};
二、关键功能实现
1. 手势滑动支持
// 在setup()中添加
const touchStartX = ref(0);
const handleTouchStart = (e) => {
touchStartX.value = e.touches[0].clientX;
};
const handleTouchEnd = (e) => {
const deltaX = e.changedTouches[0].clientX - touchStartX.value;
if (Math.abs(deltaX) > 50) {
deltaX > 0 ? prevMonth() : nextMonth();
}
};
// 在模板中添加事件
<div
class="days-grid"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
>
2. 日期范围限制
// props 添加
disablePast: {
type: Boolean,
default: false
},
// 修改日期点击处理
const selectDate = (day) => {
if (!day.isCurrentMonth) return;
if (props.disablePast && day.date < new Date().setHours(0,0,0,0)) return;
// ...
};
三、最佳实践与优化
1. 性能优化
- 虚拟滚动:当需要显示多个月份时,使用虚拟滚动技术
- 缓存计算:对复杂计算使用
computed
缓存 - 防抖处理:对手势操作添加防抖逻辑
2. 国际化处理
// 使用Intl API处理周显示
const weekdayNames = computed(() => {
const formatter = new Intl.DateTimeFormat([], { weekday: 'short' });
return Array.from({ length: 7 }, (_, i) => {
const date = new Date(2023, 0, 2 + i); // 任意包含完整周的日期
return formatter.format(date);
});
});
3. 样式优化方案
/* 移动端适配 */
.calendar-container {
width: 100%;
font-size: 14px;
}
.days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 2px;
}
.day-cell {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
touch-action: manipulation; /* 禁用双击缩放 */
}
/* 当前月样式 */
.current-month {
background: #fff;
}
/* 非当前月样式 */
.day-cell:not(.current-month) {
color: #999;
}
/* 事件标记 */
.event-dot {
position: absolute;
bottom: 2px;
width: 4px;
height: 4px;
border-radius: 50%;
background: #f00;
}
四、常见问题与解决方案
1. 时区问题
- 问题:服务器时间与本地时间不一致
- 方案:统一使用UTC时间处理
const utcDate = new Date(Date.UTC(year, month, day));
2. 性能卡顿
- 问题:快速滑动时出现卡顿
- 方案:
- 使用CSS
will-change: transform;
- 对日期生成使用Web Worker
- 添加加载状态过渡动画
- 使用CSS
3. 跨月日期选择
- 问题:需要支持跨月份选择日期范围
- 方案:
const selectedRange = ref([]);
const handleRangeSelect = (day) => {
if (selectedRange.value.length === 2) selectedRange.value = [];
selectedRange.value.push(day.date);
selectedRange.value.sort((a, b) => a - b);
};
4. 日期格式化一致性
- 问题:不同浏览器日期格式差异
- 方案:统一使用 date-fns 库处理
npm install date-fns
import { format, addMonths, isSameMonth } from 'date-fns';
const currentMonth = computed(() =>
format(currentDate.value, 'yyyy年MM月')
);
五、典型使用场景
1. 酒店预订(日期范围选择)
<template>
<Calendar
@select-range="handleRangeSelect"
:allowed-range="[new Date(), addMonths(new Date(), 3)]"
/>
</template>
2. 会议预约系统
<Calendar
:events="meetingDates"
:hour-markers="true"
@select="showTimePicker"
/>
3. 生日选择器
<Calendar
:max-date="new Date()"
:year-range="[1950, new Date().getFullYear()]"
@select="updateBirthday"
/>
通过以上实现方案,可以构建一个高性能、可定制化的移动端日历组件。建议根据具体业务需求进行功能扩展,同时注意移动端特有的用户体验优化。