LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

使用 HTML + JavaScript 实现仿携程双日期选择器(附完整代码)

admin
2026年1月29日 10:48 本文热度 159
在旅游预订网站中,日期选择器是一个核心组件,特别是像携程这样的平台使用的双日期选择器,能够让用户直观地选择入住和退房日期。本文将详细介绍如何使用 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// 最多显示 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">&lt;</span>      <span>${mon.getFullYear()}${mon.getMonth() + 1}月</span>      `;  } else {    hd.innerHTML = `      <span>${mon.getFullYear()}${mon.getMonth() + 1}月</span>      <span class="arrow" id="next">&gt;</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() + 10);  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>      * {          margin0;          padding0;          box-sizing: border-box;      }      body {          background#f5f5f5;          min-height100vh;          padding20px;      }      .container {          max-width800px;          margin0 auto;          background: white;          border-radius15px;          box-shadow0 20px 40px rgba(0,0,0,0.1);          overflow: hidden;      }
      .header {          background#0086f6;          color: white;          padding20px;          text-align: center;      }
      .header h1 {          font-size28px;          font-weight500;      }      .main {          padding20px;          display: flex;          flex-direction: column;          align-items: center;          height500px;      }      .date-bar {          display: flex;          align-items: center;          width350px;          background#fff;          border1px solid #dcdfe6;          border-radius4px;          height44px;          cursor: pointer;          user-select: none;          position: relative;          z-index1000;      }
      .date-bar .item {          flex1;          text-align: center;          position: relative;      }      .date-bar .item:last-child::after {          display: none;      }
      .date-bar .label {          font-size14px;          color#999;          margin-bottom2px;      }
      .date-bar .val {          font-size14px;          color#333;      }      #nightCount {          display: inline-block;          width32px;          height18px;          line-height18px;          text-align: center;          position: absolute;          top50%;          left50%;          margin-top: -9px;          margin-left: -15px;          font-size14px;          color#666;          z-index1;      }      #nightCount::before {          content"";          position: absolute;          height1px;          width11px;          top9px;          left: -13px;          background-color#dadfe6;      }      #nightCount::after {          content"";          position: absolute;          height1px;          width11px;          top9px;          left34px;          background-color#dadfe6;      }      .modal-wrap {          position: absolute;          top100%;          left0;          margin-top5px;          background#fff;          border-radius4px;          box-shadow0 2px 20px rgba(000, .15);          z-index1000;          display: none;      }
      .modal-bd {          display: flex;      }
      .month-box {          width260px;          padding0 15px 15px;      }
      .month-hd {          text-align: center;          height40px;          line-height40px;          font-weight500;          font-size15px;          display: flex;          justify-content: space-between;          align-items: center;      }
      .month-hd .arrow {          font-size20px;          cursor: pointer;          color#666;          padding0 8px;      }
      .month-hd .arrow:hover {          color#0086f6;      }
      table {          width100%;          border-collapse: collapse;      }
      table th {          font-size14px;          color#999;          height30px;      }
      table td {          text-align: center;          height32px;          font-size14px;          cursor: pointer;          position: relative;      }
      table td.oldtable td.new {          color#ccc;          cursor: not-allowed;      }
      table td.today {          color#0086f6;          font-weight700;      }
      table td.starttable 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(0000);
  // 状态变量  var selectedIn = null// 已选入住  var selectedOut = null// 已选离店  var hoverDate = null// 鼠标悬停  var currentMon = new Date(today.getFullYear(), today.getMonth(), 1); // 弹层当前左月  var maxMonth = 12// 最多显示 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">&lt;</span>        <span>${mon.getFullYear()}${mon.getMonth() + 1}月</span>        `;    } else {      hd.innerHTML = `        <span>${mon.getFullYear()}${mon.getMonth() + 1}月</span>        <span class="arrow" id="next">&gt;</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() + 10);    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() {    // 仅当已选入住、未选离店时才有 hover 预览    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);    // 控制范围:最早今天所在月,最多后 maxMonth 个月    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 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved