在现代企业管理中,考勤系统是不可或缺的一部分。它不仅帮助公司管理员工的工作时间,还能提高管理效率和员工自律性。本文将介绍一个基于 HTML、CSS 和 JavaScript 实现的简单考勤系统。
效果演示
本系统通过日历形式展示每月的考勤情况,用户可以切换月份查看不同时间段的数据。每个日期单元格显示当天的出勤状态(正常出勤、缺勤、迟到、请假等),以及上下班时间、原因说明等信息。此外,系统还提供了统计面板,用于展示当月的考勤汇总数据。
页面结构
月份选择器
这是一个包含月份选择器的区域,允许用户通过点击按钮来切换查看不同月份的考勤数据。中间显示当前所选月份的年月信息。
<div class="header">
<div class="month-selector">
<button id="prev-month">< 上月</button>
<div class="month-year" id="current-month"></div>
<button id="next-month">下月 ></button>
</div>
</div>
统计面板
展示当月的考勤统计数据。分别展示“正常出勤”、“异常考勤”、“迟到/早退”和“请假天数”的统计值。数值初始为 0,后续通过 JavaScript 动态更新。
<div class="stats">
<div class="stat-box">
<div class="stat-value" id="present-days">0</div>
<div class="stat-label">正常出勤</div>
</div>
<div class="stat-box">
<div class="stat-value" id="abnormal-days">0</div>
<div class="stat-label">异常考勤</div>
</div>
<div class="stat-box">
<div class="stat-value" id="late-days">0</div>
<div class="stat-label">迟到/早退</div>
</div>
<div class="stat-box">
<div class="stat-value" id="leave-days">0</div>
<div class="stat-label">请假天数</div>
</div>
</div>
日历表格
一个日历表格结构,以周为单位展示每天的考勤信息(从周日到周六)。有一个占位区域,后续通过 JavaScript 动态生成每一天的考勤数据和状态。
<table class="calendar" id="calendar">
<thead>
<tr>
<th>周日</th>
<th>周一</th>
<th>周二</th>
<th>周三</th>
<th>周四</th>
<th>周五</th>
<th>周六</th>
</tr>
</thead>
<tbody id="calendar-body">
<!-- 日历内容将通过JavaScript动态生成 -->
</tbody>
</table>
核心功能实现
定义示例考勤数据
定义一个包含多个月份的考勤数据对象 attendanceData,用于存储用户在不同日期的出勤记录。后期可以直接对接服务端数据。
数据按月份划分(如 “2025-04”、“2025-05” 等),每个月份下包含具体的日期和对应的考勤信息。每个日期记录包含状态(如正常出勤、迟到、请假等)、上下班时间以及可能的原因说明。
const attendanceData = {
// ...
"2025-06": {
"2025-06-02": {
status: "present",
clockIn: "08:50",
clockOut: "18:05"
},
"2025-06-03": {
status: "present",
clockIn: "08:52",
clockOut: "18:10"
},
"2025-06-04": {
status: "present",
clockIn: "08:49",
clockOut: "18:08"
},
// ...
}
};
初始化日历
初始化日历的默认显示,并为“上月”和“下月”按钮绑定点击事件,实现月份切换功能。
// 默认显示当前月份
const today = new Date();
let currentDate = new Date(today.getFullYear(), today.getMonth(), 1);
// 初始化日历
function initCalendar() {
renderCalendar();
// 添加上月/下月按钮事件
document.getElementById('prev-month').addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
});
document.getElementById('next-month').addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
});
}
渲染日历并更新统计信息
根据 currentDate 获取当前年份、月份,并计算当月的第一天、最后一天以及总天数。清空原有的日历内容,并基于当月的日期结构重新生成日历表格。每个单元格代表一天,区分“上个月”、“本月”和“下个月”的日期,并标记周末。结合 attendanceData 数据为当天添加考勤状态(如正常出勤、迟到等)、上下班时间及原因说明。对不同类型的考勤状态进行统计,更新页面中的统计面板数据(如正常出勤天数、请假天数等)。
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const today = new Date();
// 更新显示的月份年份
document.getElementById('current-month').textContent = `${year}年${month + 1}月`;
// 获取当月第一天和最后一天
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// 获取当月天数
const daysInMonth = lastDay.getDate();
// 获取当月第一天是星期几 (0-6, 0是周日)
const firstDayOfWeek = firstDay.getDay();
// 清空日历
const calendarBody = document.getElementById('calendar-body');
calendarBody.innerHTML = '';
// 统计考勤数据
let presentCount = 0;
let abnormalCount = 0;
let lateCount = 0;
let leaveCount = 0;
// 获取当前月份的考勤数据
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
const monthAttendance = attendanceData[monthKey] || {};
// 创建日历行
let date = 1;
for (let i = 0; i < 6; i++) {
// 如果已经超过当月天数,并且已经显示完所有日期,就停止
if (date > daysInMonth && i > 0) break;
const row = document.createElement('tr');
// 创建日历单元格
for (let j = 0; j < 7; j++) {
const cell = document.createElement('td');
// 标记周末
if (j === 0 || j === 6) {
cell.classList.add('weekend');
}
if (i === 0 && j < firstDayOfWeek) {
// 上个月的日期
const prevMonthLastDay = new Date(year, month, 0).getDate();
const prevDate = prevMonthLastDay - (firstDayOfWeek - j - 1);
cell.innerHTML = `<div class="day-number">${prevDate}</div>`;
cell.classList.add('other-month');
} else if (date > daysInMonth) {
// 下个月的日期
const nextDate = date - daysInMonth;
cell.innerHTML = `<div class="day-number">${nextDate}</div>`;
cell.classList.add('other-month');
date++;
} else {
// 当月的日期
const currentDateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`;
const dayOfWeek = new Date(year, month, date).getDay();
cell.innerHTML = `<div class="day-number">${date}</div>`;
// 如果是今天,添加特殊样式
if (year === today.getFullYear() &&
month === today.getMonth() &&
date === today.getDate()) {
cell.classList.add('today');
}
// 添加考勤状态
const record = monthAttendance[currentDateStr];
if (record) {
let statusText = '';
let statusClass = '';
switch (record.status) {
case 'present':
statusText = '正常出勤';
statusClass = 'present';
presentCount++;
break;
case 'absent':
statusText = '缺勤';
statusClass = 'absent';
abnormalCount++;
break;
// 迟到(late)、请假(leave)、早退(early-leave)、异常(abnormal)
// ...
}
cell.innerHTML += `<span class="attendance-status ${statusClass}">${statusText}</span>
<div class="attendance-info work-time">上班: ${record.clockIn || '-'}</div>
<div class="attendance-info work-time">下班: ${record.clockOut || '-'}</div>
${record.reason ? `<div class="reason">原因: ${record.reason}</div>` : ''}`;
}
date++;
}
row.appendChild(cell);
}
calendarBody.appendChild(row);
}
// 更新统计信息
document.getElementById('present-days').textContent = presentCount;
document.getElementById('abnormal-days').textContent = abnormalCount;
document.getElementById('late-days').textContent = lateCount;
document.getElementById('leave-days').textContent = leaveCount;
}
扩展建议
- 支持导出考勤数据:提供“导出”按钮,将当前月的考勤数据导出为 Excel 或 PDF 文件。
- 增加节假日标识:自动识别法定节假日,并在日历中高亮显示。
- 增加图表可视化统计:添加柱状图或饼图,展示各类考勤的比例。
- 支持打印功能:点击“打印”按钮时,仅打印当前月的考勤日历和统计信息。
- 增加搜索/跳转日期功能:提供日期输入框,输入特定日期后自动跳转到该月份。
- 对接服务端API:
完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的考勤</title>
<style>
body {
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2.5rem;
}
.header {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.month-selector {
display: flex;
align-items: center;
}
.month-selector button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin: 0 10px;
transition: background-color 0.3s;
}
.month-selector button:hover {
background-color: #45a049;
}
.month-year {
font-size: 1.5rem;
font-weight: bold;
color: #4CAF50;
}
.calendar {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
margin-top: 20px;
}
.calendar th {
background-color: #4CAF50;
color: white;
padding: 15px;
text-align: center;
font-size: 1.1rem;
}
.calendar td {
border: 1px solid #ddd;
height: 140px;
vertical-align: top;
padding: 10px;
position: relative;
width: 14.28%;
transition: background-color 0.3s;
}
.calendar td:hover {
background-color: #f0f0f0;
}
.day-number {
font-weight: bold;
margin-bottom: 10px;
font-size: 1.2rem;
}
.other-month {
background-color: #f9f9f9;
color: #aaa;
}
.attendance-info {
font-size: 0.9rem;
margin: 4px 0;
}
.work-time {
color: #333;
}
.attendance-status {
padding: 3px 8px;
border-radius: 4px;
margin: 4px 0;
display: block;
font-size: 0.9rem;
}
.present {
background-color: #dff0d8;
color: #3c763d;
}
.absent {
background-color: #f2dede;
color: #a94442;
}
.late {
background-color: #fcf8e3;
color: #8a6d3b;
}
.leave {
background-color: #d9edf7;
color: #31708f;
}
.early-leave {
background-color: #f8d7da;
color: #721c24;
}
.abnormal {
background-color: #e2e3e5;
color: #383d41;
}
.reason {
font-size: 0.8rem;
color: #d63384;
margin-top: 4px;
font-style: italic;
}
.stats {
margin-top: 30px;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
.stat-box {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
width: 22%;
text-align: center;
margin-bottom: 15px;
transition: transform 0.3s;
box-sizing: border-box;
}
.stat-box:hover {
transform: translateY(-5px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}
.stat-value {
font-size: 2.2rem;
font-weight: bold;
color: #4CAF50;
margin: 10px 0;
}
.stat-label {
color: #666;
font-size: 1.1rem;
}
.weekend {
background-color: #f8f9fa;
}
.today {
background-color: #e7f5fe;
border: 2px solid #4CAF50;
}
@media (max-width: 768px) {
.stat-box {
width: 45%;
}
.calendar td {
height: 120px;
}
}
@media (max-width: 480px) {
.stat-box {
width: 100%;
}
.calendar td {
height: 100px;
font-size: 0.9rem;
}
.attendance-info, .reason {
font-size: 0.8rem;
}
}
</style>
</head>
<body>
<div class="container">
<h1>我的考勤</h1>
<div class="header">
<div class="month-selector">
<button id="prev-month">< 上月</button>
<div class="month-year" id="current-month"></div>
<button id="next-month">下月 ></button>
</div>
</div>
<div class="stats">
<div class="stat-box">
<div class="stat-value" id="present-days">0</div>
<div class="stat-label">正常出勤</div>
</div>
<div class="stat-box">
<div class="stat-value" id="abnormal-days">0</div>
<div class="stat-label">异常考勤</div>
</div>
<div class="stat-box">
<div class="stat-value" id="late-days">0</div>
<div class="stat-label">迟到/早退</div>
</div>
<div class="stat-box">
<div class="stat-value" id="leave-days">0</div>
<div class="stat-label">请假天数</div>
</div>
</div>
<table class="calendar" id="calendar">
<thead>
<tr>
<th>周日</th>
<th>周一</th>
<th>周二</th>
<th>周三</th>
<th>周四</th>
<th>周五</th>
<th>周六</th>
</tr>
</thead>
<tbody id="calendar-body">
<!-- 日历内容将通过JavaScript动态生成 -->
</tbody>
</table>
</div>
<script>
// 示例考勤数据
const attendanceData = {
"2025-04": {
// ...
},
"2025-05": {
// ...
},
"2025-06": {
"2025-06-02": {
status: "present",
clockIn: "08:50",
clockOut: "18:05"
},
"2025-06-03": {
status: "present",
clockIn: "08:52",
clockOut: "18:10"
},
"2025-06-04": {
status: "present",
clockIn: "08:49",
clockOut: "18:08"
},
"2025-06-05": {
status: "present",
clockIn: "08:48",
clockOut: "18:05"
},
"2025-06-06": {
status: "present",
clockIn: "08:50",
clockOut: "17:45",
reason: "团队建设活动"
},
"2025-06-09": {
status: "present",
clockIn: "08:52",
clockOut: "18:10"
},
"2025-06-10": {
status: "present",
clockIn: "08:49",
clockOut: "18:05"
},
"2025-06-11": {
status: "late",
clockIn: "09:30",
clockOut: "18:35",
reason: "车辆故障"
},
"2025-06-12": {
status: "present",
clockIn: "08:48",
clockOut: "18:08"
},
"2025-06-13": {
status: "present",
clockIn: "08:50",
clockOut: "18:05"
},
"2025-06-16": {
status: "present",
clockIn: "08:52",
clockOut: "18:10"
},
"2025-06-17": {
status: "present",
clockIn: "08:49",
clockOut: "18:05"
},
}
};
// 默认显示当前月份
const today = new Date();
let currentDate = new Date(today.getFullYear(), today.getMonth(), 1);
// 初始化日历
function initCalendar() {
renderCalendar();
// 添加上月/下月按钮事件
document.getElementById('prev-month').addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
renderCalendar();
});
document.getElementById('next-month').addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
renderCalendar();
});
}
// 渲染日历
function renderCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const today = new Date();
// 更新显示的月份年份
document.getElementById('current-month').textContent = `${year}年${month + 1}月`;
// 获取当月第一天和最后一天
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// 获取当月天数
const daysInMonth = lastDay.getDate();
// 获取当月第一天是星期几 (0-6, 0是周日)
const firstDayOfWeek = firstDay.getDay();
// 清空日历
const calendarBody = document.getElementById('calendar-body');
calendarBody.innerHTML = '';
// 统计考勤数据
let presentCount = 0;
let abnormalCount = 0;
let lateCount = 0;
let leaveCount = 0;
// 获取当前月份的考勤数据
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
const monthAttendance = attendanceData[monthKey] || {};
// 创建日历行
let date = 1;
for (let i = 0; i < 6; i++) {
// 如果已经超过当月天数,并且已经显示完所有日期,就停止
if (date > daysInMonth && i > 0) break;
const row = document.createElement('tr');
// 创建日历单元格
for (let j = 0; j < 7; j++) {
const cell = document.createElement('td');
// 标记周末
if (j === 0 || j === 6) {
cell.classList.add('weekend');
}
if (i === 0 && j < firstDayOfWeek) {
// 上个月的日期
const prevMonthLastDay = new Date(year, month, 0).getDate();
const prevDate = prevMonthLastDay - (firstDayOfWeek - j - 1);
cell.innerHTML = `<div class="day-number">${prevDate}</div>`;
cell.classList.add('other-month');
} else if (date > daysInMonth) {
// 下个月的日期
const nextDate = date - daysInMonth;
cell.innerHTML = `<div class="day-number">${nextDate}</div>`;
cell.classList.add('other-month');
date++;
} else {
// 当月的日期
const currentDateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`;
const dayOfWeek = new Date(year, month, date).getDay();
cell.innerHTML = `<div class="day-number">${date}</div>`;
// 如果是今天,添加特殊样式
if (year === today.getFullYear() &&
month === today.getMonth() &&
date === today.getDate()) {
cell.classList.add('today');
}
// 添加考勤状态
const record = monthAttendance[currentDateStr];
if (record) {
let statusText = '';
let statusClass = '';
switch (record.status) {
case 'present':
statusText = '正常出勤';
statusClass = 'present';
presentCount++;
break;
case 'absent':
statusText = '缺勤';
statusClass = 'absent';
abnormalCount++;
break;
case 'late':
statusText = '迟到';
statusClass = 'late';
lateCount++;
abnormalCount++;
break;
case 'leave':
statusText = '请假';
statusClass = 'leave';
leaveCount++;
break;
case 'early-leave':
statusText = '早退';
statusClass = 'early-leave';
lateCount++;
abnormalCount++;
break;
default:
statusText = '异常';
statusClass = 'abnormal';
abnormalCount++;
}
cell.innerHTML += `<span class="attendance-status ${statusClass}">${statusText}</span>
<div class="attendance-info work-time">上班: ${record.clockIn || '-'}</div>
<div class="attendance-info work-time">下班: ${record.clockOut || '-'}</div>
${record.reason ? `<div class="reason">原因: ${record.reason}</div>` : ''}`;
}
date++;
}
row.appendChild(cell);
}
calendarBody.appendChild(row);
}
// 更新统计信息
document.getElementById('present-days').textContent = presentCount;
document.getElementById('abnormal-days').textContent = abnormalCount;
document.getElementById('late-days').textContent = lateCount;
document.getElementById('leave-days').textContent = leaveCount;
}
// 页面加载时初始化日历
window.onload = initCalendar;
</script>
</body>
</html>