You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

192 lines
4.8 KiB

{% extends 'base.html' %}
{% block title %}统计 - 课堂点名系统{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col-md-8">
<h4>积分走势图</h4>
<form class="row g-2 mb-2" id="chart-filter-form">
<div class="col-auto">
<label class="col-form-label">时间范围</label>
</div>
<div class="col-auto">
<select id="filter-days" class="form-select form-select-sm">
<option value="">全部</option>
<option value="7">最近7天</option>
<option value="30">最近30天</option>
</select>
</div>
<div class="col-auto">
<label class="col-form-label">前 N 名</label>
</div>
<div class="col-auto">
<input
type="number"
id="filter-top-n"
class="form-control form-control-sm"
value="5"
min="1"
max="10"
/>
</div>
<div class="col-auto">
<button type="button" id="btn-refresh-chart" class="btn btn-sm btn-outline-primary">
更新
</button>
</div>
</form>
<canvas id="scoreChart" height="150"></canvas>
</div>
<div class="col-md-4 text-end align-self-end">
<a href="{{ url_for('main.export_students') }}" class="btn btn-sm btn-outline-primary">
导出积分详单
</a>
<a
href="{{ url_for('main.export_rollcalls') }}"
class="btn btn-sm btn-outline-secondary ms-2"
>
导出点名记录
</a>
</div>
</div>
<div class="row mt-3">
<div class="col-md-8">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>名次</th>
<th>学号</th>
<th>姓名</th>
<th>专业</th>
<th>总积分</th>
<th>出勤次数</th>
<th>随机点名次数</th>
</tr>
</thead>
<tbody>
{% for s in students %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ s.student_no }}</td>
<td>{{ s.name }}</td>
<td>{{ s.major }}</td>
<td>{{ '%.1f'|format(s.total_score or 0) }}</td>
<td>{{ s.attendance_count or 0 }}</td>
<td>{{ s.random_called_count or 0 }}</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center">暂无学生数据。</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-4">
<h5>最近点名记录</h5>
<div class="table-responsive" style="max-height: 500px; overflow-y: auto">
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>时间</th>
<th>姓名</th>
<th>学号</th>
<th>模式</th>
<th>状态</th>
<th>积分</th>
</tr>
</thead>
<tbody>
{% for rc, s in records %}
<tr>
<td>{{ rc.local_time.strftime('%m-%d %H:%M') }}</td>
<td>{{ s.name }}</td>
<td>{{ s.student_no }}</td>
<td>{{ '随机' if rc.mode == 'random' else '顺序' }}</td>
<td>
{% if rc.status == 'absent' %}缺勤{% elif rc.status == 'distracted' %}走神{% else %}到课{% endif %}
</td>
<td>{{ '%.1f'|format(rc.score_change or 0) }}</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center">暂无点名记录。</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
let chartInstance = null;
async function loadScoreChart() {
const days = document.getElementById("filter-days").value;
const topNInput = document.getElementById("filter-top-n");
let topN = parseInt(topNInput.value || "5", 10);
if (Number.isNaN(topN) || topN <= 0) topN = 5;
if (topN > 10) topN = 10;
topNInput.value = topN;
const params = new URLSearchParams();
params.set("top_n", topN.toString());
if (days) params.set("days", days);
const resp = await fetch(`/api/stats/top_students_timeline?${params.toString()}`);
const data = await resp.json();
if (!data.labels || data.labels.length === 0) {
return;
}
const ctx = document.getElementById("scoreChart");
const colors = [
"#007bff",
"#28a745",
"#ffc107",
"#dc3545",
"#6610f2",
];
if (chartInstance) {
chartInstance.destroy();
}
chartInstance = new Chart(ctx, {
type: "line",
data: {
labels: data.labels,
datasets: data.series.map((s, idx) => ({
label: s.name,
data: s.data,
borderColor: colors[idx % colors.length],
backgroundColor: "rgba(0,0,0,0)",
tension: 0.2,
})),
},
options: {
responsive: true,
interaction: { mode: "index", intersect: false },
stacked: false,
plugins: {
legend: { position: "bottom" },
},
scales: {
x: { title: { display: true, text: "时间" } },
y: { title: { display: true, text: "累计积分" } },
},
},
});
}
document.addEventListener("DOMContentLoaded", () => {
document
.getElementById("btn-refresh-chart")
.addEventListener("click", loadScoreChart);
loadScoreChart();
});
</script>
{% endblock %}