使用 HTML + JavaScript 实现多会议室甘特视图管理系统

文章目录

一、多会议室甘特视图管理系统

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

二、效果演示

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


三、系统分析

1、页面结构

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

1.1 控制面板区域

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

html 复制代码
<div class="controls">
  <input type="date" id="dateInp">
  <button onclick="search()">查询</button>
  <button onclick="openApplyModal()">申请会议</button>
</div>

1.2 图例说明区域

为了让用户更好理解不同颜色代表的含义,系统提供了图例说明。

html 复制代码
<div class="legend">
  <div class="legend-item">
    <div class="legend-color yellow"></div>
    <span>待审批</span>
  </div>
  <!-- 其他状态图例 -->
</div>

1.3 甘特图展示区域

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

html 复制代码
<div class="gantt-container">
  <div id="gantt" class="gantt"></div>
</div>

1.4 弹窗区域

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

html 复制代码
<div id="applyModal" class="modal">...</div>
<div id="meetingModal" class="modal">...</div>

2、核心功能实现

2.1 数据模型设计

系统首先定义了基础数据结构,包括会议室列表、时间片数组和状态映射。

javascript 复制代码
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: '已完成'
};

2.2 甘特图渲染机制

甘特图渲染是系统最核心的功能,通过 renderGantt 函数实现:

  1. 首先创建时间标题行,显示各个时间点
  2. 然后为每个会议室创建一行,显示该会议室在各个时间段的状态
  3. 最后为每个会议条目绑定点击事件,用于显示详细信息
javascript 复制代码
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);
    });
  });
}

2.3 会议申请与冲突检测

系统支持用户提交新的申请,并具备冲突检测功能:

  1. 用户填写会议信息,包括主题、日期、会议室、时间等
  2. 系统检查所选时间段是否与现有会议冲突
  3. 如果没有冲突,则将新会议添加到用户会议列表中
javascript 复制代码
function submitMeeting() {
  // ... 获取用户填写信息
  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();
}

2.4 时间选择联动

为了提高用户体验,系统的会议时间选择具有联动效果。当用户选择开始时间后,结束时间选项会自动更新,只显示晚于开始时间的选项。

javascript 复制代码
function updateEndTimeOptions() {
  var startTimeSelect = document.getElementById('startTimeSelect');
  var endTimeSelect = document.getElementById('endTimeSelect');
  var selectedStartTime = startTimeSelect.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);
  }
}

四、扩展建议

  • 权限管理系统:增加用户角色管理,区分普通用户、管理员等不同权限,允许管理员审批会议申请。
  • 导入导出功能:支持将会议室预订情况导出为Excel或PDF格式,便于统计和汇报。
  • 会议室资源配置:为每个会议室添加容量、设备等详细信息,帮助用户选择合适的会议室。
  • 重复会议功能:支持创建周期性会议,如每周例会等。
  • 人员选择功能:支持直接选择参会人员,方便每个人参看自己要参与的会议。

五、完整代码

git地址:https://gitee.com/ironpro/hjdemo/blob/master/meeting-gantt/index.html

html 复制代码
<!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()">&times;</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()">&times;</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>
相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax