使用 HTML + JavaScript 实现多会议室甘特视图管理系统(附完整代码)


在现代企业办公环境中,会议室资源的有效管理是提升工作效率的重要环节。本文将详细介绍一个基于 HTML、CSS 和 JavaScript 实现的多会议室甘特视图管理系统,帮助用户直观地查看和管理会议室预订情况。

效果演示

该系统通过甘特图形式展示多个会议室在一天内的使用情况,用户可以选择不同日期查看会议室预订状态,并能申请新的会议。系统提供了清晰的时间轴和颜色编码来区分不同状态的会议。用户可以方便地查看会议室占用情况,并通过简单的界面提交新的会议室预订申请。

页面结构

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

控制面板区域

控制面板位于页面顶部,提供日期选择和基本操作按钮。这个区域允许用户选择查看的日期,并提供了申请会议的按钮。


<div class="controls">
 <input type="date" id="dateInp">
 <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>

甘特图展示区域

这是系统的核心展示区域,以表格形式呈现各会议室在不同时段的使用情况。


<div class="gantt-container">
 <div id="gantt" class="gantt"></div>
</div>

弹窗区域

系统包含两个主要弹窗:会议申请弹窗和会议详情弹窗,分别用于创建新会议和查看详情。


<div id="applyModal" class="modal">...</div>
<div id="meetingModal" class="modal">...</div>

完整代码


<!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;
     }
     .controls {
         display: flex;
         gap: 10px;
         margin-bottom: 10px;
         flex-wrap: wrap;
     }

     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;
     }

     .gantt-container {
         background: white;
         border: 1px solid #e0e0e0;
         border-radius: 4px;
         overflow: hidden;
         box-shadow: 0 2px 5px rgba(0,0,0,0.05);
     }

     .gantt {
         width: 100%;
         display: table;
         table-layout: fixed;
     }

     .row {
         display: table-row;
     }

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

     .cell.room {
         width: 11.8%;
         font-weight: bold;
         background: #fafafa;
     }

     .cell-time {
         width: 4.2%;
         height: 38px;
         font-size: 12px;
         color: #666;
         display: table-cell;
         text-align: center;
         vertical-align: middle;
         position: relative;
         left: -2.1%;
     }

     .cell-time:first-child {
         left: 0;
     }

     .meeting {
         height: 10px;
         border-radius: 3px;
         position: absolute;
         left: 0;
         top: 13px;
         z-index: 999;
         cursor: pointer;
     }

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

     .pagination {
         text-align: right;
         margin-top: 10px;
     }

     .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;
     }

     .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;
     }

     .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="controls">
     <input type="date" id="dateInp">
     <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 class="gantt-container">
     <div id="gantt" class="gantt"></div>
   </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': {
     '梅花厅': [
       {start: '09:00', time: 2, status: 1, title: '项目启动会', content: '讨论新项目启动相关事宜', attendeeCount: 15},
       {start: '14:00', time: 3, status: 3, title: '技术评审会', content: '代码和技术方案评审', attendeeCount: 8}
     ],
     '兰亭厅': [
       {start: '10:00', time: 1, status: 4, title: '客户洽谈会', content: '重要客户合作洽谈', attendeeCount: 5},
       {start: '15:00', time: 2, status: 5, title: '培训会', content: '新员工技能培训', attendeeCount: 20}
     ],
     '竹苑厅': [
       {start: '09:30', time: 2, status: 1, title: '部门例会', content: '部门日常工作安排', attendeeCount: 5},
       {start: '14:30', time: 1, status: 3, title: '预算审批会', content: '部门预算审批讨论', attendeeCount: 6}
     ],
     '菊堂厅': [
       {start: '11:00', time: 2, status: 4, title: '合作伙伴会', content: '合作伙伴关系维护', attendeeCount: 10},
       {start: '16:00', time: 1, status: 5, title: '安全培训会', content: '安全知识培训', attendeeCount: 25}
     ]
   },
   // 其他日期数据
 };

 var userMeetings = [];

 var dayjs = (d) => {
   var date = new Date(d);
   return {
     format(fmt) {
       return fmt.replace('YYYY', date.getFullYear())
         .replace('MM', String(date.getMonth() + 1).padStart(2, '0'))
         .replace('DD', String(date.getDate()).padStart(2, '0'));
     }
   };
 };

 // 检查时间重叠
 var isTimeOverlap = (start1, duration1, start2, duration2) => {
   var startIndex1 = timeArr.indexOf(start1);
   var endIndex1 = startIndex1 + duration1;
   var startIndex2 = timeArr.indexOf(start2);
   var endIndex2 = startIndex2 + duration2;
   return (startIndex1 < endIndex2) && (startIndex2 < endIndex1);
 };

 // 获取指定日期的会议数据
 var getMeetingsByDate = (date) => {
   var meetings = [];

   rooms.forEach(room => {
     var dateData = mockDataByDate[date] || {};
     var roomMeetings = dateData[room] || [];

     roomMeetings.forEach(meeting => {
       meetings.push({
         room,
         date,
         start: meeting.start,
         time: meeting.time,
         status: meeting.status,
         isCreator: Math.random() > 0.5 ? 'true' : 'false',
         title: meeting.title,
         content: meeting.content,
         attendeeCount: meeting.attendeeCount
       });
     });
   });

   meetings.push(...userMeetings.filter(m => m.date === date));
   return meetings;
 };

 // 渲染甘特图
 function renderGantt(meetingsData) {
   var box = document.getElementById('gantt');
   box.innerHTML = '';

   // 创建时间标题行
   var hRow = document.createElement('div');
   hRow.className = 'row';
   hRow.innerHTML = '<div class="cell-time"></div>' +
     timeArr.map(t => `<div class="cell-time">${t}</div>`).join('');
   box.appendChild(hRow);

   // 渲染每行会议室
   rooms.forEach(room => {
     var row = document.createElement('div');
     row.className = 'row';

     var html = `<div class="cell room">${room}</div>`;
     var roomMeetings = meetingsData.filter(m => m.room === room)[0]?.map || {};

     timeArr.forEach(time => {
       if (roomMeetings[time]) {
         var [len, color, meeting] = roomMeetings[time];
         html += `<div class="cell">
                     <div class="meeting ${color}"
                          style="width:calc(${len*100}% + ${(len-1)*2}px)"
                          data-meeting='${JSON.stringify(meeting)}'>
                     </div>
                    </div>`;
       } else {
         html += '<div class="cell"></div>';
       }
     });

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

   // 绑定点击事件
   document.querySelectorAll('.meeting').forEach(el => {
     el.addEventListener('click', () => {
       var meetingData = JSON.parse(el.getAttribute('data-meeting'));
       showMeetingDetail(meetingData);
     });
   });
 }

 // 查询功能
 function search() {
   var date = document.getElementById('dateInp').value;
   if (!date) date = dayjs(new Date()).format('YYYY-MM-DD');
   else date = dayjs(date).format('YYYY-MM-DD');

   var list = getMeetingsByDate(date);

   // 按会议室分组
   var groupedData = rooms.map(room => ({
     room,
     name: room,
     map: {}
   }));

   list.forEach(meeting => {
     var color = {1:'yellow', 3:'blue', 4:'pink', 5:'gray'}[meeting.status];
     var roomData = groupedData.find(r => r.room === meeting.room);
     if (roomData) {
       roomData.map[meeting.start] = [meeting.time, color, meeting];
     }
   });

   renderGantt(groupedData);
 }

 // 弹窗相关函数
 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);
   });

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

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

 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 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;
   }
 }

 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 conflictingMeetings = getMeetingsByDate(date).filter(m =>
     m.room === room && m.date === date && isTimeOverlap(startTime, duration, m.start, m.time)
   );

   if (conflictingMeetings.length > 0) {
     alert('该时间段已有会议,请选择其他时间');
     return;
   }

   userMeetings.push({
     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 showMeetingDetail(meeting) {
   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';
 }

 // 页面初始化
 window.onload = function() {
   document.getElementById('dateInp').value = dayjs(new Date()).format('YYYY-MM-DD');
   search();

   window.onclick = function(event) {
     var applyModal = document.getElementById('applyModal');
     var meetingModal = document.getElementById('meetingModal');

     if (event.target === applyModal) closeApplyModal();
     if (event.target === meetingModal) closeMeetingModal();
   };
 };
</script>

</body>
</html>

0 条评论

当前评论已经关闭


登录用户头像
  • 从业日期: 2014/03/20
  • 性别:
口头禅

每天搬一点,幸福多一点

62

发帖数

98

源码数

0

接单

2

获赞

13

获评

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