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

文章目录

一、单会议室周日历管理系统

本文将介绍一个基于HTML、CSS 和 JavaScript 实现的单会议室周日历视图系统。

二、效果演示

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

三、系统分析

1、页面结构

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

1.1 控制面板区域

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

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

1.2 日历展示区域

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

html 复制代码
<div id="calendar" class="calendar"></div>

2、核心功能实现

2.1 日历渲染

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

  1. 创建表头行,显示星期和日期信息
  2. 为每个时间槽创建一行,显示该时段在每天的会议情况
  3. 根据会议持续时间合并单元格,正确显示跨时段的会议
javascript 复制代码
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);
  }
}

四、完整代码

git地址:https://gitee.com/ironpro/hjdemo/blob/master/meeting-week/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;
      }

      .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()">&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': {
      '梅花厅': [
        {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>
相关推荐
天天向上10242 小时前
el-table 解决一渲染数据页面就卡死
前端·javascript·vue.js
weixin_431600442 小时前
开发中遇到需要对组件库组件结构调整的两种落地方案实践
前端·组件库
Code知行合壹2 小时前
Vue3入门
前端·javascript·vue.js
LawrenceLan2 小时前
17.Flutter 零基础入门(十七):StatelessWidget 与 State 的第一次分离
开发语言·前端·flutter·dart
酷酷的鱼2 小时前
Expo Router vs 原生React Native 完全对比指南
javascript·react native·react.js
桃子叔叔2 小时前
react-wavesurfer录音组件2:前端如何处理后端返回的仅Blob字段
前端·react.js·状态模式
nie_xl2 小时前
VS/TRAE中设置本地maven地址的方法
运维·服务器·前端
烧饼Fighting2 小时前
统信UOS操作系统离线安装ffmpeg
开发语言·javascript·ffmpeg