一、组件功能需求分析
在酒店预订场景中,日期选择器需要满足以下核心需求:
- 日期范围约束:退房日期必须晚于入住日期,默认限制最大预订天数(如30天)
- 动态价格计算:根据入住天数和基础房价自动计算总价
- 交互状态可视化:
- 当前选中日期高亮显示
- 连续日期范围特殊标记
- 不可选日期(如过去日期、已满房日期)置灰处理
- 响应式适配:适配桌面端和移动端不同屏幕尺寸
二、组件架构设计
1. 整体结构
采用模块化设计思想,将组件拆分为三个核心模块:
<div class="booking-container"><!-- 日期输入区 --><div class="date-inputs"><!-- 包含两个只读输入框 --></div><!-- 日历面板 --><div class="calendar-panel"><!-- 包含月份导航和日期网格 --></div><!-- 预订摘要 --><div class="booking-summary"><!-- 显示价格计算结果 --></div></div>
2. 数据模型设计
const bookingModel = {checkInDate: null, // 入住日期checkOutDate: null, // 退房日期basePrice: 388, // 基础房价maxStayDays: 30, // 最大入住天数disabledDates: [], // 不可选日期数组// 计算方法getStayDays() {if (!this.checkInDate || !this.checkOutDate) return 0return Math.ceil((this.checkOutDate - this.checkInDate) / (1000*60*60*24))},getTotalPrice() {return this.getStayDays() * this.basePrice}}
三、核心功能实现
1. 日历渲染引擎
function renderCalendar(year, month) {const calendarGrid = document.getElementById('calendarGrid')const firstDay = new Date(year, month, 1)const daysInMonth = new Date(year, month + 1, 0).getDate()const startingDay = firstDay.getDay()// 生成日期单元格let html = ''for (let i = 0; i < startingDay; i++) {html += '<div></div>' // 上个月日期占位}for (let day = 1; day <= daysInMonth; day++) {const currentDate = new Date(year, month, day)const isToday = isSameDay(currentDate, new Date())const isDisabled = isDateDisabled(currentDate)html += `<div class="day ${getClassNames(currentDate)}"data-date="${currentDate.toISOString()}">${day}${isToday ? '<span></span>' : ''}</div>`}calendarGrid.innerHTML = html}// 辅助函数:获取日期单元格的CSS类function getClassNames(date) {const classes = []if (isSameDay(date, bookingModel.checkInDate)) classes.push('start-date')if (isSameDay(date, bookingModel.checkOutDate)) classes.push('end-date')if (isDateInRange(date)) classes.push('in-range')if (bookingModel.disabledDates.includes(date.toISOString())) classes.push('disabled')return classes.join(' ')}
2. 日期选择逻辑
let isSelectingRange = falselet tempCheckOutDate = nulldocument.querySelectorAll('.day:not(.disabled)').forEach(day => {day.addEventListener('click', function() {const selectedDate = new Date(this.dataset.date)if (!bookingModel.checkInDate) {// 首次选择入住日期bookingModel.checkInDate = selectedDatetempCheckOutDate = new Date(selectedDate)tempCheckOutDate.setDate(selectedDate.getDate() + 1)isSelectingRange = true} else if (isSelectingRange) {// 选择退房日期if (selectedDate < bookingModel.checkInDate) {// 如果点击的日期早于入住日期,重新选择入住日期bookingModel.checkInDate = selectedDate} else {bookingModel.checkOutDate = selectedDateisSelectingRange = false}}updateUI()})})// 鼠标移动时高亮范围(桌面端优化)document.querySelector('.calendar-grid').addEventListener('mouseover', (e) => {if (!isSelectingRange || !bookingModel.checkInDate) returnconst day = e.target.closest('.day:not(.disabled)')if (!day) returntempCheckOutDate = new Date(day.dataset.date)updateUI()})
3. 交互状态管理
function updateUI() {// 更新输入框显示document.getElementById('checkin').value =bookingModel.checkInDate ? formatDate(bookingModel.checkInDate) : ''document.getElementById('checkout').value =bookingModel.checkOutDate ? formatDate(bookingModel.checkOutDate) : ''// 更新预订摘要document.getElementById('summaryNights').textContent =bookingModel.getStayDays() || 0document.getElementById('summaryTotal').textContent =bookingModel.getTotalPrice() || 0// 重新渲染日历const currentMonth = document.getElementById('currentMonth')const [year, month] = currentMonth.dataset.ym.split('-').map(Number)renderCalendar(year, month)}// 日期格式化工具函数function formatDate(date) {return date.toISOString().split('T')[0].replace(/-/g, '/')}
四、样式设计与交互优化
1. 核心样式实现
.calendar-grid {display: grid;grid-template-columns: repeat(7, 1fr);gap: 8px;padding: 16px;}.day {aspect-ratio: 1;display: flex;align-items: center;justify-content: center;border-radius: 8px;position: relative;font-size: 14px;transition: all 0.2s ease;}/* 交互状态样式 */.day:hover:not(.disabled):not(.selected) {background-color: #f5f5f5;transform: scale(1.05);}.day.selected {background-color: #4285f4;color: white;font-weight: bold;}.day.in-range {background-color: #e8f0fe;}.today-marker {position: absolute;bottom: 2px;width: 4px;height: 4px;border-radius: 50%;background-color: #ff5722;}
2. 移动端适配优化
@media (max-width: 768px) {.booking-container {flex-direction: column;padding: 12px;}.calendar-panel {position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);width: 90%;max-width: 400px;box-shadow: 0 4px 20px rgba(0,0,0,0.15);}.day {font-size: 12px;}}
五、完整实现代码
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>酒店日期选择器</title><style>/* 完整CSS样式见上文代码块 */</style></head><body><div class="booking-container"><div class="date-inputs"><div class="input-group"><label for="checkin">入住日期</label><input type="text" id="checkin" readonly placeholder="选择入住日期"></div><div class="input-group"><label for="checkout">退房日期</label><input type="text" id="checkout" readonly placeholder="选择退房日期"></div></div><div class="calendar-panel"><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"><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">0</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>// 完整JavaScript实现见上文代码块// 包含bookingModel定义、renderCalendar、事件处理等函数// 初始化组件document.addEventListener('DOMContentLoaded', () => {const today = new Date()const currentMonth = today.getMonth()const currentYear = today.getFullYear()document.getElementById('currentMonth').textContent =`${currentYear}年${currentMonth + 1}月`document.getElementById('currentMonth').dataset.ym = `${currentYear}-${currentMonth}`renderCalendar(currentYear, currentMonth)// 初始化输入框点击事件document.getElementById('checkin').addEventListener('click', () => {// 实际项目中这里应该显示日历面板console.log('点击了入住日期输入框')})})</script></body></html>
六、扩展功能建议
- 多房间选择:添加房间数量选择器,动态计算总价
- 价格日历:根据日期显示不同价格(周末/节假日溢价)
- 数据持久化:使用localStorage保存用户选择
- 国际化支持:添加多语言和日期格式支持
- 服务端集成:通过API获取不可订日期和动态价格
本文实现的日期选择器组件具有以下优势:
- 纯原生实现,无需依赖第三方库
- 完整的日期范围选择逻辑
- 丰富的交互状态反馈
- 响应式设计适配多设备
- 模块化架构便于扩展
开发者可根据实际项目需求在此基础上进行功能扩展和样式定制,建议将核心逻辑封装为可复用的组件,便于在不同项目中集成使用。