在旅游预订网站中,日期选择器是一个核心组件,特别是像携程这样的平台使用的双日期选择器,能够让用户直观地选择入住和退房日期。本文将详细介绍如何使用 HTML、CSS 和 JavaScript 实现一个仿携程的双日期选择器。效果演示
这个日期选择器具有以下特点:
双面板日历展示,方便用户同时查看两个月份
直观的日期选择交互,支持入住和退房日期的选择
自动计算并显示选择的天数
支持月份切换导航
直观的视觉反馈,包括选中日期、日期范围高亮等
用户只需点击日期输入框即可打开日历面板,先选择入住日期,再选择退房日期,系统会自动计算并显示总共的住宿晚数。
页面结构
页面主要由两大部分组成:日期显示栏和日历弹窗。
日期显示栏
日期显示栏采用 flex 布局,分为三个部分:入住日期显示、晚数显示和退房日期显示。
<div class="date-bar" id="dateBar"> <div class="item"> <div class="label">入住</div> <div class="val" id="checkInStr">请选择日期</div> </div> <div class="val" id="nightCount">0晚</div> <div class="item"> <div class="label">退房</div> <div class="val" id="checkOutStr">请选择日期</div> </div></div>
日历弹窗
日历弹窗在用户点击日期栏时显示,包含两个月的日历视图。
<div class="modal-wrap" id="calendarWrap"> <div class="modal-bd" id="calendarBox"></div></div>
核心功能实现
状态管理
系统使用多个状态变量来管理日期选择的状态。
var selectedIn = null; var selectedOut = null; var hoverDate = null; var currentMon = new Date(today.getFullYear(), today.getMonth(), 1); var maxMonth = 12;
日期选择逻辑
日期选择遵循以下规则:第一次点击选择入住日期,第二次点击选择退房日期(必须晚于入住日期)并关闭日历面板;再次打开日历面板时默认显示上次选择,点击则重新开始选择(清空之前的选择)。
td.onclick = (e) => { e.stopPropagation(); if (!selectedIn || (selectedIn && selectedOut)) { selectedIn = d; selectedOut = null; } else { if (d <= selectedIn) return; selectedOut = d; } updateCalendarStyles(); updateBar(); if (selectedIn && selectedOut) { setTimeout(() => close(), 100); }};
天数计算与显示
当选定入住和退房日期后,系统会自动计算间隔天数并显示。
function updateBar() { document.querySelector('#checkInStr').textContent = selectedIn ? format(selectedIn) : '请选择日期'; document.querySelector('#checkOutStr').textContent = selectedOut ? format(selectedOut) : '请选择日期'; if (selectedIn && selectedOut) { var nights = Math.ceil((selectedOut - selectedIn) / (1000 * 60 * 60 * 24)); document.querySelector('#nightCount').textContent = nights + '晚'; }}
日历渲染机制
日历采用动态渲染方式,每个月份独立渲染,包含完整的日期矩阵。
function createMonthTable(mon, position) { var box = document.createElement('div'); box.className = 'month-box'; var hd = document.createElement('div'); hd.className = 'month-hd'; if (position === 'first') { hd.innerHTML = `<span class="arrow" id="prev"><</span> <span>${mon.getFullYear()}年${mon.getMonth() + 1}月</span> `; } else { hd.innerHTML = ` <span>${mon.getFullYear()}年${mon.getMonth() + 1}月</span> <span class="arrow" id="next">></span>`; } box.appendChild(hd);
var table = document.createElement('table'); var head = document.createElement('thead'); head.innerHTML = '<tr><th>日</th><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th></tr>'; table.appendChild(head);
var firstDay = new Date(mon.getFullYear(), mon.getMonth(), 1); var lastDay = new Date(mon.getFullYear(), mon.getMonth() + 1, 0); var tr = document.createElement('tr'); var startWeek = firstDay.getDay(); for (var i = 0; i < startWeek; i++) { var td = document.createElement('td'); td.className = 'old'; tr.appendChild(td); } for (var d = firstDay.getDate(); d <= lastDay.getDate(); d++) { if (tr.children.length === 7) { table.appendChild(tr); tr = document.createElement('tr'); } var cellDate = new Date(mon.getFullYear(), mon.getMonth(), d); var td = createCell(cellDate); tr.appendChild(td); } while (tr.children.length < 7) { var td = document.createElement('td'); td.className = 'new'; tr.appendChild(td); } table.appendChild(tr); box.appendChild(table); return box;}
扩展建议
完整代码
git地址:https://gitee.com/ironpro/hjdemo/blob/master/calendar-xc/index.html<!doctype html><html lang="zh-CN"><head> <meta charset="utf-8"> <title>仿携程双日期选择</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #f5f5f5; min-height: 100vh; padding: 20px; } .container { max-width: 800px; margin: 0 auto; background: white; border-radius: 15px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); overflow: hidden; }
.header { background: #0086f6; color: white; padding: 20px; text-align: center; }
.header h1 { font-size: 28px; font-weight: 500; } .main { padding: 20px; display: flex; flex-direction: column; align-items: center; height: 500px; } .date-bar { display: flex; align-items: center; width: 350px; background: #fff; border: 1px solid #dcdfe6; border-radius: 4px; height: 44px; cursor: pointer; user-select: none; position: relative; z-index: 1000; }
.date-bar .item { flex: 1; text-align: center; position: relative; } .date-bar .item:last-child::after { display: none; }
.date-bar .label { font-size: 14px; color: #999; margin-bottom: 2px; }
.date-bar .val { font-size: 14px; color: #333; } #nightCount { display: inline-block; width: 32px; height: 18px; line-height: 18px; text-align: center; position: absolute; top: 50%; left: 50%; margin-top: -9px; margin-left: -15px; font-size: 14px; color: #666; z-index: 1; } #nightCount::before { content: ""; position: absolute; height: 1px; width: 11px; top: 9px; left: -13px; background-color: #dadfe6; } #nightCount::after { content: ""; position: absolute; height: 1px; width: 11px; top: 9px; left: 34px; background-color: #dadfe6; } .modal-wrap { position: absolute; top: 100%; left: 0; margin-top: 5px; background: #fff; border-radius: 4px; box-shadow: 0 2px 20px rgba(0, 0, 0, .15); z-index: 1000; display: none; }
.modal-bd { display: flex; }
.month-box { width: 260px; padding: 0 15px 15px; }
.month-hd { text-align: center; height: 40px; line-height: 40px; font-weight: 500; font-size: 15px; display: flex; justify-content: space-between; align-items: center; }
.month-hd .arrow { font-size: 20px; cursor: pointer; color: #666; padding: 0 8px; }
.month-hd .arrow:hover { color: #0086f6; }
table { width: 100%; border-collapse: collapse; }
table th { font-size: 14px; color: #999; height: 30px; }
table td { text-align: center; height: 32px; font-size: 14px; cursor: pointer; position: relative; }
table td.old, table td.new { color: #ccc; cursor: not-allowed; }
table td.today { color: #0086f6; font-weight: 700; }
table td.start, table td.end { background: #0086f6; color: #fff; }
table td.range { background: #bfe0fc; color: #fff; }
table td:hover:not(.disabled):not(.old):not(.new) { background: #4daaf8; color: #fff; } </style></head><body><div class="container"> <div class="header"> <h1>仿携程双日期选择</h1> </div> <div class="main"> <div class="date-bar" id="dateBar"> <div class="item"> <div class="label">入住</div> <div class="val" id="checkInStr">请选择日期</div> </div> <div class="val" id="nightCount">0晚</div> <div class="item"> <div class="label">退房</div> <div class="val" id="checkOutStr">请选择日期</div> </div> </div> <div class="modal-wrap" id="calendarWrap"> <div class="modal-bd" id="calendarBox"></div> </div> </div></div>
<script> var format = (d, sep = '-') => d.getFullYear() + sep + String(d.getMonth() + 1).padStart(2, '0') + sep + String(d.getDate()).padStart(2, '0'); var parse = str => new Date(str.replace(/-/g, '/')); var today = new Date(); today.setHours(0, 0, 0, 0);
var selectedIn = null; var selectedOut = null; var hoverDate = null; var currentMon = new Date(today.getFullYear(), today.getMonth(), 1); var maxMonth = 12;
document.querySelector('#dateBar').onclick = (e) => { e.stopPropagation(); if (document.querySelector('#calendarWrap').style.display === 'block') { close(); return; } if (!selectedIn) { currentMon = new Date(today.getFullYear(), today.getMonth(), 1); } else if (!document.querySelector('.modal-wrap').style.display || document.querySelector('.modal-wrap').style.display === 'none') { currentMon = new Date(selectedIn.getFullYear(), selectedIn.getMonth(), 1); } renderCalendar(); document.querySelector('#calendarWrap').style.display = 'block'; var dateBarRect = document.querySelector('#dateBar').getBoundingClientRect(); document.querySelector('#calendarWrap').style.left = dateBarRect.left + 'px'; document.querySelector('#calendarWrap').style.top = (dateBarRect.bottom + 5) + 'px'; };
document.addEventListener('click', (e) => { if (document.querySelector('#calendarWrap').style.display === 'block' && !(e.target.closest('#dateBar') || e.target.closest('#calendarWrap'))) { close(); } });
function renderCalendar() { document.querySelector('#calendarBox').innerHTML = ''; document.querySelector('#calendarBox').appendChild(createMonthTable(currentMon, 'first')); document.querySelector('#calendarBox').appendChild(createMonthTable(nextMonth(currentMon), 'second')); }
function createMonthTable(mon, position) { var box = document.createElement('div'); box.className = 'month-box'; var hd = document.createElement('div'); hd.className = 'month-hd'; if (position === 'first') { hd.innerHTML = `<span class="arrow" id="prev"><</span> <span>${mon.getFullYear()}年${mon.getMonth() + 1}月</span> `; } else { hd.innerHTML = ` <span>${mon.getFullYear()}年${mon.getMonth() + 1}月</span> <span class="arrow" id="next">></span>`; } box.appendChild(hd);
var table = document.createElement('table'); var head = document.createElement('thead'); head.innerHTML = '<tr><th>日</th><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th></tr>'; table.appendChild(head);
var firstDay = new Date(mon.getFullYear(), mon.getMonth(), 1); var lastDay = new Date(mon.getFullYear(), mon.getMonth() + 1, 0); var tr = document.createElement('tr');
var startWeek = firstDay.getDay(); for (var i = 0; i < startWeek; i++) { var td = document.createElement('td'); td.className = 'old'; tr.appendChild(td); }
for (var d = firstDay.getDate(); d <= lastDay.getDate(); d++) { if (tr.children.length === 7) { table.appendChild(tr); tr = document.createElement('tr'); } var cellDate = new Date(mon.getFullYear(), mon.getMonth(), d); var td = createCell(cellDate); tr.appendChild(td); } while (tr.children.length < 7) { var td = document.createElement('td'); td.className = 'new'; tr.appendChild(td); } table.appendChild(tr); box.appendChild(table); return box; }
function createCell(d) { var td = document.createElement('td'); var time = d.getTime(); td.textContent = d.getDate(); td.dataset.date = format(d); if (time < today.getTime()) { td.classList.add('disabled', 'old'); return td; } if (selectedIn && selectedOut) { if (time === selectedIn.getTime()) td.classList.add('start'); else if (time === selectedOut.getTime()) td.classList.add('end'); else if (time > selectedIn.getTime() && time < selectedOut.getTime()) td.classList.add('range'); } if (selectedIn && !selectedOut) { if (time === selectedIn.getTime()) td.classList.add('start'); if (time < selectedIn.getTime()) { td.classList.add('disabled', 'old'); return td; } } td.onmouseenter = () => { hoverDate = d; refreshHover(); }; td.onmouseleave = () => { hoverDate = null; refreshHover(); };
td.onclick = (e) => { e.stopPropagation(); if (!selectedIn || (selectedIn && selectedOut)) { selectedIn = d; selectedOut = null; } else { if (d <= selectedIn) return; selectedOut = d; } updateCalendarStyles(); updateBar(); if (selectedIn && selectedOut) { setTimeout(() => close(), 100); } }; return td; }
function updateCalendarStyles() { document.querySelectorAll('td[data-date]').forEach(td => { var d = parse(td.dataset.date); var time = d.getTime(); td.classList.remove('start', 'end', 'range'); if (selectedIn && selectedOut) { if (time === selectedIn.getTime()) td.classList.add('start'); else if (time === selectedOut.getTime()) td.classList.add('end'); else if (time > selectedIn.getTime() && time < selectedOut.getTime()) td.classList.add('range'); } if (selectedIn && !selectedOut) { if (time === selectedIn.getTime()) td.classList.add('start'); if (time < selectedIn.getTime()) { td.classList.add('disabled', 'old'); } } }); }
function refreshHover() { if (!selectedIn || selectedOut) return; document.querySelectorAll('td[data-date]').forEach(td => { var d = parse(td.dataset.date); td.classList.remove('range'); if (hoverDate && d > selectedIn && d < hoverDate) td.classList.add('range'); }); }
function updateBar() { document.querySelector('#checkInStr').textContent = selectedIn ? format(selectedIn) : '请选择日期'; document.querySelector('#checkOutStr').textContent = selectedOut ? format(selectedOut) : '请选择日期'; if (selectedIn && selectedOut) { var nights = Math.ceil((selectedOut - selectedIn) / (1000 * 60 * 60 * 24)); document.querySelector('#nightCount').textContent = nights + '晚'; } }
function close() { document.querySelector('#calendarWrap').style.display = 'none'; }
function nextMonth(d) { var n = new Date(d); n.setMonth(n.getMonth() + 1); return n; }
function changeMonth(delta) { var newMon = new Date(currentMon); newMon.setMonth(newMon.getMonth() + delta); var minMon = new Date(today.getFullYear(), today.getMonth(), 1); var maxMon = new Date(today.getFullYear(), today.getMonth() + maxMonth, 1); if (newMon < minMon || newMon > maxMon) return; currentMon = newMon; renderCalendar(); }
document.addEventListener('click', function(e) { if (e.target.id === 'prev') { changeMonth(-1); } else if (e.target.id === 'next') { changeMonth(1); } });</script></body></html>
阅读原文:https://mp.weixin.qq.com/s/fokYJXGYeaJFBRiNcbQ6oQ
该文章在 2026/1/29 10:48:05 编辑过