init
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import { state } from '../core/state.js';
|
||||
import { getToday } from '../services/ebbinghaus.js';
|
||||
import { getErrorTopWords, getLast30DaysData, getStats, getStreak } from '../services/stats.js';
|
||||
import { escapeHtml } from '../ui/dom.js';
|
||||
|
||||
let studyChart = null;
|
||||
// ==================== Page: Stats ====================
|
||||
export function renderStats(el) {
|
||||
destroyStudyChart();
|
||||
if (state.records.length === 0 && state.words.length === 0) {
|
||||
el.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>学习统计</h1>
|
||||
<p class="page-desc">查看你的学习进度与数据分析</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon"><i class="fas fa-chart-line"></i></div>
|
||||
<h3>暂无数据</h3>
|
||||
<p>开始学习后这里会显示你的统计数据</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const s = getStats();
|
||||
const streak = getStreak();
|
||||
const todayRecords = state.records.filter(r => r.date === getToday());
|
||||
const todayCorrect = todayRecords.filter(r => r.isCorrect).length;
|
||||
const todayTotal = todayRecords.length;
|
||||
const todayRate = todayTotal > 0 ? Math.round(todayCorrect / todayTotal * 100) : 0;
|
||||
|
||||
const allCorrect = state.records.filter(r => r.isCorrect).length;
|
||||
const allTotal = state.records.length;
|
||||
const allRate = allTotal > 0 ? Math.round(allCorrect / allTotal * 100) : 0;
|
||||
|
||||
const dailyData = getLast30DaysData();
|
||||
const errorTop = getErrorTopWords(10);
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>学习统计</h1>
|
||||
<p class="page-desc">查看你的学习进度与数据分析</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-body">
|
||||
<div class="stats-left">
|
||||
<div class="stats-left-cards">
|
||||
<div class="stat-card" style="border-left:3px solid var(--primary)">
|
||||
<div class="stat-icon" style="background:var(--primary-bg);color:var(--primary)"><i class="fas fa-database"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">${s.total}</div>
|
||||
<div class="stat-label">总单词</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left:3px solid var(--success)">
|
||||
<div class="stat-icon" style="background:var(--success-light);color:var(--success)"><i class="fas fa-check-circle"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">${s.mastered}</div>
|
||||
<div class="stat-label">已掌握</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left:3px solid var(--warning)">
|
||||
<div class="stat-icon" style="background:var(--warning-light);color:var(--warning)"><i class="fas fa-fire"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">${streak}</div>
|
||||
<div class="stat-label">连续天数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left:3px solid var(--error)">
|
||||
<div class="stat-icon" style="background:var(--error-light);color:var(--error)"><i class="fas fa-bullseye"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">${allRate}%</div>
|
||||
<div class="stat-label">总正确率</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left:3px solid var(--primary)">
|
||||
<div class="stat-icon" style="background:var(--primary-bg);color:var(--primary)"><i class="fas fa-pen"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">${todayTotal}</div>
|
||||
<div class="stat-label">今日练习次数</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card" style="border-left:3px solid var(--success)">
|
||||
<div class="stat-icon" style="background:var(--success-light);color:var(--success)"><i class="fas fa-bullseye"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">${todayRate}%</div>
|
||||
<div class="stat-label">今日正确率</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="flex:1">
|
||||
<h3><i class="fas fa-chart-bar"></i> 近30天学习量</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="study-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card stats-error-card">
|
||||
<h3><i class="fas fa-exclamation-triangle" style="color:var(--error)"></i> 最容易出错的单词 Top 10</h3>
|
||||
${errorTop.length > 0 ? `
|
||||
<div style="overflow-y:auto;flex:1">
|
||||
<table class="error-table">
|
||||
<thead><tr><th>单词</th><th>释义</th><th>正确率</th><th>次数</th></tr></thead>
|
||||
<tbody>
|
||||
${errorTop.map(w => {
|
||||
const color = w.rate < 40 ? 'var(--error)' : w.rate < 70 ? 'var(--warning)' : 'var(--success)';
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(w.english)}</strong></td>
|
||||
<td style="color:var(--text-secondary)">${escapeHtml(w.chinese)}</td>
|
||||
<td>
|
||||
<span style="color:${color};font-weight:600">${w.rate}%</span>
|
||||
<div class="accuracy-bar"><div class="accuracy-fill" style="width:${w.rate}%;background:${color}"></div></div>
|
||||
</td>
|
||||
<td style="color:var(--text-muted);text-align:center">${w.total}</td>
|
||||
</tr>`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>` : '<div style="text-align:center;padding:40px 20px;color:var(--text-muted);flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center"><i class="fas fa-chart-bar" style="font-size:32px;margin-bottom:12px;opacity:0.4"></i><p>暂无测试数据</p><p style="font-size:12px;margin-top:4px">完成 AI 测试后,错误统计将在此展示</p></div>'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3><i class="fas fa-chart-pie"></i> 掌握程度分布</h3>
|
||||
<div style="padding:16px 0">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:6px">
|
||||
<span style="color:var(--success);font-weight:600"><i class="fas fa-check-circle" style="margin-right:4px"></i> 已掌握</span><span style="font-weight:700">${s.mastered}</span>
|
||||
</div>
|
||||
<div class="progress-bar" style="height:10px;margin-bottom:16px">
|
||||
<div class="progress-fill" style="width:${s.total ? s.mastered/s.total*100 : 0}%;background:var(--success)"></div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:6px">
|
||||
<span style="color:var(--warning);font-weight:600"><i class="fas fa-spinner" style="margin-right:4px"></i> 学习中</span><span style="font-weight:700">${s.learning}</span>
|
||||
</div>
|
||||
<div class="progress-bar" style="height:10px;margin-bottom:16px">
|
||||
<div class="progress-fill" style="width:${s.total ? s.learning/s.total*100 : 0}%;background:var(--warning)"></div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:13px;margin-bottom:6px">
|
||||
<span style="color:var(--text-muted);font-weight:600"><i class="fas fa-plus-circle" style="margin-right:4px"></i> 新词</span><span style="font-weight:700">${s.newCount}</span>
|
||||
</div>
|
||||
<div class="progress-bar" style="height:10px">
|
||||
<div class="progress-fill" style="width:${s.total ? s.newCount/s.total*100 : 0}%;background:var(--text-muted)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
renderStudyChart(dailyData);
|
||||
}
|
||||
|
||||
export function destroyStudyChart() {
|
||||
if (!studyChart) return;
|
||||
studyChart.destroy();
|
||||
studyChart = null;
|
||||
}
|
||||
|
||||
export function refreshStudyChart() {
|
||||
if (!document.getElementById('study-chart')) return;
|
||||
renderStudyChart(getLast30DaysData());
|
||||
}
|
||||
|
||||
export function renderStudyChart(dailyData) {
|
||||
const ctx = document.getElementById('study-chart');
|
||||
if (!ctx || typeof globalThis.Chart === 'undefined') return;
|
||||
|
||||
destroyStudyChart();
|
||||
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
const gridColor = isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(148, 163, 184, 0.15)';
|
||||
const textColor = isDark ? '#94a3b8' : '#64748b';
|
||||
|
||||
studyChart = new globalThis.Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: dailyData.map(d => d.label),
|
||||
datasets: [{
|
||||
label: '每日学习量',
|
||||
data: dailyData.map(d => d.count),
|
||||
backgroundColor: 'rgba(99, 102, 241, 0.7)',
|
||||
hoverBackgroundColor: 'rgba(99, 102, 241, 0.9)',
|
||||
borderRadius: 4,
|
||||
borderSkipped: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: isDark ? '#334155' : '#1e293b',
|
||||
titleFont: { size: 12 },
|
||||
bodyFont: { size: 13 },
|
||||
padding: 10,
|
||||
cornerRadius: 8
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: { color: gridColor },
|
||||
ticks: { color: textColor, font: { size: 11 } }
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: { color: textColor, font: { size: 10 }, maxRotation: 45 }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user