540 lines
22 KiB
HTML
540 lines
22 KiB
HTML
{% extends 'layout/base.html' %}
|
|
|
|
{% block title %}我的考勤 - CHM考勤管理系统{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid mt-4">
|
|
<!-- 页面标题 -->
|
|
<div class="row mb-4">
|
|
<div class="col">
|
|
<h2 class="fw-bold text-primary">
|
|
<i class="fas fa-calendar-check me-2"></i>我的考勤记录
|
|
</h2>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{{ url_for('student.dashboard') }}">首页</a></li>
|
|
<li class="breadcrumb-item active">考勤记录</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 统计卡片 -->
|
|
{% if total_stats %}
|
|
<div class="row mb-4">
|
|
<div class="col-md-2">
|
|
<div class="card border-left-primary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
|
总考勤周数
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ total_stats.total_weeks }}周
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-calendar-week fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<div class="card border-left-success shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
|
|
总出勤时长
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ "%.1f"|format(total_stats.total_actual_hours) }}h
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-clock fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<div class="card border-left-info shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
|
班内工作
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ "%.1f"|format(total_stats.total_class_hours) }}h
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-briefcase fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<div class="card border-left-warning shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
|
迟到次数
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ total_stats.total_late_count }}次
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-exclamation-triangle fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<div class="card border-left-danger shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-danger text-uppercase mb-1">
|
|
旷工天数
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ total_stats.total_absent_days }}天
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-times-circle fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-2">
|
|
<div class="card border-left-secondary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">
|
|
周均时长
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{{ "%.1f"|format(total_stats.avg_weekly_hours) }}h
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-chart-line fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- 筛选器 -->
|
|
<div class="card mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-filter me-2"></i>筛选条件
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="GET" action="{{ url_for('student.attendance') }}" class="row g-3">
|
|
<div class="col-md-4">
|
|
<label for="start_date" class="form-label">开始日期</label>
|
|
<input type="date" class="form-control" id="start_date" name="start_date"
|
|
value="{{ start_date or '' }}">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="end_date" class="form-label">结束日期</label>
|
|
<input type="date" class="form-control" id="end_date" name="end_date"
|
|
value="{{ end_date or '' }}">
|
|
</div>
|
|
<div class="col-md-4 d-flex align-items-end">
|
|
<div class="d-grid gap-2 d-md-flex w-100">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-search me-1"></i>筛选
|
|
</button>
|
|
<a href="{{ url_for('student.attendance') }}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-refresh me-1"></i>重置
|
|
</a>
|
|
{% if attendance_records %}
|
|
<button type="button" class="btn btn-outline-info" onclick="exportData()">
|
|
<i class="fas fa-download me-1"></i>导出
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 考勤记录表格 -->
|
|
<div class="card shadow">
|
|
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-table me-2"></i>考勤记录列表
|
|
</h6>
|
|
{% if attendance_records %}
|
|
<span class="badge bg-info">
|
|
共 {{ pagination.total }} 条记录
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="card-body">
|
|
{% if attendance_records %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>周次</th>
|
|
<th>实际工作时长</th>
|
|
<th>班内工作时长</th>
|
|
<th>迟到次数</th>
|
|
<th>旷工天数</th>
|
|
<th>加班时长</th>
|
|
<th>记录时间</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for record in attendance_records %}
|
|
<tr>
|
|
<td>
|
|
<div>
|
|
<strong>{{ record.week_start_date.strftime('%Y-%m-%d') }}</strong>
|
|
<small class="d-block text-muted">
|
|
至 {{ record.week_end_date.strftime('%Y-%m-%d') }}
|
|
</small>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-primary">{{ "%.1f"|format(record.actual_work_hours) }}h</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-success">{{ "%.1f"|format(record.class_work_hours) }}h</span>
|
|
</td>
|
|
<td>
|
|
{% if record.late_count > 0 %}
|
|
<span class="badge bg-warning">{{ record.late_count }}次</span>
|
|
{% else %}
|
|
<span class="badge bg-success">0次</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if record.absent_days > 0 %}
|
|
<span class="badge bg-danger">{{ record.absent_days }}天</span>
|
|
{% else %}
|
|
<span class="badge bg-success">0天</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if record.overtime_hours > 0 %}
|
|
<span class="badge bg-info">{{ "%.1f"|format(record.overtime_hours) }}h</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">0h</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<small class="text-muted">
|
|
{{ record.created_at.strftime('%m-%d %H:%M') }}
|
|
</small>
|
|
</td>
|
|
<td>
|
|
<a href="{{ url_for('student.attendance_details', record_id=record.record_id) }}"
|
|
class="btn btn-sm btn-outline-primary">
|
|
<i class="fas fa-eye me-1"></i>查看详情
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- 分页 -->
|
|
{% if pagination.pages > 1 %}
|
|
<nav aria-label="考勤记录分页" class="mt-4">
|
|
<ul class="pagination justify-content-center">
|
|
{% if pagination.has_prev %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('student.attendance', page=pagination.prev_num, start_date=start_date, end_date=end_date) }}">
|
|
<i class="fas fa-chevron-left"></i>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for page_num in pagination.iter_pages() %}
|
|
{% if page_num %}
|
|
{% if page_num != pagination.page %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('student.attendance', page=page_num, start_date=start_date, end_date=end_date) }}">
|
|
{{ page_num }}
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ page_num }}</span>
|
|
</li>
|
|
{% endif %}
|
|
{% else %}
|
|
<li class="page-item disabled">
|
|
<span class="page-link">…</span>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if pagination.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('student.attendance', page=pagination.next_num, start_date=start_date, end_date=end_date) }}">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-calendar-times fa-4x text-muted mb-3"></i>
|
|
<h5 class="text-muted">暂无考勤记录</h5>
|
|
<p class="text-muted">当前筛选条件下没有找到考勤记录</p>
|
|
<a href="{{ url_for('student.dashboard') }}" class="btn btn-primary">
|
|
<i class="fas fa-home me-2"></i>返回首页
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 快捷操作 -->
|
|
{% if attendance_records %}
|
|
<div class="row mt-4">
|
|
<div class="col-md-6">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-chart-pie me-2"></i>本期统计分析
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row text-center">
|
|
<div class="col-4">
|
|
<div class="border-end">
|
|
<h6 class="text-primary">{{ "%.1f"|format(total_stats.avg_weekly_hours) }}h</h6>
|
|
<small class="text-muted">周均出勤</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<div class="border-end">
|
|
<h6 class="text-success">{{ "%.1f"|format((total_stats.total_class_hours / total_stats.total_actual_hours * 100) if total_stats.total_actual_hours > 0 else 0) }}%</h6>
|
|
<small class="text-muted">班内工作率</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-4">
|
|
<h6 class="text-info">{{ "%.1f"|format((total_stats.total_overtime_hours / total_stats.total_weeks) if total_stats.total_weeks > 0 else 0) }}h</h6>
|
|
<small class="text-muted">周均加班</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="card shadow">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-tools me-2"></i>快捷操作
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-6 mb-2">
|
|
<a href="{{ url_for('student.statistics') }}" class="btn btn-outline-primary btn-block">
|
|
<i class="fas fa-chart-bar me-2"></i>统计报表
|
|
</a>
|
|
</div>
|
|
<div class="col-6 mb-2">
|
|
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-secondary btn-block">
|
|
<i class="fas fa-user me-2"></i>个人资料
|
|
</a>
|
|
</div>
|
|
<div class="col-6 mb-2">
|
|
<a href="{{ url_for('auth.change_password') }}" class="btn btn-outline-warning btn-block">
|
|
<i class="fas fa-key me-2"></i>修改密码
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.border-left-primary {
|
|
border-left: 0.25rem solid #4e73df !important;
|
|
}
|
|
|
|
.border-left-success {
|
|
border-left: 0.25rem solid #1cc88a !important;
|
|
}
|
|
|
|
.border-left-info {
|
|
border-left: 0.25rem solid #36b9cc !important;
|
|
}
|
|
|
|
.border-left-warning {
|
|
border-left: 0.25rem solid #f6c23e !important;
|
|
}
|
|
|
|
.border-left-danger {
|
|
border-left: 0.25rem solid #e74a3b !important;
|
|
}
|
|
|
|
.border-left-secondary {
|
|
border-left: 0.25rem solid #858796 !important;
|
|
}
|
|
|
|
.border-end {
|
|
border-right: 1px solid #dee2e6;
|
|
}
|
|
|
|
.text-xs {
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.card {
|
|
border: 0;
|
|
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15) !important;
|
|
}
|
|
|
|
.btn-block {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
.table th {
|
|
background-color: #f8f9fc;
|
|
border-top: none;
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
color: #5a5c69;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
/* 响应式调整 */
|
|
@media (max-width: 768px) {
|
|
.col-md-2 {
|
|
flex: 0 0 50%;
|
|
max-width: 50%;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.table-responsive {
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.65rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
function exportData() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
params.set('export', 'excel');
|
|
|
|
// 构建导出URL
|
|
const exportUrl = '{{ url_for("student.attendance") }}?' + params.toString();
|
|
|
|
// 创建临时链接并触发下载
|
|
const link = document.createElement('a');
|
|
link.href = exportUrl;
|
|
link.download = '我的考勤记录.xlsx';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
// 自动设置结束日期为开始日期的一个月后
|
|
document.getElementById('start_date').addEventListener('change', function() {
|
|
const startDate = new Date(this.value);
|
|
if (startDate && !document.getElementById('end_date').value) {
|
|
const endDate = new Date(startDate);
|
|
endDate.setMonth(startDate.getMonth() + 1);
|
|
document.getElementById('end_date').value = endDate.toISOString().split('T')[0];
|
|
}
|
|
});
|
|
|
|
// 页面加载完成后的初始化
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('学生考勤记录页面已加载');
|
|
|
|
// 显示统计信息的提示
|
|
{% if total_stats and total_stats.total_weeks > 0 %}
|
|
console.log('统计信息:', {
|
|
总周数: {{ total_stats.total_weeks }},
|
|
总出勤时长: {{ total_stats.total_actual_hours }},
|
|
迟到次数: {{ total_stats.total_late_count }},
|
|
周均时长: {{ total_stats.avg_weekly_hours }}
|
|
});
|
|
{% endif %}
|
|
});
|
|
|
|
// 如果有迟到记录,显示温馨提示
|
|
{% if total_stats and total_stats.total_late_count > 0 %}
|
|
setTimeout(function() {
|
|
if ({{ total_stats.total_late_count }} > 5) {
|
|
const toast = `
|
|
<div class="toast show position-fixed bottom-0 end-0 m-3" role="alert" style="z-index: 1055;">
|
|
<div class="toast-header">
|
|
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
|
|
<strong class="me-auto">考勤提醒</strong>
|
|
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
<div class="toast-body">
|
|
您的迟到次数较多({{ total_stats.total_late_count }}次),请注意准时上班。
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.insertAdjacentHTML('beforeend', toast);
|
|
|
|
// 5秒后自动关闭
|
|
setTimeout(function() {
|
|
const toastElement = document.querySelector('.toast');
|
|
if (toastElement) {
|
|
toastElement.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
}, 1000);
|
|
{% endif %}
|
|
</script>
|
|
{% endblock %}
|