431 lines
15 KiB
HTML
431 lines
15 KiB
HTML
{% extends 'layout/base.html' %}
|
|
|
|
{% block title %}统计报表 - SmartDSP考勤管理系统{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
|
|
.chart-container canvas {
|
|
max-height: 300px !important;
|
|
}
|
|
|
|
.chart-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
min-height: 350px; /* 确保容器有足够高度 */
|
|
}
|
|
|
|
.statistics-card {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 10px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.grade-section {
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.grade-title {
|
|
color: #495057;
|
|
border-bottom: 2px solid #007bff;
|
|
padding-bottom: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.student-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
margin-bottom: 10px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.student-card:hover {
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.stats-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: center;
|
|
flex: 1;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 1.2em;
|
|
font-weight: bold;
|
|
color: #007bff;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.9em;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.search-section {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.chart-container {
|
|
background: white;
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2><i class="fas fa-chart-bar me-2"></i>统计报表</h2>
|
|
<div>
|
|
<a href="{{ url_for('admin.export_statistics') }}" class="btn btn-success">
|
|
<i class="fas fa-download me-1"></i>导出数据
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 总体统计卡片 -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="statistics-card">
|
|
<div class="text-center">
|
|
<h3>{{ overall_stats.total_students }}</h3>
|
|
<p class="mb-0">总学生数</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="statistics-card">
|
|
<div class="text-center">
|
|
<h3>{{ "%.1f"|format(overall_stats.total_work_hours) }}</h3>
|
|
<p class="mb-0">总出勤时长(小时)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="statistics-card">
|
|
<div class="text-center">
|
|
<h3>{{ overall_stats.total_absent_days }}</h3>
|
|
<p class="mb-0">总缺勤天数</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="statistics-card">
|
|
<div class="text-center">
|
|
<h3>{{ overall_stats.total_late_count }}</h3>
|
|
<p class="mb-0">总迟到次数</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 搜索和筛选 -->
|
|
<div class="search-section">
|
|
<form method="GET" class="row g-3">
|
|
<div class="col-md-3">
|
|
<label class="form-label">搜索学生</label>
|
|
<input type="text" class="form-control" name="search"
|
|
value="{{ search }}" placeholder="姓名或学号">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">年级</label>
|
|
<select class="form-select" name="grade">
|
|
<option value="">全部年级</option>
|
|
{% for grade in grades %}
|
|
<option value="{{ grade }}" {{ 'selected' if selected_grade == grade|string }}>
|
|
{% if grade == 1 %}研一{% elif grade == 2 %}研二{% elif grade == 3 %}研三{% else %}{{ grade }}年级{% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">学院</label>
|
|
<select class="form-select" name="college">
|
|
<option value="">全部学院</option>
|
|
{% for college in colleges %}
|
|
<option value="{{ college }}" {{ 'selected' if selected_college == college }}>{{ college }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label">导师</label>
|
|
<select class="form-select" name="supervisor">
|
|
<option value="">全部导师</option>
|
|
{% for supervisor in supervisors %}
|
|
<option value="{{ supervisor }}" {{ 'selected' if selected_supervisor == supervisor }}>{{ supervisor }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">时间范围</label>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<input type="date" class="form-control" name="start_date" value="{{ start_date }}">
|
|
</div>
|
|
<div class="col-6">
|
|
<input type="date" class="form-control" name="end_date" value="{{ end_date }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-search me-1"></i>筛选
|
|
</button>
|
|
<a href="{{ url_for('admin.statistics') }}" class="btn btn-outline-secondary ms-2">
|
|
<i class="fas fa-undo me-1"></i>重置
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 按年级分组的学生统计 -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
{% for grade_label, students in grade_groups.items() %}
|
|
<div class="grade-section">
|
|
<h4 class="grade-title">
|
|
<i class="fas fa-graduation-cap me-2"></i>{{ grade_label }} ({{ students|length }}人)
|
|
</h4>
|
|
|
|
<div class="row">
|
|
{% for student in students %}
|
|
<div class="col-md-6 col-lg-4 mb-3">
|
|
<div class="student-card">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
<h6 class="mb-1">
|
|
<a href="{{ url_for('admin.student_detail', student_number=student.student_number) }}"
|
|
class="text-decoration-none">
|
|
{{ student.name }}
|
|
</a>
|
|
</h6>
|
|
<small class="text-muted">{{ student.student_number }}</small>
|
|
</div>
|
|
<div class="text-end">
|
|
{% if student.total_work_hours >= 200 %}
|
|
<span class="badge bg-success">优秀</span>
|
|
{% elif student.total_work_hours >= 100 %}
|
|
<span class="badge bg-primary">良好</span>
|
|
{% elif student.total_work_hours >= 50 %}
|
|
<span class="badge bg-warning">一般</span>
|
|
{% else %}
|
|
<span class="badge bg-danger">待改进</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-row mt-3">
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ "%.1f"|format(student.total_work_hours) }}</div>
|
|
<div class="stat-label">总工时</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ student.total_absent_days }}</div>
|
|
<div class="stat-label">缺勤天数</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ student.total_late_count }}</div>
|
|
<div class="stat-label">迟到次数</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value">{{ student.avg_weekly_hours }}</div>
|
|
<div class="stat-label">周均工时</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-2">
|
|
<small class="text-muted">
|
|
<i class="fas fa-building me-1"></i>{{ student.college or '未设置' }} |
|
|
<i class="fas fa-user-tie me-1"></i>{{ student.supervisor or '未设置' }}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{% if not grade_groups %}
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-search fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">没有找到符合条件的学生</h5>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表区域 -->
|
|
<div class="row mt-4">
|
|
<div class="col-md-6">
|
|
<div class="chart-container">
|
|
<h5><i class="fas fa-chart-line me-2"></i>月度考勤趋势</h5>
|
|
<canvas id="monthlyChart" width="400" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="chart-container">
|
|
<h5><i class="fas fa-chart-pie me-2"></i>学院分布</h5>
|
|
<canvas id="collegeChart" width="400" height="200"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// 检查数据
|
|
console.log('月度统计数据:', [{% for stat in monthly_stats %}{ month: '{{ stat.month }}', hours: {{ stat.total_hours or 0 }} }{% if not loop.last %},{% endif %}{% endfor %}]);
|
|
console.log('学院统计数据:', [{% for stat in college_stats %}{ college: '{{ stat.college or "未设置" }}', count: {{ stat.student_count }} }{% if not loop.last %},{% endif %}{% endfor %}]);
|
|
|
|
// 月度趋势图
|
|
const monthlyCtx = document.getElementById('monthlyChart').getContext('2d');
|
|
|
|
// 准备月度数据
|
|
const monthlyData = [{% for stat in monthly_stats %}{{ stat.total_hours or 0 }}{% if not loop.last %},{% endif %}{% endfor %}];
|
|
const monthlyLabels = [{% for stat in monthly_stats %}'{{ stat.month }}'{% if not loop.last %},{% endif %}{% endfor %}];
|
|
|
|
console.log('图表数据:', monthlyData);
|
|
console.log('图表标签:', monthlyLabels);
|
|
|
|
const monthlyChart = new Chart(monthlyCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: monthlyLabels,
|
|
datasets: [{
|
|
label: '总工时',
|
|
data: monthlyData,
|
|
borderColor: 'rgb(75, 192, 192)',
|
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
|
tension: 0.1,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: '工时(小时)'
|
|
}
|
|
},
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: '月份'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: '月度工时统计'
|
|
},
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// 学院分布图
|
|
const collegeCtx = document.getElementById('collegeChart').getContext('2d');
|
|
|
|
// 准备学院数据
|
|
const collegeData = [{% for stat in college_stats %}{{ stat.student_count }}{% if not loop.last %},{% endif %}{% endfor %}];
|
|
const collegeLabels = [{% for stat in college_stats %}'{{ stat.college or "未设置" }}'{% if not loop.last %},{% endif %}{% endfor %}];
|
|
|
|
console.log('学院数据:', collegeData);
|
|
console.log('学院标签:', collegeLabels);
|
|
|
|
// 只有当有数据时才创建图表
|
|
if (collegeData.length > 0 && collegeData.some(d => d > 0)) {
|
|
const collegeChart = new Chart(collegeCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: collegeLabels,
|
|
datasets: [{
|
|
data: collegeData,
|
|
backgroundColor: [
|
|
'#FF6384',
|
|
'#36A2EB',
|
|
'#FFCE56',
|
|
'#4BC0C0',
|
|
'#9966FF',
|
|
'#FF9F40',
|
|
'#C7C7C7',
|
|
'#FF6384'
|
|
],
|
|
borderWidth: 2,
|
|
borderColor: '#fff'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: true,
|
|
text: '学院学生分布'
|
|
},
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
padding: 20,
|
|
usePointStyle: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
// 如果没有数据,显示提示信息
|
|
collegeCtx.font = '16px Arial';
|
|
collegeCtx.textAlign = 'center';
|
|
collegeCtx.fillText('暂无数据', collegeCtx.canvas.width / 2, collegeCtx.canvas.height / 2);
|
|
}
|
|
|
|
// 如果月度数据为空,显示提示
|
|
if (monthlyData.length === 0 || monthlyData.every(d => d === 0)) {
|
|
const monthlyCanvas = document.getElementById('monthlyChart');
|
|
const ctx = monthlyCanvas.getContext('2d');
|
|
ctx.font = '16px Arial';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillText('暂无月度数据', monthlyCanvas.width / 2, monthlyCanvas.height / 2);
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|