使用 HTML + JavaScript 实现酒店订房日期选择器(附完整源码)

0 阅读5分钟

本项目实现了一个美观实用的酒店订房日期选择器界面,具有完整的日期选择功能和预订计算逻辑。

效果演示

img

img

页面结构分析

主要组成部分

整个页面采用居中布局,主要包含三个部分:

  1. 日期输入区域 - 两个只读输入框分别用于显示入住和退房日期
  2. 日历选择器 - 弹出式日历组件,支持月份切换和日期选择
  3. 预订摘要 - 展示预订详细信息和费用计算结果
<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'
];

日历渲染机制

日历渲染采用标准算法:

  1. 清空现有网格内容
  2. 添加星期标题栏
  3. 计算当前月份的第一天和最后一天
  4. 填充上个月末尾的几天(灰色显示)
  5. 填充本月所有日期(正常显示)
  6. 填充下个月开头的几天(灰色显示)

这样保证了每页日历始终显示完整的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);
    }
}

日期选择逻辑

日期选择遵循以下规则:

  1. 不允许选择已预订或过去的日期
  2. 第一次点击设置入住日期
  3. 第二次点击若晚于入住日期则设为退房日期,否则更新入住日期
  4. 选择完整区间后自动隐藏日历并显示预订摘要
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>