本项目实现了一个美观实用的酒店订房日期选择器界面,具有完整的日期选择功能和预订计算逻辑。
效果演示
页面结构分析
主要组成部分
整个页面采用居中布局,主要包含三个部分:
- 日期输入区域 - 两个只读输入框分别用于显示入住和退房日期
- 日历选择器 - 弹出式日历组件,支持月份切换和日期选择
- 预订摘要 - 展示预订详细信息和费用计算结果
<div class="container">
<h1>酒店预订</h1>
<!-- 日期输入区域 -->
<div class="date-inputs">
<div class="input-group">
<label for="checkin">入住日期</label>
<input type="text" id="checkin" onclick="showCalendar()" placeholder="选择入住日期" readonly>
</div>
<div class="input-group">
<label for="checkout">退房日期</label>
<input type="text" id="checkout" onclick="showCalendar()" placeholder="选择退房日期" readonly>
</div>
</div>
<!-- 日历容器 -->
<div class="calendar-container" id="calendar">
<div class="calendar-header">
<button class="nav-button" onclick="changeMonth(-1)">‹</button>
<h2 id="currentMonth"></h2>
<button class="nav-button" onclick="changeMonth(1)">›</button>
</div>
<div class="calendar-grid" id="calendarGrid"></div>
</div>
<!-- 预订摘要 -->
<div class="booking-summary" id="bookingSummary">
<h3>预订详情</h3>
<div class="summary-item">
<span>入住日期:</span>
<span id="summaryCheckin"></span>
</div>
<div class="summary-item">
<span>退房日期:</span>
<span id="summaryCheckout"></span>
</div>
<div class="summary-item">
<span>入住天数:</span>
<span id="summaryNights"></span>
</div>
<div class="summary-item">
<span>房间单价:</span>
<span>¥<span id="summaryPrice">388</span></span>
</div>
<div class="summary-item total">
<span>总价:</span>
<span>¥<span id="summaryTotal">0</span></span>
</div>
</div>
</div>
样式设计
日历交互效果
日历中的每个日期格子都实现了丰富的交互反馈:
- 默认状态:基础数字展示
- 悬停效果:轻微放大并改变背景色
- 选中状态:蓝色高亮标识
- 范围选择:紫色标识起止日期,浅蓝标识中间日期
- 特殊标记:今日边框、已预订置灰、其他月份淡化
.day {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 24px;
position: relative;
}
.day:hover:not(.disabled):not(.selected) {
background: #f0f0f0;
transform: scale(1.05);
}
.day.selected {
background: #667eea;
color: white;
font-weight: 600;
}
.day.start-date,
.day.end-date {
background: #764ba2;
color: white;
}
.day.in-range {
background: #e0e7ff;
color: #667eea;
}
核心功能逻辑
初始化数据与配置
定义关键的状态变量:
- 当前显示的日历月份和年份
- 已选择的入住和退房日期
- 最小可选日期(默认为今天)
- 房间单价
- 已被预订的日期数组
let currentMonth = new Date().getMonth();
let currentYear = new Date().getFullYear();
let selectedCheckin = null;
let selectedCheckout = null;
let minDate = new Date();
let pricePerNight = 388;
let bookedDates = [
'2025-11-15',
'2025-11-16',
'2025-11-20',
'2025-11-21',
'2025-11-22'
];
日历渲染机制
日历渲染采用标准算法:
- 清空现有网格内容
- 添加星期标题栏
- 计算当前月份的第一天和最后一天
- 填充上个月末尾的几天(灰色显示)
- 填充本月所有日期(正常显示)
- 填充下个月开头的几天(灰色显示)
这样保证了每页日历始终显示完整的6行7列共42个格子。
function updateCalendar() {
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
document.getElementById('currentMonth').textContent = `${currentYear}年 ${monthNames[currentMonth]}`;
const grid = document.getElementById('calendarGrid');
grid.innerHTML = '';
// 添加星期标题
const dayHeaders = ['日', '一', '二', '三', '四', '五', '六'];
dayHeaders.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'day-header';
dayHeader.textContent = day;
grid.appendChild(dayHeader);
});
// 获取月份第一天和最后一天
const firstDay = new Date(currentYear, currentMonth, 1);
const lastDay = new Date(currentYear, currentMonth + 1, 0);
const prevLastDay = new Date(currentYear, currentMonth, 0);
// 添加上月末尾日期
const firstDayOfWeek = firstDay.getDay();
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
const day = prevLastDay.getDate() - i;
const dayElement = createDayElement(day, true);
grid.appendChild(dayElement);
}
// 添加当月日期
for (let day = 1; day <= lastDay.getDate(); day++) {
const dayElement = createDayElement(day, false);
grid.appendChild(dayElement);
}
// 添加下月开头日期
const remainingDays = 42 - (firstDayOfWeek + lastDay.getDate());
for (let day = 1; day <= remainingDays; day++) {
const dayElement = createDayElement(day, true);
grid.appendChild(dayElement);
}
}
日期选择逻辑
日期选择遵循以下规则:
- 不允许选择已预订或过去的日期
- 第一次点击设置入住日期
- 第二次点击若晚于入住日期则设为退房日期,否则更新入住日期
- 选择完整区间后自动隐藏日历并显示预订摘要
function selectDate(date) {
const dateStr = formatDate(date);
// 检查是否可预订
if (bookedDates.includes(dateStr) || date < minDate) {
return;
}
if (date < minDate) {
return;
}
if (!selectedCheckin || (selectedCheckin && selectedCheckout)) {
selectedCheckin = date;
selectedCheckout = null;
} else {
// 选择退房日期
if (date > selectedCheckin) {
// 检查选择的日期范围内是否有已预订的日期
if (hasBookedDateInRange(selectedCheckin, date)) {
alert('选择的日期范围内包含已预订的日期,请重新选择');
return;
}
selectedCheckout = date;
} else {
selectedCheckin = date;
selectedCheckout = null;
}
}
updateSelection();
updateInputs();
if (selectedCheckin && selectedCheckout) {
setTimeout(() => {
document.getElementById('calendar').style.display = 'none';
showBookingSummary();
}, 300);
}
}
预订信息计算
预订摘要会动态计算:
- 入住天数:通过时间差计算得出
- 总价:天数乘以单价
- 格式化显示:将日期转换为易读的中文格式
function showBookingSummary() {
if (selectedCheckin && selectedCheckout) {
const nights = Math.ceil((selectedCheckout - selectedCheckin) / (1000 * 60 * 60 * 24));
const total = nights * pricePerNight;
document.getElementById('summaryCheckin').textContent = formatDateForDisplay(selectedCheckin);
document.getElementById('summaryCheckout').textContent = formatDateForDisplay(selectedCheckout);
document.getElementById('summaryNights').textContent = `${nights} 晚`;
document.getElementById('summaryTotal').textContent = total;
document.getElementById('bookingSummary').style.display = 'block';
}
}
功能增强建议
- 动态价格展示:根据日期显示不同价格
- 特殊价格标识:节假日、促销价等特殊标记
- 日期范围限制:最多能预定多长时间内的房间
- 房间库存显示:在 day 元素上展示剩余房间数
完整代码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>酒店订房日期选择器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
display: flex;
justify-content: center;
padding: 20px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
max-width: 800px;
width: 100%;
padding: 10px;
}
.date-inputs {
display: flex;
gap: 20px;
margin-bottom: 30px;
flex-wrap: wrap;
}
.input-group {
flex: 1;
min-width: 200px;
}
.input-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
.input-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s ease;
cursor: pointer;
background: #f8f9fa;
}
/* */
.calendar-container {
position: relative;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-top: 20px;
display: none;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.calendar-header h2 {
color: #333;
font-size: 20px;
}
.nav-button {
background: #667eea;
color: white;
border: none;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.nav-button:hover {
background: #5a67d8;
transform: translateY(-1px);
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
}
/* 日历交互效果 */
.day-header {
text-align: center;
padding: 10px;
font-weight: 600;
color: #666;
font-size: 14px;
}
.day {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 24px;
position: relative;
}
.day:hover:not(.disabled):not(.selected) {
background: #f0f0f0;
transform: scale(1.05);
}
.day.other-month {
color: #ccc;
cursor: default;
}
.day.disabled {
color: #ddd;
cursor: not-allowed;
background: #f5f5f5;
}
.day.selected {
background: #667eea;
color: white;
font-weight: 600;
}
.day.start-date,
.day.end-date {
background: #764ba2;
color: white;
}
.day.in-range {
background: #e0e7ff;
color: #667eea;
}
.day.today {
border: 2px solid #667eea;
}
.price-tag {
color: #667eea;
font-weight: 600;
}
.day.selected .price-tag,
.day.start-date .price-tag,
.day.end-date .price-tag {
color: white;
}
/* */
.booking-summary {
margin-top: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 15px;
display: none;
}
.booking-summary h3 {
color: #333;
margin-bottom: 15px;
}
.summary-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
color: #555;
}
.summary-item.total {
font-weight: 600;
font-size: 18px;
color: #667eea;
border-top: 2px solid #e0e0e0;
padding-top: 10px;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>酒店预订</h1>
<!-- 日期输入区域 -->
<div class="date-inputs">
<div class="input-group">
<label for="checkin">入住日期</label>
<input type="text" id="checkin" onclick="showCalendar()" placeholder="选择入住日期" readonly>
</div>
<div class="input-group">
<label for="checkout">退房日期</label>
<input type="text" id="checkout" onclick="showCalendar()" placeholder="选择退房日期" readonly>
</div>
</div>
<!-- 日历容器 -->
<div class="calendar-container" id="calendar">
<div class="calendar-header">
<button class="nav-button" onclick="changeMonth(-1)">‹</button>
<h2 id="currentMonth"></h2>
<button class="nav-button" onclick="changeMonth(1)">›</button>
</div>
<div class="calendar-grid" id="calendarGrid"></div>
</div>
<!-- 预订摘要 -->
<div class="booking-summary" id="bookingSummary">
<h3>预订详情</h3>
<div class="summary-item">
<span>入住日期:</span>
<span id="summaryCheckin"></span>
</div>
<div class="summary-item">
<span>退房日期:</span>
<span id="summaryCheckout"></span>
</div>
<div class="summary-item">
<span>入住天数:</span>
<span id="summaryNights"></span>
</div>
<div class="summary-item">
<span>房间单价:</span>
<span>¥<span id="summaryPrice">388</span></span>
</div>
<div class="summary-item total">
<span>总价:</span>
<span>¥<span id="summaryTotal">0</span></span>
</div>
</div>
</div>
<script>
let currentMonth = new Date().getMonth();
let currentYear = new Date().getFullYear();
let selectedCheckin = null;
let selectedCheckout = null;
let minDate = new Date();
let pricePerNight = 388;
let bookedDates = [
'2025-11-15',
'2025-11-16',
'2025-11-20',
'2025-11-21',
'2025-11-22'
];
updateCalendar();
// 显示日历
function showCalendar() {
document.getElementById('calendar').style.display = 'block';
}
// 更新日历
function changeMonth(direction) {
currentMonth += direction;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
} else if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
updateCalendar();
}
// 创建日期元素
function updateCalendar() {
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
document.getElementById('currentMonth').textContent = `${currentYear}年 ${monthNames[currentMonth]}`;
const grid = document.getElementById('calendarGrid');
grid.innerHTML = '';
// 添加星期标题
const dayHeaders = ['日', '一', '二', '三', '四', '五', '六'];
dayHeaders.forEach(day => {
const dayHeader = document.createElement('div');
dayHeader.className = 'day-header';
dayHeader.textContent = day;
grid.appendChild(dayHeader);
});
// 获取月份第一天和最后一天
const firstDay = new Date(currentYear, currentMonth, 1);
const lastDay = new Date(currentYear, currentMonth + 1, 0);
const prevLastDay = new Date(currentYear, currentMonth, 0);
// 添加上月末尾日期
const firstDayOfWeek = firstDay.getDay();
for (let i = firstDayOfWeek - 1; i >= 0; i--) {
const day = prevLastDay.getDate() - i;
const dayElement = createDayElement(day, true);
grid.appendChild(dayElement);
}
// 添加当月日期
for (let day = 1; day <= lastDay.getDate(); day++) {
const dayElement = createDayElement(day, false);
grid.appendChild(dayElement);
}
// 添加下月开头日期
const remainingDays = 42 - (firstDayOfWeek + lastDay.getDate());
for (let day = 1; day <= remainingDays; day++) {
const dayElement = createDayElement(day, true);
grid.appendChild(dayElement);
}
}
function createDayElement(day, isOtherMonth) {
const dayElement = document.createElement('div');
dayElement.className = 'day';
dayElement.textContent = day;
if (isOtherMonth) {
dayElement.classList.add('other-month');
} else {
const date = new Date(currentYear, currentMonth, day);
const dateStr = formatDate(date);
// 检查是否是今天
if (isToday(date)) {
dayElement.classList.add('today');
}
// 检查是否已预订
if (bookedDates.includes(dateStr)) {
dayElement.classList.add('disabled');
dayElement.title = '已预订';
}
// 检查是否早于最小日期
if (date < minDate) {
dayElement.classList.add('disabled');
}
// 添加价格标签
if (!dayElement.classList.contains('disabled')) {
const priceTag = document.createElement('span');
priceTag.className = 'price-tag';
priceTag.textContent = `¥${pricePerNight}`;
dayElement.appendChild(priceTag);
}
// 处理日期选择
dayElement.addEventListener('click', () => selectDate(date));
}
return dayElement;
}
function selectDate(date) {
const dateStr = formatDate(date);
// 检查是否可预订
if (bookedDates.includes(dateStr) || date < minDate) {
return;
}
if (date < minDate) {
return;
}
if (!selectedCheckin || (selectedCheckin && selectedCheckout)) {
selectedCheckin = date;
selectedCheckout = null;
} else {
// 选择退房日期
if (date > selectedCheckin) {
// 检查选择的日期范围内是否有已预订的日期
if (hasBookedDateInRange(selectedCheckin, date)) {
alert('选择的日期范围内包含已预订的日期,请重新选择');
return;
}
selectedCheckout = date;
} else {
selectedCheckin = date;
selectedCheckout = null;
}
}
updateSelection();
updateInputs();
if (selectedCheckin && selectedCheckout) {
setTimeout(() => {
document.getElementById('calendar').style.display = 'none';
showBookingSummary();
}, 300);
}
}
function hasBookedDateInRange(startDate, endDate) {
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const dateStr = formatDate(new Date(d));
if (bookedDates.includes(dateStr)) {
return true;
}
}
return false;
}
function updateSelection() {
const days = document.querySelectorAll('.day:not(.other-month)');
days.forEach(day => {
const dayNumber = parseInt(day.textContent);
const date = new Date(currentYear, currentMonth, dayNumber);
const dateStr = formatDate(date);
day.classList.remove('selected', 'start-date', 'end-date', 'in-range');
if (selectedCheckin && dateStr === formatDate(selectedCheckin)) {
day.classList.add('selected', 'start-date');
}
if (selectedCheckout && dateStr === formatDate(selectedCheckout)) {
day.classList.add('selected', 'end-date');
}
if (selectedCheckin && selectedCheckout &&
date > selectedCheckin && date < selectedCheckout) {
day.classList.add('in-range');
}
});
}
function updateInputs() {
const checkinInput = document.getElementById('checkin');
const checkoutInput = document.getElementById('checkout');
if (selectedCheckin) {
checkinInput.value = formatDateForDisplay(selectedCheckin);
}
if (selectedCheckout) {
checkoutInput.value = formatDateForDisplay(selectedCheckout);
}
}
function showBookingSummary() {
if (selectedCheckin && selectedCheckout) {
const nights = Math.ceil((selectedCheckout - selectedCheckin) / (1000 * 60 * 60 * 24));
const total = nights * pricePerNight;
document.getElementById('summaryCheckin').textContent = formatDateForDisplay(selectedCheckin);
document.getElementById('summaryCheckout').textContent = formatDateForDisplay(selectedCheckout);
document.getElementById('summaryNights').textContent = `${nights} 晚`;
document.getElementById('summaryTotal').textContent = total;
document.getElementById('bookingSummary').style.display = 'block';
}
}
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDateForDisplay(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const weekday = weekdays[date.getDay()];
return `${year}年${month}月${day}日 ${weekday}`;
}
function isToday(date) {
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
}
</script>
</body>
</html>