使用 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
- 性别: 男
每天搬一点,幸福多一点
发帖数
源码数
接单
获赞
获评
- 积分优惠充值通道: 点我传送
- 源码编号: NO0000442
- 下载方式: 免费
- 源码类型: 静态页面源码
{{commentItem.nickName}}
{{formatIntervalTime(commentItem.createTime)}}{{childComment.nickName}} {{childComment.replyNickName}}
{{formatIntervalTime(childComment.createTime)}}