想要飞的小乌龟
免费

使用 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>

    ← 返回源码站
    源码信息
    • 源码编号NO0000442
    • 下载方式免费
    图片放大