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
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 %}
|