使用 HTML + JavaScript 实现单会议室周日历管理系统


效果演示

该系统通过日历视图的形式展示单个会议室在一周内的使用情况。用户可以选择特定会议室并查看其在本周的预订状态,还能申请新的会议,删除已有会议。系统采用颜色编码来区分不同的会议状态,使用户能够快速识别会议室的可用性。

页面结构

系统主要包括以下几个功能区域:

控制面板区域

控制面板位于页面顶部,提供会议室选择和基本操作按钮。用户可以通过下拉菜单选择要查看的会议室。


<div class="form">
 <select id="roomSel">
   <option value="">选择会议室</option>
 </select>
 <button onclick="search()">查询</button>
 <button onclick="openApplyModal()">申请会议</button>
</div>

历展示区域

这是系统的核心展示区域,以表格形式呈现选定会议室在一周内每天的使用情况。

  •  
<div id="calendar" class="calendar"></div>

核心功能实现

日历渲染

日历渲染是系统最核心的功能,通过 renderCal 函数实现。该函数负责将会议数据转换为可视化的日历表格:

创建表头行,显示星期和日期信息

为每个时间槽创建一行,显示该时段在每天的会议情况

根据会议持续时间合并单元格,正确显示跨时段的会议


function renderCal(map, days) {
 var box = document.getElementById('calendar');
 box.innerHTML = '';

 var hRow = document.createElement('div');
 hRow.className = 'row';
 // 使用新的日期格式显示
 hRow.innerHTML = '<div class="cell cell-time">时间</div>' +
   days.map(d => `<div class="cell cell-time">${d.day}</div>`).join('');
 box.appendChild(hRow);

 for(var i = 0; i < timeArr.length - 1; i++) {
   var row = document.createElement('div');
   row.className = 'row';

   var html = `<div class="cell">${timeArr[i]} ~ ${timeArr[i+1]}</div>`;

   days.forEach(d => {
     var key = `${d.date}#${timeArr[i]}`;
     if(map[key]) {
       var m = map[key];
       var colorClass = {1:'yellow', 3:'blue', 4:'pink', 5:'gray'}[m.status];

       // 计算会议跨越的单元格数量
       var startIndex = timeArr.indexOf(m.start);
       var span = m.time;

       // 只在会议开始时间渲染会议元素
       if (startIndex === i) {
         // 计算宽度:跨越的单元格数 * 100% + 边框宽度
         var height = `calc(${span * 100}% + ${(span - 1) * 2}px)`;

         html += `<div class="cell">
             <div class="meeting-container">
               <div class="meeting ${colorClass}"
                    style="width:100%;height: ${height}"
                    onclick="showMeetingDetail(event, '${m.id}', '${m.date}')">
                 ${m.title}
                 ${m.isCreator === 'true' && [1, 3].includes(m.status) ?
           `<button class="close-btn" onclick="delMeet(event,'${m.id}', '${m.date}', '${m.room}')">×</button>` : ''}
               </div>
             </div>
           </div>`;
       } else if (i > startIndex && i < startIndex + span) {
         // 对于被会议跨越的中间单元格,保持空单元格
         html += '<div class="cell"></div>';
       } else {
         html += '<div class="cell"></div>';
       }
     } else {
       html += '<div class="cell"></div>';
     }
   });

   row.innerHTML = html;
   box.appendChild(row);
 }
}

完整代码


<!DOCTYPE html>
<html lang="zh-CN">
<head>
 <meta charset="UTF-8">
 <title>单会议室周日历</title>
 <style>
     * {
         margin: 0;
         padding: 0;
         box-sizing: border-box;
     }
     body {
         background-color: #f5f5f5;
         min-height: 100vh;
         padding: 20px;
     }
     .container {
         max-width: 1500px;
         margin: 0 auto;
         background: white;
         border-radius: 15px;
         box-shadow: 0 20px 40px rgba(0,0,0,0.1);
         overflow: hidden;
     }
     .header {
         background: #4a5568;
         color: white;
         padding: 20px;
         text-align: center;
     }

     .header h1 {
         font-size: 24px;
         font-weight: 500;
     }
     .main {
         padding: 20px;
     }

     .form {
         display: flex;
         gap: 10px;
         margin-bottom: 20px;
         flex-wrap: wrap;
         align-items: center;
     }

     select, button, input, textarea {
         padding: 8px 12px;
         border: 1px solid #e0e0e0;
         border-radius: 4px;
     }

     button {
         background: #409EFF;
         color: white;
         border: none;
         cursor: pointer;
         transition: background 0.3s;
     }

     button:hover {
         opacity: 0.9;
     }

     .calendar {
         width: 100%;
         border: 1px solid #e0e0e0;
         background: #fff;
         border-radius: 4px;
         overflow: hidden;
         box-shadow: 0 2px 5px rgba(0,0,0,0.05);
     }

     .row {
         display: table-row;
     }

     .cell {
         display: table-cell;
         border: 1px solid #e0e0e0;
         text-align: center;
         vertical-align: middle;
         width: 14.28%;
         min-width: 120px;
         line-height: 36px;
         height: 36px;
         position: relative;
     }

     .cell-time {
         width: 14.28%;
         min-width: 120px;
         color: #2691FF;
         font-weight: bold;
         background: #fafafa;
     }

     .meeting-container {
         position: relative;
         width: 100%;
         height: 100%;
     }

     .meeting {
         position: absolute;
         top: 0;
         left: 0;
         height: 36px;
         line-height: 36px;
         background: #B160DA;
         color: #fff;
         white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
         padding: 0 25px 0 10px;
         cursor: pointer;
         z-index: 10;
     }

     .yellow { background: #FFCE1A; }
     .blue { background: #409EFF; }
     .pink { background: #DE1794; }
     .gray { background: #777; }

     .close-btn {
         position: absolute;
         right: 7px;
         top: 10px;
         transform: translateY(-50%);
         width: 16px;
         height: 16px;
         cursor: pointer;
         background: transparent;
         border: none;
         color: white;
         font-weight: bold;
         font-size: 16px;
         line-height: 1;
         padding: 0;
     }

     .modal {
         display: none;
         position: fixed;
         z-index: 1000;
         left: 0;
         top: 0;
         width: 100%;
         height: 100%;
         background-color: rgba(0,0,0,0.5);
     }

     .modal-content {
         background-color: #fff;
         margin: 10% auto;
         padding: 20px;
         border: none;
         width: 90%;
         max-width: 500px;
         border-radius: 8px;
         box-shadow: 0 4px 15px rgba(0,0,0,0.2);
     }

     .close {
         color: #aaa;
         float: right;
         font-size: 28px;
         font-weight: bold;
         cursor: pointer;
         line-height: 1;
     }

     .close:hover {
         color: #000;
     }

     .meeting-detail div {
         margin-bottom: 12px;
     }

     .meeting-detail label {
         font-weight: bold;
         margin-right: 10px;
         display: inline-block;
         width: 80px;
     }

     .legend {
         margin: 15px 0;
         display: flex;
         align-items: center;
         gap: 20px;
         flex-wrap: wrap;
     }

     .legend-item {
         display: flex;
         align-items: center;
         gap: 5px;
     }

     .legend-color {
         width: 20px;
         height: 10px;
         border-radius: 3px;
     }

     .apply-form {
         margin: 20px 0;
         padding: 15px;
         border: 1px solid #e0e0e0;
         background: #fff;
         border-radius: 4px;
     }

     .apply-form input,
     .apply-form textarea,
     .apply-form select {
         margin-right: 10px;
         margin-bottom: 10px;
     }

     .time-selection {
         display: flex;
         align-items: center;
         gap: 10px;
         margin: 15px 0;
     }

     .time-selection select {
         padding: 5px;
     }
 </style>
</head>
<body>
<div class="container">
 <div class="header">
   <h1>单会议室周日历视图</h1>
 </div>

 <div class="main">
   <div class="form">
     <select id="roomSel">
       <option value="">选择会议室</option>
     </select>
     <button onclick="search()">查询</button>
     <button onclick="openApplyModal()">申请会议</button>
   </div>

   <div class="legend">
     <div class="legend-item">
       <div class="legend-color yellow"></div>
       <span>待审批</span>
     </div>
     <div class="legend-item">
       <div class="legend-color blue"></div>
       <span>已批准</span>
     </div>
     <div class="legend-item">
       <div class="legend-color pink"></div>
       <span>进行中</span>
     </div>
     <div class="legend-item">
       <div class="legend-color gray"></div>
       <span>已完成</span>
     </div>
   </div>

   <div id="calendar" class="calendar"></div>
 </div>
</div>

<!-- 会议申请弹窗 -->
<div id="applyModal" class="modal">
 <div class="modal-content">
   <span class="close" onclick="closeApplyModal()">×</span>
   <h3>会议申请</h3>
   <div class="meeting-detail">
     <div>
       <label>会议主题:</label>
       <input type="text" id="modalMeetingTitle" placeholder="请输入会议主题">
     </div>
     <div>
       <label>会议日期:</label>
       <input type="date" id="modalApplyDate">
     </div>
     <div>
       <label>会议室:</label>
       <select id="modalApplyRoom">
         <option value="">选择会议室</option>
       </select>
     </div>
     <div class="time-selection">
       <label>会议时间:</label>
       <select id="startTimeSelect"></select>
       <span>到</span>
       <select id="endTimeSelect"></select>
     </div>
     <div>
       <label>参会人数:</label>
       <input type="number" id="modalAttendeeCount" min="1" placeholder="请输入人数">
     </div>
     <div>
       <label>会议内容:</label>
       <textarea id="modalMeetingContent" placeholder="请输入会议内容"></textarea>
     </div>
     <div style="text-align: right; margin-top: 15px;">
       <button onclick="closeApplyModal()" style="background:#999">取消</button>
       <button onclick="submitMeeting()">提交申请</button>
     </div>
   </div>
 </div>
</div>

<!-- 会议详情弹窗 -->
<div id="meetingModal" class="modal">
 <div class="modal-content">
   <span class="close" onclick="closeMeetingModal()">×</span>
   <h3>会议详情</h3>
   <div class="meeting-detail" id="meetingDetailContent"></div>
 </div>
</div>

<script>
 var rooms = ['梅花厅','兰亭厅','竹苑厅','菊堂厅'];
 var timeArr = ['08:30','09:00','09:30','10:00','10:30','11:00','11:30','12:00','12:30','13:00','13:30','14:00','14:30','15:00','15:30','16:00','16:30','17:00','17:30','18:00','18:30'];
 var statusMap = {
   1: '待审批',
   3: '已批准',
   4: '进行中',
   5: '已完成'
 };

 // 固定的模拟数据
 var mockDataByDate = {
   '2025-11-25': {
     '梅花厅': [
       {id: 'm1', start: '09:00', time: 2, status: 1, isCreator: 'true', title: '项目启动会', content: '讨论新项目启动相关事宜', attendeeCount: 15},
       {id: 'm2', start: '14:00', time: 3, status: 3, isCreator: 'false', title: '技术评审会', content: '代码和技术方案评审', attendeeCount: 8}
     ],
     // ... 其他数据
   },
   // ... 其他数据
 };

 // 用户申请的会议数据
 var userMeetings = [];

 var dayjs = (d) => {
   var date = new Date(d);
   return {
     format(f) {
       return f.replace('YYYY', date.getFullYear())
         .replace('MM', String(date.getMonth() + 1).padStart(2, '0'))
         .replace('DD', String(date.getDate()).padStart(2, '0'));
     },
     add(n, u) {
       if(u === 'day') date.setDate(date.getDate() + n);
       return dayjs(date);
     }
   };
 }

 function getWeekMeetings(room, startDate) {
   var meetings = [];
   var monday = new Date(startDate);
   monday.setDate(monday.getDate() - monday.getDay() + 1);

   for(var i = 0; i < 7; i++) {
     var date = dayjs(monday).add(i, 'day').format('YYYY-MM-DD');
     if(mockDataByDate[date] && mockDataByDate[date][room]) {
       mockDataByDate[date][room].forEach(meeting => {
         meetings.push({
           ...meeting,
           date: date,
           room: room
         });
       });
     }

     // 添加用户申请的会议
     userMeetings.forEach(meeting => {
       if(meeting.date === date && meeting.room === room) {
         meetings.push(meeting);
       }
     });
   }
   return meetings;
 }

 function initTimeSelectors() {
   var startTimeSelect = document.getElementById('startTimeSelect');
   var endTimeSelect = document.getElementById('endTimeSelect');

   startTimeSelect.innerHTML = '';
   endTimeSelect.innerHTML = '';

   timeArr.slice(0, -1).forEach(time => {
     var option = document.createElement('option');
     option.value = time;
     option.textContent = time;
     startTimeSelect.appendChild(option);
   });

   timeArr.slice(1).forEach(time => {
     var option = document.createElement('option');
     option.value = time;
     option.textContent = time;
     endTimeSelect.appendChild(option);
   });

   startTimeSelect.selectedIndex = 0;
   endTimeSelect.selectedIndex = 0;

   startTimeSelect.onchange = updateEndTimeOptions;
 }

 function renderCal(map, days) {
   var box = document.getElementById('calendar');
   box.innerHTML = '';

   var hRow = document.createElement('div');
   hRow.className = 'row';
   // 使用新的日期格式显示
   hRow.innerHTML = '<div class="cell cell-time">时间</div>' +
     days.map(d => `<div class="cell cell-time">${d.day}</div>`).join('');
   box.appendChild(hRow);

   for(var i = 0; i < timeArr.length - 1; i++) {
     var row = document.createElement('div');
     row.className = 'row';

     var html = `<div class="cell">${timeArr[i]} ~ ${timeArr[i+1]}</div>`;

     days.forEach(d => {
       var key = `${d.date}#${timeArr[i]}`;
       if(map[key]) {
         var m = map[key];
         var colorClass = {1:'yellow', 3:'blue', 4:'pink', 5:'gray'}[m.status];

         // 计算会议跨越的单元格数量
         var startIndex = timeArr.indexOf(m.start);
         var span = m.time;

         // 只在会议开始时间渲染会议元素
         if (startIndex === i) {
           // 计算宽度:跨越的单元格数 * 100% + 边框宽度
           var height = `calc(${span * 100}% + ${(span - 1) * 2}px)`;

           html += `<div class="cell">
               <div class="meeting-container">
                 <div class="meeting ${colorClass}"
                      style="width:100%;height: ${height}"
                      onclick="showMeetingDetail(event, '${m.id}', '${m.date}')">
                   ${m.title}
                   ${m.isCreator === 'true' && [1, 3].includes(m.status) ?
             `<button class="close-btn" onclick="delMeet(event,'${m.id}', '${m.date}', '${m.room}')">×</button>` : ''}
                 </div>
               </div>
             </div>`;
         } else if (i > startIndex && i < startIndex + span) {
           // 对于被会议跨越的中间单元格,保持空单元格
           html += '<div class="cell"></div>';
         } else {
           html += '<div class="cell"></div>';
         }
       } else {
         html += '<div class="cell"></div>';
       }
     });

     row.innerHTML = html;
     box.appendChild(row);
   }
 }

 function search() {
   var room = document.getElementById('roomSel').value;
   if(!room) {
     alert('请选择会议室');
     return;
   }

   var currentDate = new Date();

   var days = [];
   var monday = new Date(currentDate);
   monday.setDate(monday.getDate() - monday.getDay() + 1);

   for(var i = 0; i < 7; i++) {
     var d = dayjs(monday).add(i, 'day').format('YYYY-MM-DD');
     var dateObj = new Date(d);
     // 修改日期显示格式为 "月/日(周几)"
     var monthDay = `${dateObj.getMonth() + 1}/${dateObj.getDate()}`;
     var weekday = '周' +'日一二三四五六'[dateObj.getDay()];
     days.push({
       date: d,
       day: `${monthDay}(${weekday})`
     });
   }

   var allMeetings = getWeekMeetings(room, currentDate);

   var map = {};
   allMeetings.forEach(m => {
     map[`${m.date}#${m.start}`] = m;
   });

   renderCal(map, days);
 }

 function openApplyModal() {
   var modal = document.getElementById('applyModal');
   document.getElementById('modalApplyDate').value = dayjs(new Date()).format('YYYY-MM-DD');

   var roomSelect = document.getElementById('modalApplyRoom');
   roomSelect.innerHTML = '<option value="">选择会议室</option>';
   rooms.forEach(room => {
     var option = document.createElement('option');
     option.value = room;
     option.textContent = room;
     roomSelect.appendChild(option);
   });

   // 设置当前选择的会议室为默认值
   roomSelect.value = document.getElementById('roomSel').value;

   initTimeSelectors();
   modal.style.display = 'block';
 }

 function closeApplyModal() {
   document.getElementById('applyModal').style.display = 'none';
 }

 function submitMeeting() {
   var title = document.getElementById('modalMeetingTitle').value;
   var content = document.getElementById('modalMeetingContent').value;
   var date = document.getElementById('modalApplyDate').value;
   var room = document.getElementById('modalApplyRoom').value;
   var attendeeCount = document.getElementById('modalAttendeeCount').value;
   var startTime = document.getElementById('startTimeSelect').value;
   var endTime = document.getElementById('endTimeSelect').value;

   if (!title || !date || !room || !attendeeCount || !startTime || !endTime) {
     alert('请填写完整信息');
     return;
   }

   var startIndex = timeArr.indexOf(startTime);
   var endIndex = timeArr.indexOf(endTime);

   if (startIndex >= endIndex) {
     alert('结束时间必须晚于开始时间');
     return;
   }

   var duration = endIndex - startIndex;

   // 获取指定日期和会议室的所有会议
   var currentDateMeetings = [];

   // 添加固定模拟数据
   if (mockDataByDate[date] && mockDataByDate[date][room]) {
     mockDataByDate[date][room].forEach(meeting => {
       currentDateMeetings.push({
         ...meeting,
         date: date,
         room: room
       });
     });
   }

   // 添加用户申请的数据
   userMeetings.forEach(meeting => {
     if (meeting.date === date && meeting.room === room) {
       currentDateMeetings.push(meeting);
     }
   });

   // 检查时间冲突
   var hasConflict = false;
   for (var existingMeeting of currentDateMeetings) {
     var existingStartIndex = timeArr.indexOf(existingMeeting.start);
     var existingEndIndex = existingStartIndex + existingMeeting.time;

     // 检查时间区间是否有重叠
     if (!(endIndex <= existingStartIndex || startIndex >= existingEndIndex)) {
       hasConflict = true;
       break;
     }
   }
   if (hasConflict) {
     alert('该时间段已有会议,请选择其他时间');
     return;
   }

   // 生成唯一ID
   var id = 'user_' + Date.now();

   userMeetings.push({
     id: id,
     room,
     date,
     start: startTime,
     time: duration,
     status: 1, // 待审批
     isCreator: 'true',
     title,
     content,
     attendeeCount: parseInt(attendeeCount)
   });

   alert('会议申请已提交');
   closeApplyModal();

   // 清空表单
   ['modalMeetingTitle', 'modalMeetingContent', 'modalAttendeeCount'].forEach(id => {
     document.getElementById(id).value = '';
   });

   search();
 }

 function delMeet(ev, id, date, room) {
   ev.stopPropagation();
   if(!confirm('确定删除该会议吗?')) return;

   // 从用户数据中删除会议
   var idx = userMeetings.findIndex(m => m.id === id && m.date === date && m.room === room);
   if(idx > -1) {
     userMeetings.splice(idx, 1);
     alert('会议已删除');
     search();
     return;
   }

   // 从模拟数据中删除会议
   if(mockDataByDate[date] && mockDataByDate[date][room]) {
     var idx = mockDataByDate[date][room].findIndex(m => m.id === id);
     if(idx > -1) {
       mockDataByDate[date][room].splice(idx, 1);
       alert('会议已删除');
       search();
     }
   }
 }

 function showMeetingDetail(event, id, date) {
   event.stopPropagation();

   // 查找会议详情
   var meeting = null;
   if(mockDataByDate[date]) {
     for(var room in mockDataByDate[date]) {
       var found = mockDataByDate[date][room].find(m => m.id === id);
       if(found) {
         meeting = {...found, date, room};
         break;
       }
     }
   }

   // 查找用户会议
   if(!meeting) {
     meeting = userMeetings.find(m => m.id === id && m.date === date);
     if(meeting) {
       meeting = {...meeting}; // 创建副本避免修改原数据
     }
   }

   if (!meeting) return;

   var modal = document.getElementById('meetingModal');
   var detailContent = document.getElementById('meetingDetailContent');

   var startTimeIndex = timeArr.indexOf(meeting.start);
   var endTimeIndex = startTimeIndex + meeting.time;
   var endTime = endTimeIndex < timeArr.length ? timeArr[endTimeIndex] : '结束';
   var statusText = statusMap[meeting.status] || '未知';

   detailContent.innerHTML = `
       <div><label>主题:</label> ${meeting.title}</div>
       <div><label>日期:</label> ${meeting.date}</div>
       <div><label>地点:</label> ${meeting.room}</div>
       <div><label>时间:</label> ${meeting.start} - ${endTime}</div>
       <div><label>状态:</label> ${statusText}</div>
       <div><label>参会人数:</label> ${meeting.attendeeCount || 'N/A'}</div>
       <div><label>会议内容:</label> ${meeting.content || '无'}</div>
     `;

   modal.style.display = 'block';
 }

 function closeMeetingModal() {
   document.getElementById('meetingModal').style.display = 'none';
 }

 function updateEndTimeOptions() {
   var startTimeSelect = document.getElementById('startTimeSelect');
   var endTimeSelect = document.getElementById('endTimeSelect');
   var selectedStartTime = startTimeSelect.value;
   var currentEndTime = endTimeSelect.value;

   endTimeSelect.innerHTML = '';

   var startIndex = timeArr.indexOf(selectedStartTime);
   for (var i = startIndex + 1; i < timeArr.length; i++) {
     var option = document.createElement('option');
     option.value = timeArr[i];
     option.textContent = timeArr[i];
     endTimeSelect.appendChild(option);
   }

   if (timeArr.indexOf(currentEndTime) > startIndex) {
     endTimeSelect.value = currentEndTime;
   } else {
     endTimeSelect.selectedIndex = 0;
   }
 }

 window.onload = function() {
   var sel = document.getElementById('roomSel');
   rooms.forEach(r => {
     var option = document.createElement('option');
     option.value = r;
     option.textContent = r;
     sel.appendChild(option);
   });

   document.getElementById('roomSel').value = rooms[0];
   search();

   // 点击模态框外部关闭
   window.onclick = function(event) {
     var meetingModal = document.getElementById('meetingModal');
     var applyModal = document.getElementById('applyModal');
     if (event.target === meetingModal) closeMeetingModal();
     if (event.target === applyModal) closeApplyModal();
   };
 };
</script>
</body>
</html>

0 条评论

当前评论已经关闭


登录用户头像
想要飞的小乌龟

Java软件开发

  • 从业日期: 2022/08/04
  • 性别:
口头禅

每天搬一点,幸福多一点

41

发帖数

60

源码数

0

接单

8

获赞

23

获评

源码信息
  • 积分优惠充值通道: 点我传送
  • 源码编号: NO0000442
  • 下载方式: 免费
  • 源码类型: 静态页面源码