简单的酒店日程日历展示
在本篇博客中,我将分享如何使用 Vue 3、 Element Plus、 date-fns 创建一个动态日历组件,该组件支持房间预订管理功能。这个组件不仅美观,而且用户友好,能够直观地展示每个房间在不同日期的可用性和预订情况。
项目简览
概述
我们构建的这个日历组件具有以下功能:
- 用户可以选择日期,并查看相应日期的房间可用性。
- 每个房间在特定日期可以被预订,用户可以输入客户姓名进行预订。
- 组件支持月份的前后切换,以便于用户快速浏览不同月份的房间状态。
- 在表格中,已预订的房间会显示客户的名字,而可用房间则提示"可用"。
- 在编辑状态下,用户可以输入客户信息并保存预订。
概览
初始化展示当天所在月的预定情况
可选定日期进行跳转 并高亮与居中选中日期
可以通过按钮跳转上个月与下个月 来查看预定信息
点击预定信息已预定的房间 可切换为可用 再次点击输入用户信息来进行预定
组件结构
模板部分
在组件的模板中,我们使用了 Element Plus 的表格组件 (el-table
) 来展示房间信息和预订状态。下面是组件的核心结构:
ini
<template>
<div class="calendar">
<div class="controls">
<el-button @click="previousMonth">上个月</el-button>
<el-date-picker
v-model="selectedDate"
type="date"
format="YYYY-MM-DD"
placeholder="选择日期"
@change="handleDateChange"
/>
<el-button @click="nextMonth">下个月</el-button>
</div>
<el-table
ref="tableRef"
:data="tableData"
stripe
border
style="width: 100%"
:cell-style="columnStyle"
:header-cell-style="columnStyle"
>
<el-table-column
fixed
label="房间类型"
prop="roomType"
width="150"
align="center"
></el-table-column>
<el-table-column
v-for="(day, index) in days"
:key="index"
align="center"
:label="day.label"
:prop="day.prop"
:width="150"
>
<template #default="scope">
<div v-if="isEditing(scope.row, day.prop)" class="editing-cell">
<el-input
v-model="scope.row[day.prop].customerName"
placeholder="输入用户名"
@blur="saveBooking(scope.row, day.prop)"
@keyup.enter="saveBooking(scope.row, day.prop)"
/>
</div>
<div
v-else
:class="['cell-content', { 'non-clickable': scope.row.roomType === '剩几间房' }]"
@click="handleCellClick(scope.row, day.prop)"
>
<span v-if="scope.row.roomType === '剩几间房'">
{{ scope.row[day.prop] }}
</span>
<span v-else-if="isBooked(scope.row, day.prop)">
已预订<br>{{ scope.row[day.prop].customerName }}
</span>
<span v-else>
可用
</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
状态管理
我们使用了 Vue 的响应式特性来管理组件状态,包括选定的日期、当前月份的开始日期、天数和表格数据。以下是部分状态定义代码:
ini
const selectedDate = ref(new Date());
const currentMonthStartDate = ref(startOfMonth(selectedDate.value));
const days = ref([]);
const tableData = ref([]);
生成月份天数
通过 generateMonthDays
函数,我们动态生成当前月份的天数,并为每一天设置标签和属性:
ini
const generateMonthDays = () => {
const daysArray = [];
const start = startOfMonth(currentMonthStartDate.value);
const daysInMonth = getDaysInMonth(start);
for (let i = 0; i < daysInMonth; i++) {
const date = addDays(start, i);
daysArray.push({
label: format(date, 'dd\nEEE', {locale: zhCN}),
prop: `day${i + 1}`
});
}
return daysArray;
};
更新表格数据
表格数据的更新通过 updateTableData
函数来实现,我们根据选定的月份和房间的预订状态来填充表格数据:
ini
const updateTableData = () => {
// ...更新逻辑
const roomData = Array.from({length: 5}, (_, rowIndex) => {
const row = {roomType: `房间${rowIndex + 1}`};
// 随机生成房间状态
return row;
});
// 添加剩余房间数的行
const availableRoomsRow = {roomType: '剩几间房'};
// ...计算剩余房间数
tableData.value = [availableRoomsRow, ...roomData];
};
处理用户交互
我们通过事件处理函数如handleCellClick和saveBooking来处理用户的点击和保存操作。这些函数确保用户能够轻松地输入预订信息,并实时更新房间的状态。
ini
// 处理单元格点击事件
const handleCellClick = (row, prop) => {
if (row.roomType === '剩几间房') return;
if (row[prop].status === '可用') {
editingCell.value = {rowProp: row.roomType, dayProp: prop};
row[prop].customerName = '';
} else {
row[prop] = {status: '可用', customerName: ''};
updateAvailableRooms(prop);
}
};
// 保存预订信息
const saveBooking = (row, prop) => {
const customerName = row[prop].customerName.trim();
if (customerName !== '') {
row[prop].status = '已预订';
} else {
row[prop].status = '可用';
}
editingCell.value = {rowProp: null, dayProp: null};
updateAvailableRooms(prop);
};
优化用户体验
通过scrollToSelectedDate和columnStyle来处理选定日期居中和高亮
ini
const scrollToSelectedDate = (date) => {
const selectedDay = format(date, 'dd');
const columnIndex = parseInt(selectedDay) - 1; // 获取列索引,假设日期为 01 - 31 对应索引 0 - 30
const columnWidth = 150; // 根据你的列宽设置
const tableWidth = tableRef.value.$el.offsetWidth;
// 计算居中位置
const scrollLeft = (columnIndex + 1) * columnWidth - (tableWidth / 2) + (columnWidth / 2);
// 调用 setScrollLeft 滚动到对应列
if (tableRef.value) {
tableRef.value.setScrollLeft(scrollLeft);
}
};
const columnStyle = ({row, column}) => {
const dayProp = column.property;
if (selectedDate.value && isCurrentDay(dayProp)) {
return {backgroundColor: '#1C86EE', color: '#fff'}; // 高亮的样式
}
return {};
};
样式
我们为组件添加了一些基本样式,使其更加美观和易于使用:
css
.calendar {
max-width: 100%;
margin: 0 auto;
}
.controls {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.cell-content {
cursor: pointer;
white-space: pre-line;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
完整的代码
ini
<template>
<div class="calendar">
<div class="controls">
<el-button @click="previousMonth">上个月</el-button>
<el-date-picker
v-model="selectedDate"
type="date"
format="YYYY-MM-DD"
placeholder="选择日期"
@change="handleDateChange"
/>
<el-button @click="nextMonth">下个月</el-button>
</div>
<el-table
ref="tableRef"
:data="tableData"
stripe
border
style="width: 100%"
:cell-style="columnStyle"
:header-cell-style="columnStyle"
>
<el-table-column
fixed
label=""
prop="roomType"
width="150"
align="center"
></el-table-column>
<el-table-column
v-for="(day, index) in days"
:key="index"
align="center"
:label="day.label"
:prop="day.prop"
:width="150"
v-memo="[day, tableData]"
>
<template #default="scope">
<div v-if="isEditing(scope.row, day.prop)" class="editing-cell">
<el-input
v-model="scope.row[day.prop].customerName"
placeholder="输入用户名"
@blur="saveBooking(scope.row, day.prop)"
@keyup.enter="saveBooking(scope.row, day.prop)"
/>
</div>
<div
v-else
:class="['cell-content', { 'non-clickable': scope.row.roomType === '剩几间房' }]"
@click="handleCellClick(scope.row, day.prop)"
>
<span v-if="scope.row.roomType === '剩几间房'">
{{ scope.row[day.prop] }}
</span>
<span v-else-if="isBooked(scope.row, day.prop)">
已预订<br>{{ scope.row[day.prop].customerName }}
</span>
<span v-else>
可用
</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import {ref, watch, onMounted} from 'vue';
import {format, startOfMonth, addDays, subMonths, addMonths, getDaysInMonth} from 'date-fns';
import {zhCN} from 'date-fns/locale';
// 状态定义
const selectedDate = ref(new Date());
const currentMonthStartDate = ref(startOfMonth(selectedDate.value));
const days = ref([]);
const tableData = ref([]);
const editingCell = ref({rowProp: null, dayProp: null});
const dataCache = ref({});
// 创建 el-table 的引用
const tableRef = ref(null);
// 生成当前月的天数
const generateMonthDays = () => {
const daysArray = [];
const start = startOfMonth(currentMonthStartDate.value);
const daysInMonth = getDaysInMonth(start);
for (let i = 0; i < daysInMonth; i++) {
const date = addDays(start, i);
daysArray.push({
label: format(date, 'dd\nEEE', {locale: zhCN}),
prop: `day${i + 1}`
});
}
return daysArray;
};
// 更新表格数据
const updateTableData = () => {
if (!selectedDate.value) return;
const monthKey = format(currentMonthStartDate.value, 'yyyy-MM');
if (dataCache.value[monthKey]) {
tableData.value = dataCache.value[monthKey];
days.value = generateMonthDays();
return;
}
days.value = generateMonthDays();
const roomData = Array.from({length: 5}, (_, rowIndex) => {
const row = {roomType: `房间${rowIndex + 1}`};
days.value.forEach((day) => {
const isBooked = Math.random() > 0.3;
if (isBooked) {
const customerName = `客户${Math.floor(Math.random() * 100) + 1}`;
row[day.prop] = {status: "已预订", customerName};
} else {
row[day.prop] = {status: "可用", customerName: ""};
}
});
return row;
});
const availableRoomsRow = {roomType: '剩几间房'};
days.value.forEach((day) => {
const availableCount = roomData.reduce((count, room) => {
return room[day.prop].status === '可用' ? count + 1 : count;
}, 0);
availableRoomsRow[day.prop] = `剩余${availableCount}间`;
});
tableData.value = [availableRoomsRow, ...roomData];
dataCache.value[monthKey] = tableData.value;
};
// 处理单元格点击事件
const handleCellClick = (row, prop) => {
if (row.roomType === '剩几间房') return;
if (row[prop].status === '可用') {
editingCell.value = {rowProp: row.roomType, dayProp: prop};
row[prop].customerName = '';
} else {
row[prop] = {status: '可用', customerName: ''};
updateAvailableRooms(prop);
}
};
// 保存预订信息
const saveBooking = (row, prop) => {
const customerName = row[prop].customerName.trim();
if (customerName !== '') {
row[prop].status = '已预订';
} else {
row[prop].status = '可用';
}
editingCell.value = {rowProp: null, dayProp: null};
updateAvailableRooms(prop);
};
// 更新剩余房间数
const updateAvailableRooms = (prop) => {
const availableCount = tableData.value.slice(1).reduce((count, room) => {
return room[prop].status === '可用' ? count + 1 : count;
}, 0);
tableData.value[0][prop] = `剩余${availableCount}间`;
};
// 判断单元格是否正在编辑
const isEditing = (row, prop) => {
return editingCell.value.rowProp === row.roomType && editingCell.value.dayProp === prop;
};
// 判断单元格是否已预订
const isBooked = (row, prop) => {
return row[prop].status === '已预订';
};
// 导航功能
const previousMonth = () => {
currentMonthStartDate.value = subMonths(currentMonthStartDate.value, 1);
selectedDate.value = currentMonthStartDate.value;
resetScroll();
};
const nextMonth = () => {
currentMonthStartDate.value = addMonths(currentMonthStartDate.value, 1);
selectedDate.value = currentMonthStartDate.value;
resetScroll();
};
// 重置滚动条
const resetScroll = () => {
if (tableRef.value) {
tableRef.value.setScrollLeft(0); // 将横向滚动条位置设置为 0
}
};
// 处理日期变化
const handleDateChange = (date) => {
if (!date) return
selectedDate.value = date;
currentMonthStartDate.value = startOfMonth(date);
// 调用滚动函数
scrollToSelectedDate(date);
};
const scrollToSelectedDate = (date) => {
const selectedDay = format(date, 'dd');
const columnIndex = parseInt(selectedDay) - 1; // 获取列索引,假设日期为 01 - 31 对应索引 0 - 30
const columnWidth = 150; // 根据你的列宽设置
const tableWidth = tableRef.value.$el.offsetWidth;
// 计算居中位置
const scrollLeft = (columnIndex + 1) * columnWidth - (tableWidth / 2) + (columnWidth / 2);
// 调用 setScrollLeft 滚动到对应列
if (tableRef.value) {
tableRef.value.setScrollLeft(scrollLeft);
}
};
// 判断当前列是否是选择的日期
const isCurrentDay = (dayProp) => {
const selectedDay = format(selectedDate.value, 'dd');
return dayProp === `day${selectedDay * 1}`;
};
const columnStyle = ({row, column}) => {
const dayProp = column.property;
if (selectedDate.value && isCurrentDay(dayProp)) {
return {backgroundColor: '#1C86EE', color: '#fff'}; // 高亮的样式
}
return {};
};
// 监听月份变化,更新表格数据
watch(currentMonthStartDate, updateTableData);
// 初始化
onMounted(() => {
updateTableData();
});
</script>
<style scoped>
.calendar {
max-width: 100%;
margin: 0 auto;
}
.controls {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.header-cell {
white-space: pre-line;
text-align: center;
}
.cell-content {
cursor: pointer;
white-space: pre-line;
display: flex;
align-items: center;
justify-content: center; /* 使内容居中 */
height: 100%; /* 确保高度填充 */
}
.cell-content.non-clickable {
cursor: default; /* 禁用点击手势 */
}
.editing-cell {
padding: 5px;
}
</style>
总结
通过本次分享,我们学习了如何使用 Vue 3 和 Element Plus 创建一个功能完备的动态日历组件。这个组件展示了 Vue 的响应式能力和 Element Plus 组件库的强大功能,能够高效地处理房间预订的需求。希望这篇博客能对你的项目开发有所帮助!