基于HTML/JavaScript构建交互式酒店日期选择器:完整实现方案与源码解析

一、组件功能需求分析

在酒店预订场景中,日期选择器需要满足以下核心需求:

  1. 日期范围约束:退房日期必须晚于入住日期,默认限制最大预订天数(如30天)
  2. 动态价格计算:根据入住天数和基础房价自动计算总价
  3. 交互状态可视化
    • 当前选中日期高亮显示
    • 连续日期范围特殊标记
    • 不可选日期(如过去日期、已满房日期)置灰处理
  4. 响应式适配:适配桌面端和移动端不同屏幕尺寸

二、组件架构设计

1. 整体结构

采用模块化设计思想,将组件拆分为三个核心模块:

  1. <div class="booking-container">
  2. <!-- 日期输入区 -->
  3. <div class="date-inputs">
  4. <!-- 包含两个只读输入框 -->
  5. </div>
  6. <!-- 日历面板 -->
  7. <div class="calendar-panel">
  8. <!-- 包含月份导航和日期网格 -->
  9. </div>
  10. <!-- 预订摘要 -->
  11. <div class="booking-summary">
  12. <!-- 显示价格计算结果 -->
  13. </div>
  14. </div>

2. 数据模型设计

  1. const bookingModel = {
  2. checkInDate: null, // 入住日期
  3. checkOutDate: null, // 退房日期
  4. basePrice: 388, // 基础房价
  5. maxStayDays: 30, // 最大入住天数
  6. disabledDates: [], // 不可选日期数组
  7. // 计算方法
  8. getStayDays() {
  9. if (!this.checkInDate || !this.checkOutDate) return 0
  10. return Math.ceil((this.checkOutDate - this.checkInDate) / (1000*60*60*24))
  11. },
  12. getTotalPrice() {
  13. return this.getStayDays() * this.basePrice
  14. }
  15. }

三、核心功能实现

1. 日历渲染引擎

  1. function renderCalendar(year, month) {
  2. const calendarGrid = document.getElementById('calendarGrid')
  3. const firstDay = new Date(year, month, 1)
  4. const daysInMonth = new Date(year, month + 1, 0).getDate()
  5. const startingDay = firstDay.getDay()
  6. // 生成日期单元格
  7. let html = ''
  8. for (let i = 0; i < startingDay; i++) {
  9. html += '<div></div>' // 上个月日期占位
  10. }
  11. for (let day = 1; day <= daysInMonth; day++) {
  12. const currentDate = new Date(year, month, day)
  13. const isToday = isSameDay(currentDate, new Date())
  14. const isDisabled = isDateDisabled(currentDate)
  15. html += `
  16. <div class="day ${getClassNames(currentDate)}"
  17. data-date="${currentDate.toISOString()}">
  18. ${day}
  19. ${isToday ? '<span></span>' : ''}
  20. </div>
  21. `
  22. }
  23. calendarGrid.innerHTML = html
  24. }
  25. // 辅助函数:获取日期单元格的CSS类
  26. function getClassNames(date) {
  27. const classes = []
  28. if (isSameDay(date, bookingModel.checkInDate)) classes.push('start-date')
  29. if (isSameDay(date, bookingModel.checkOutDate)) classes.push('end-date')
  30. if (isDateInRange(date)) classes.push('in-range')
  31. if (bookingModel.disabledDates.includes(date.toISOString())) classes.push('disabled')
  32. return classes.join(' ')
  33. }

2. 日期选择逻辑

  1. let isSelectingRange = false
  2. let tempCheckOutDate = null
  3. document.querySelectorAll('.day:not(.disabled)').forEach(day => {
  4. day.addEventListener('click', function() {
  5. const selectedDate = new Date(this.dataset.date)
  6. if (!bookingModel.checkInDate) {
  7. // 首次选择入住日期
  8. bookingModel.checkInDate = selectedDate
  9. tempCheckOutDate = new Date(selectedDate)
  10. tempCheckOutDate.setDate(selectedDate.getDate() + 1)
  11. isSelectingRange = true
  12. } else if (isSelectingRange) {
  13. // 选择退房日期
  14. if (selectedDate < bookingModel.checkInDate) {
  15. // 如果点击的日期早于入住日期,重新选择入住日期
  16. bookingModel.checkInDate = selectedDate
  17. } else {
  18. bookingModel.checkOutDate = selectedDate
  19. isSelectingRange = false
  20. }
  21. }
  22. updateUI()
  23. })
  24. })
  25. // 鼠标移动时高亮范围(桌面端优化)
  26. document.querySelector('.calendar-grid').addEventListener('mouseover', (e) => {
  27. if (!isSelectingRange || !bookingModel.checkInDate) return
  28. const day = e.target.closest('.day:not(.disabled)')
  29. if (!day) return
  30. tempCheckOutDate = new Date(day.dataset.date)
  31. updateUI()
  32. })

3. 交互状态管理

  1. function updateUI() {
  2. // 更新输入框显示
  3. document.getElementById('checkin').value =
  4. bookingModel.checkInDate ? formatDate(bookingModel.checkInDate) : ''
  5. document.getElementById('checkout').value =
  6. bookingModel.checkOutDate ? formatDate(bookingModel.checkOutDate) : ''
  7. // 更新预订摘要
  8. document.getElementById('summaryNights').textContent =
  9. bookingModel.getStayDays() || 0
  10. document.getElementById('summaryTotal').textContent =
  11. bookingModel.getTotalPrice() || 0
  12. // 重新渲染日历
  13. const currentMonth = document.getElementById('currentMonth')
  14. const [year, month] = currentMonth.dataset.ym.split('-').map(Number)
  15. renderCalendar(year, month)
  16. }
  17. // 日期格式化工具函数
  18. function formatDate(date) {
  19. return date.toISOString().split('T')[0].replace(/-/g, '/')
  20. }

四、样式设计与交互优化

1. 核心样式实现

  1. .calendar-grid {
  2. display: grid;
  3. grid-template-columns: repeat(7, 1fr);
  4. gap: 8px;
  5. padding: 16px;
  6. }
  7. .day {
  8. aspect-ratio: 1;
  9. display: flex;
  10. align-items: center;
  11. justify-content: center;
  12. border-radius: 8px;
  13. position: relative;
  14. font-size: 14px;
  15. transition: all 0.2s ease;
  16. }
  17. /* 交互状态样式 */
  18. .day:hover:not(.disabled):not(.selected) {
  19. background-color: #f5f5f5;
  20. transform: scale(1.05);
  21. }
  22. .day.selected {
  23. background-color: #4285f4;
  24. color: white;
  25. font-weight: bold;
  26. }
  27. .day.in-range {
  28. background-color: #e8f0fe;
  29. }
  30. .today-marker {
  31. position: absolute;
  32. bottom: 2px;
  33. width: 4px;
  34. height: 4px;
  35. border-radius: 50%;
  36. background-color: #ff5722;
  37. }

2. 移动端适配优化

  1. @media (max-width: 768px) {
  2. .booking-container {
  3. flex-direction: column;
  4. padding: 12px;
  5. }
  6. .calendar-panel {
  7. position: fixed;
  8. top: 50%;
  9. left: 50%;
  10. transform: translate(-50%, -50%);
  11. width: 90%;
  12. max-width: 400px;
  13. box-shadow: 0 4px 20px rgba(0,0,0,0.15);
  14. }
  15. .day {
  16. font-size: 12px;
  17. }
  18. }

五、完整实现代码

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>酒店日期选择器</title>
  7. <style>
  8. /* 完整CSS样式见上文代码块 */
  9. </style>
  10. </head>
  11. <body>
  12. <div class="booking-container">
  13. <div class="date-inputs">
  14. <div class="input-group">
  15. <label for="checkin">入住日期</label>
  16. <input type="text" id="checkin" readonly placeholder="选择入住日期">
  17. </div>
  18. <div class="input-group">
  19. <label for="checkout">退房日期</label>
  20. <input type="text" id="checkout" readonly placeholder="选择退房日期">
  21. </div>
  22. </div>
  23. <div class="calendar-panel">
  24. <div class="calendar-header">
  25. <button class="nav-button" onclick="changeMonth(-1)"></button>
  26. <h2 id="currentMonth"></h2>
  27. <button class="nav-button" onclick="changeMonth(1)"></button>
  28. </div>
  29. <div class="calendar-grid" id="calendarGrid"></div>
  30. </div>
  31. <div class="booking-summary">
  32. <h3>预订详情</h3>
  33. <div class="summary-item">
  34. <span>入住日期:</span>
  35. <span id="summaryCheckin">-</span>
  36. </div>
  37. <div class="summary-item">
  38. <span>退房日期:</span>
  39. <span id="summaryCheckout">-</span>
  40. </div>
  41. <div class="summary-item">
  42. <span>入住天数:</span>
  43. <span id="summaryNights">0</span>
  44. </div>
  45. <div class="summary-item">
  46. <span>房间单价:</span>
  47. <span>¥<span id="summaryPrice">388</span></span>
  48. </div>
  49. <div class="summary-item total">
  50. <span>总价:</span>
  51. <span>¥<span id="summaryTotal">0</span></span>
  52. </div>
  53. </div>
  54. </div>
  55. <script>
  56. // 完整JavaScript实现见上文代码块
  57. // 包含bookingModel定义、renderCalendar、事件处理等函数
  58. // 初始化组件
  59. document.addEventListener('DOMContentLoaded', () => {
  60. const today = new Date()
  61. const currentMonth = today.getMonth()
  62. const currentYear = today.getFullYear()
  63. document.getElementById('currentMonth').textContent =
  64. `${currentYear}年${currentMonth + 1}月`
  65. document.getElementById('currentMonth').dataset.ym = `${currentYear}-${currentMonth}`
  66. renderCalendar(currentYear, currentMonth)
  67. // 初始化输入框点击事件
  68. document.getElementById('checkin').addEventListener('click', () => {
  69. // 实际项目中这里应该显示日历面板
  70. console.log('点击了入住日期输入框')
  71. })
  72. })
  73. </script>
  74. </body>
  75. </html>

六、扩展功能建议

  1. 多房间选择:添加房间数量选择器,动态计算总价
  2. 价格日历:根据日期显示不同价格(周末/节假日溢价)
  3. 数据持久化:使用localStorage保存用户选择
  4. 国际化支持:添加多语言和日期格式支持
  5. 服务端集成:通过API获取不可订日期和动态价格

本文实现的日期选择器组件具有以下优势:

  • 纯原生实现,无需依赖第三方库
  • 完整的日期范围选择逻辑
  • 丰富的交互状态反馈
  • 响应式设计适配多设备
  • 模块化架构便于扩展

开发者可根据实际项目需求在此基础上进行功能扩展和样式定制,建议将核心逻辑封装为可复用的组件,便于在不同项目中集成使用。