250 lines
8.7 KiB
HTML
250 lines
8.7 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}数据统计 - 钢琴练习方案系统{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<h2 class="mb-4"><i class="bi bi-bar-chart"></i> 数据统计</h2>
|
|
|
|
<!-- 顶部统计卡片 -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-4">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h3 class="mb-0" id="totalStudents">-</h3>
|
|
<small class="text-muted">学员总数</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h3 class="mb-0" id="totalProblems">-</h3>
|
|
<small class="text-muted">问题记录总数</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<h3 class="mb-0" id="avgProblems">-</h3>
|
|
<small class="text-muted">人均问题数</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 饼图区 -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title"><i class="bi bi-exclamation-triangle"></i> 问题严重程度分布</h5>
|
|
<canvas id="severityChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title"><i class="bi bi-speedometer2"></i> 问题级别分布</h5>
|
|
<canvas id="levelChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 问题×级别矩阵 -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title"><i class="bi bi-grid"></i> 问题级别×严重程度矩阵</h5>
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered text-center" id="matrixTable">
|
|
<thead>
|
|
<tr>
|
|
<th>严重程度</th>
|
|
<th>启蒙</th>
|
|
<th>入门</th>
|
|
<th>进阶</th>
|
|
<th>熟练</th>
|
|
<th>精通</th>
|
|
<th>合计</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="matrixBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 班级统计 -->
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title"><i class="bi bi-people"></i> 各班级学员数量</h5>
|
|
<canvas id="classStudentChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title"><i class="bi bi-list-check"></i> 各班级问题数量</h5>
|
|
<canvas id="classProblemChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
let severityChart, levelChart, classStudentChart, classProblemChart;
|
|
|
|
async function loadStatistics() {
|
|
try {
|
|
const resp = await fetch('/api/statistics/students');
|
|
const data = await resp.json();
|
|
|
|
// 更新顶部卡片
|
|
document.getElementById('totalStudents').textContent = data.total_students || 0;
|
|
document.getElementById('totalProblems').textContent = data.total_problems || 0;
|
|
const avg = data.total_students > 0
|
|
? (data.total_problems / data.total_students).toFixed(1)
|
|
: 0;
|
|
document.getElementById('avgProblems').textContent = avg;
|
|
|
|
// 严重程度饼图
|
|
const sevCtx = document.getElementById('severityChart').getContext('2d');
|
|
if (severityChart) severityChart.destroy();
|
|
severityChart = new Chart(sevCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: Object.keys(data.severity_distribution),
|
|
datasets: [{
|
|
data: Object.values(data.severity_distribution),
|
|
backgroundColor: ['#28a745', '#ffc107', '#dc3545'],
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { position: 'bottom' }
|
|
}
|
|
}
|
|
});
|
|
|
|
// 级别饼图
|
|
const lvCtx = document.getElementById('levelChart').getContext('2d');
|
|
if (levelChart) levelChart.destroy();
|
|
levelChart = new Chart(lvCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: Object.keys(data.level_distribution),
|
|
datasets: [{
|
|
data: Object.values(data.level_distribution),
|
|
backgroundColor: ['#17a2b8', '#6610f2', '#e85d04', '#e83e8c', '#20c997', '#6c757d'],
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { position: 'bottom' }
|
|
}
|
|
}
|
|
});
|
|
|
|
// 矩阵表格
|
|
const matrixBody = document.getElementById('matrixBody');
|
|
const matrix = data.problem_level_matrix;
|
|
const levels = ['启蒙', '入门', '进阶', '熟练', '精通'];
|
|
const severities = ['轻微', '中等', '严重'];
|
|
let html = '';
|
|
let sevTotals = {轻微: 0, 中等: 0, 严重: 0};
|
|
let lvTotals = {启蒙: 0, 入门: 0, 进阶: 0, 熟练: 0, 精通: 0};
|
|
let grandTotal = 0;
|
|
|
|
for (const sev of severities) {
|
|
html += `<tr><td><strong>${sev}</strong></td>`;
|
|
let rowTotal = 0;
|
|
for (const lv of levels) {
|
|
const count = matrix[sev]?.[lv] || 0;
|
|
html += `<td>${count}</td>`;
|
|
rowTotal += count;
|
|
lvTotals[lv] += count;
|
|
grandTotal += count;
|
|
}
|
|
html += `<td><strong>${rowTotal}</strong></td></tr>`;
|
|
sevTotals[sev] = rowTotal;
|
|
}
|
|
// 合计行
|
|
html += `<tr class="table-secondary"><td><strong>合计</strong></td>`;
|
|
for (const lv of levels) {
|
|
html += `<td><strong>${lvTotals[lv]}</strong></td>`;
|
|
}
|
|
html += `<td><strong>${grandTotal}</strong></td></tr>`;
|
|
matrixBody.innerHTML = html;
|
|
|
|
// 班级学员条形图
|
|
const csCtx = document.getElementById('classStudentChart').getContext('2d');
|
|
if (classStudentChart) classStudentChart.destroy();
|
|
classStudentChart = new Chart(csCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.class_student_count.map(c => c.class_name),
|
|
datasets: [{
|
|
label: '学员数',
|
|
data: data.class_student_count.map(c => c.student_count),
|
|
backgroundColor: '#0d6efd',
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
y: { beginAtZero: true }
|
|
}
|
|
}
|
|
});
|
|
|
|
// 班级问题条形图
|
|
const cpCtx = document.getElementById('classProblemChart').getContext('2d');
|
|
if (classProblemChart) classProblemChart.destroy();
|
|
classProblemChart = new Chart(cpCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.class_problem_count.map(c => c.class_name),
|
|
datasets: [{
|
|
label: '问题数',
|
|
data: data.class_problem_count.map(c => c.problem_count),
|
|
backgroundColor: '#fd7e14',
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
y: { beginAtZero: true }
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (e) {
|
|
console.error('加载统计失败:', e);
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadStatistics);
|
|
</script>
|
|
{% endblock %}
|