study-analysis.xiaoban/assets/template.html

466 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学情分析报告 - 用户{{ROLE_ID}} Level{{LEVEL}} Unit{{UNIT}}</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
accent: { DEFAULT: '#7A5ADB', light: '#EDE9F9', dark: '#5B3DB0' }
}
}
}
}
</script>
<style>
.chart-container { height: 360px; width: 100%; }
.lesson-detail { display: none; }
.lesson-detail.active { display: block; }
.lesson-card { transition: all 0.25s ease; }
.lesson-card:hover { transform: translateY(-2px); }
.lesson-card.active {
background: linear-gradient(135deg, #7A5ADB 0%, #5B3DB0 100%);
color: white;
border-color: transparent;
}
.lesson-card.active .stat-value-oops,
.lesson-card.active .stat-value-wrong { color: #fecaca !important; }
.lesson-card.active .time-label-dim { opacity: 0.7; }
.lesson-card.active .border-accent-light { border-color: rgba(255,255,255,0.25); }
.audio-btn { cursor: pointer; transition: opacity 0.2s; }
.audio-btn:hover { opacity: 1; }
.audio-btn.playing { color: #7A5ADB; opacity: 1; }
table { border-collapse: collapse; }
</style>
</head>
<body class="bg-gray-50 text-gray-700 antialiased">
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-6 sm:py-10">
<!-- Header -->
<div class="rounded-2xl bg-gradient-to-br from-accent to-accent-dark text-white px-6 sm:px-10 py-8 mb-6">
<h1 class="text-2xl sm:text-3xl font-bold tracking-tight">学情分析报告</h1>
<p class="mt-2 text-sm opacity-80">用户ID{{ROLE_ID}} · Level {{LEVEL}} · Unit {{UNIT}} · {{GENERATE_TIME}}</p>
</div>
<!-- 总览 -->
<div class="bg-white rounded-2xl shadow-sm p-6 mb-5">
<h2 class="text-lg font-semibold text-gray-800 border-l-4 border-accent pl-3 mb-5">总览</h2>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4" id="stats-grid"></div>
</div>
<!-- 图表 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5 mb-5">
<div class="bg-white rounded-2xl shadow-sm p-6">
<h2 class="text-base font-semibold text-gray-800 border-l-4 border-accent pl-3 mb-4">互动组件表现</h2>
<div id="interactive-chart" class="chart-container"></div>
</div>
<div class="bg-white rounded-2xl shadow-sm p-6">
<h2 class="text-base font-semibold text-gray-800 border-l-4 border-accent pl-3 mb-4">巩固练习正确率</h2>
<div id="exercise-chart" class="chart-container"></div>
</div>
<div class="bg-white rounded-2xl shadow-sm p-6">
<h2 class="text-base font-semibold text-gray-800 border-l-4 border-accent pl-3 mb-4">能力训练正确率</h2>
<div id="ability-chart" class="chart-container"></div>
</div>
</div>
<!-- 课程详情 -->
<div class="bg-white rounded-2xl shadow-sm p-6 mb-5">
<h2 class="text-lg font-semibold text-gray-800 border-l-4 border-accent pl-3 mb-5">课程详情</h2>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 mb-6" id="lesson-nav"></div>
<div id="lesson-details"></div>
</div>
<!-- 能力训练 -->
<div class="bg-white rounded-2xl shadow-sm p-6 mb-5">
<h2 class="text-lg font-semibold text-gray-800 border-l-4 border-accent pl-3 mb-5">能力训练</h2>
<div class="overflow-x-auto">
<table class="w-full text-sm" id="ability-training-table">
<thead>
<tr class="border-b-2 border-gray-100">
<th class="text-left py-3 px-3 font-semibold text-gray-500 bg-gray-50/60">#</th>
<th class="text-left py-3 px-3 font-semibold text-gray-500 bg-gray-50/60">来源</th>
<th class="text-left py-3 px-3 font-semibold text-gray-500 bg-gray-50/60">题型</th>
<th class="text-left py-3 px-3 font-semibold text-gray-500 bg-gray-50/60">标题</th>
<th class="text-left py-3 px-3 font-semibold text-gray-500 bg-gray-50/60 hidden sm:table-cell">题目详情</th>
<th class="text-left py-3 px-3 font-semibold text-gray-500 bg-gray-50/60">子题目</th>
<th class="text-left py-3 px-3 font-semibold text-gray-500 bg-gray-50/60">结果</th>
</tr>
</thead>
<tbody id="ability-training-tbody"></tbody>
</table>
</div>
</div>
</div>
<script>
const data = {{DATA}};
// 兼容data可能直接就是lessons数组也可能有外层包装
const lessons = data.lessons || data.data?.lessons || (Array.isArray(data) ? data : []);
const summary = data.summary || data.data?.summary || null;
// ==================== 总览统计 ====================
const statsGrid = document.getElementById('stats-grid');
const totalLessons = lessons.length;
let totalInteractive = 0, totalExercises = 0;
let totalStudyDuration = 0; // 单元学习时长(分钟)
let unitStartTime = null, unitEndTime = null;
lessons.forEach(lesson => {
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
totalInteractive += ics.length;
totalExercises += ces.length;
// 计算每个lesson的学习时长
const entryTime = lesson.entry_time || lesson.entryTime;
const completionTime = lesson.completion_time || lesson.completionTime;
const consolidationEntry = lesson.consolidation_entry_time || lesson.consolidationEntryTime;
const consolidationCompletion = lesson.consolidation_completion_time || lesson.consolidationCompletionTime;
if (entryTime && completionTime) {
const lessonDuration = (new Date(completionTime) - new Date(entryTime)) / (1000 * 60);
if (lessonDuration > 0) totalStudyDuration += lessonDuration;
}
if (consolidationEntry && consolidationCompletion) {
const consolidationDuration = (new Date(consolidationCompletion) - new Date(consolidationEntry)) / (1000 * 60);
if (consolidationDuration > 0) totalStudyDuration += consolidationDuration;
}
// 记录单元开始和结束时间
if (entryTime) {
const entryDate = new Date(entryTime);
if (!unitStartTime || entryDate < unitStartTime) unitStartTime = entryDate;
}
if (consolidationCompletion) {
const completionDate = new Date(consolidationCompletion);
if (!unitEndTime || completionDate > unitEndTime) unitEndTime = completionDate;
}
});
// 计算单元长度从第1个lesson进入到第5个lesson巩固完成
let unitLengthStr = '-';
if (unitStartTime && unitEndTime) {
const unitLengthMs = unitEndTime - unitStartTime;
const unitLengthMinutes = Math.round(unitLengthMs / (1000 * 60));
const days = Math.floor(unitLengthMinutes / (24 * 60));
const hours = Math.floor((unitLengthMinutes % (24 * 60)) / 60);
const minutes = unitLengthMinutes % 60;
if (days > 0) {
unitLengthStr = `${days}${hours}小时${minutes}分钟`;
} else if (hours > 0) {
unitLengthStr = `${hours}小时${minutes}分钟`;
} else {
unitLengthStr = `${minutes}分钟`;
}
}
// 格式化学习时长
let studyDurationStr = '-';
if (totalStudyDuration > 0) {
const studyHours = Math.floor(totalStudyDuration / 60);
const studyMinutes = Math.round(totalStudyDuration % 60);
if (studyHours > 0) {
studyDurationStr = `${studyHours}小时${studyMinutes}分钟`;
} else {
studyDurationStr = `${studyMinutes}分钟`;
}
}
const statsItems = [
{ label: '单元学习时长', value: studyDurationStr, color: 'text-accent' },
{ label: '总互动次数', value: totalInteractive, color: 'text-blue-500' },
{ label: '总练习题数', value: totalExercises, color: 'text-emerald-500' },
{ label: '单元长度', value: unitLengthStr, color: 'text-amber-500' }
];
statsItems.forEach(item => {
const div = document.createElement('div');
div.className = 'bg-gray-50 rounded-xl p-5 text-center';
div.innerHTML = `<div class="text-xs text-gray-400 mb-1">${item.label}</div><div class="text-2xl font-bold ${item.color}">${item.value}</div>`;
statsGrid.appendChild(div);
});
// ==================== 互动组件柱状图(按课程,三段堆叠) ====================
const interactiveChart = echarts.init(document.getElementById('interactive-chart'));
const lessonLabels = lessons.map((l, i) => l.lesson_name || l.lessonName || `Lesson ${i + 1}`);
const perfectCounts = [];
const goodCounts = [];
const oopsCounts = [];
lessons.forEach(lesson => {
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
let perfect = 0, good = 0, oops = 0;
ics.forEach(ic => {
const r = (ic.user_result || ic.userResult || '').toLowerCase();
if (r === 'perfect') perfect++;
else if (r === 'good') good++;
else if (r === 'oops') oops++;
});
perfectCounts.push(perfect);
goodCounts.push(good);
oopsCounts.push(oops);
});
interactiveChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { bottom: 10, data: ['Perfect', 'Good', 'Oops'], textStyle: { color: '#9ca3af' } },
grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true },
xAxis: { type: 'category', data: lessonLabels, axisLabel: { rotate: 20, fontSize: 11, color: '#9ca3af' }, axisLine: { lineStyle: { color: '#e5e7eb' } } },
yAxis: { type: 'value', axisLabel: { color: '#9ca3af' }, splitLine: { lineStyle: { color: '#f3f4f6' } } },
color: ['#10b981', '#7A5ADB', '#f97316'],
series: [
{ name: 'Perfect', type: 'bar', stack: 'total', data: perfectCounts, itemStyle: { borderRadius: [0, 0, 4, 4] } },
{ name: 'Good', type: 'bar', stack: 'total', data: goodCounts, itemStyle: { borderRadius: [0, 0, 0, 0] } },
{ name: 'Oops', type: 'bar', stack: 'total', data: oopsCounts, itemStyle: { borderRadius: [4, 4, 0, 0] } }
]
});
// ==================== 巩固练习柱状图(按课程,两段堆叠) ====================
const exerciseChart = echarts.init(document.getElementById('exercise-chart'));
const correctCounts = [];
const wrongCounts = [];
lessons.forEach(lesson => {
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
let correct = 0, wrong = 0;
ces.forEach(ce => {
if (ce.is_correct === true || ce.isCorrect === true) correct++;
else wrong++;
});
correctCounts.push(correct);
wrongCounts.push(wrong);
});
exerciseChart.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { bottom: 10, data: ['正确', '错误'], textStyle: { color: '#9ca3af' } },
grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true },
xAxis: { type: 'category', data: lessonLabels, axisLabel: { rotate: 20, fontSize: 11, color: '#9ca3af' }, axisLine: { lineStyle: { color: '#e5e7eb' } } },
yAxis: { type: 'value', axisLabel: { color: '#9ca3af' }, splitLine: { lineStyle: { color: '#f3f4f6' } } },
color: ['#10b981', '#ef4444'],
series: [
{ name: '正确', type: 'bar', stack: 'total', data: correctCounts, itemStyle: { borderRadius: [0, 0, 4, 4] } },
{ name: '错误', type: 'bar', stack: 'total', data: wrongCounts, itemStyle: { borderRadius: [4, 4, 0, 0] } }
]
});
// ==================== 能力训练饼图 ====================
const abilityTraining = data.ability_training || data.abilityTraining || [];
let perfectCount = 0, goodCount = 0, oopsCount = 0;
abilityTraining.forEach(at => {
const result = (at.result || '').toLowerCase();
if (result === 'perfect') perfectCount++;
else if (result === 'good') goodCount++;
else if (result === 'oops') oopsCount++;
});
const abilityChart = echarts.init(document.getElementById('ability-chart'));
abilityChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { bottom: 10, data: ['Perfect', 'Good', 'Oops'], textStyle: { color: '#9ca3af' } },
color: ['#10b981', '#7A5ADB', '#f97316'],
series: [{
name: '能力训练结果',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '45%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}: {c}' },
data: [
{ value: perfectCount, name: 'Perfect' },
{ value: goodCount, name: 'Good' },
{ value: oopsCount, name: 'Oops' }
]
}]
});
const navContainer = document.getElementById('lesson-nav');
const detailContainer = document.getElementById('lesson-details');
lessons.forEach((lesson, idx) => {
const card = document.createElement('div');
card.className = 'lesson-card border-2 border-gray-100 rounded-xl p-4 cursor-pointer text-center shadow-sm' + (idx === 0 ? ' active' : '');
card.onclick = () => switchTab(idx);
const entryTime = lesson.entry_time || lesson.entryTime || '-';
const completionTime = lesson.completion_time || lesson.completionTime || '-';
const lessonName = lesson.lesson_name || lesson.lessonName || `Lesson ${idx + 1}`;
const icsForStats = lesson.interactive_components || lesson.interactiveComponents || [];
const cesForStats = lesson.consolidation_exercises || lesson.consolidationExercises || [];
let oopsCount = 0;
icsForStats.forEach(ic => {
const r = (ic.user_result || ic.userResult || '').toLowerCase();
if (r === 'oops') oopsCount++;
});
const oopsRate = icsForStats.length > 0 ? Math.round(oopsCount / icsForStats.length * 100) : 0;
let wrongCount = 0;
cesForStats.forEach(ce => {
if (ce.is_correct !== true && ce.isCorrect !== true) wrongCount++;
});
const wrongRate = cesForStats.length > 0 ? Math.round(wrongCount / cesForStats.length * 100) : 0;
card.innerHTML = `
<div class="text-base font-semibold mb-2">${lessonName}</div>
<div class="text-xs opacity-80 leading-relaxed">
<div><span class="time-label-dim opacity-60">进入:</span>${formatTime(entryTime)}</div>
<div><span class="time-label-dim opacity-60">完成:</span>${formatTime(completionTime)}</div>
</div>
<div class="flex justify-center gap-4 mt-3 pt-3 border-t border-accent-light border-gray-100 text-xs">
<div class="flex flex-col items-center">
<span class="opacity-60 mb-0.5">Oops率</span>
<span class="font-semibold text-sm stat-value-oops text-orange-500">${oopsRate}%</span>
</div>
<div class="flex flex-col items-center">
<span class="opacity-60 mb-0.5">错误率</span>
<span class="font-semibold text-sm stat-value-wrong text-red-500">${wrongRate}%</span>
</div>
</div>
`;
navContainer.appendChild(card);
// 详情区域
const detail = document.createElement('div');
detail.className = 'lesson-detail' + (idx === 0 ? ' active' : '');
detail.id = `lesson-${idx}`;
const kp = lesson.knowledge_points || lesson.knowledgePoints || {};
const words = kp.words || [];
const patterns = kp.sentence_patterns || kp.sentencePatterns || [];
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
let html = `
<div class="mb-5">
<h3 class="text-sm font-semibold text-gray-500 mb-3">📗 单词 (${words.length}个)</h3>
<div class="flex flex-wrap gap-2">
${words.map(w => `<span class="inline-flex items-center gap-1.5 bg-accent-light text-sm rounded-lg px-3 py-1.5"><span class="font-semibold text-accent">${w.word}</span><span class="text-gray-500">${w.meaning}</span><span class="text-gray-400 text-xs">${w.pos || ''}</span></span>`).join('')}
</div>
</div>
<div class="mb-5">
<h3 class="text-sm font-semibold text-gray-500 mb-3">📘 句型 (${patterns.length}个)</h3>
<div class="grid sm:grid-cols-2 gap-2">
${patterns.map(p => `<div class="bg-amber-50 border border-amber-200 rounded-lg px-4 py-2.5 text-sm"><span class="font-semibold text-amber-600">${p.pattern}</span><span class="block text-gray-500 text-xs mt-1">${p.meaning}</span></div>`).join('')}
</div>
</div>
<div class="mb-5">
<h3 class="text-sm font-semibold text-gray-500 mb-3">🎯 互动组件 (${ics.length}个)</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="border-b-2 border-gray-100">
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs">#</th>
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs">题型</th>
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs">知识点</th>
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs hidden sm:table-cell">详情</th>
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs">结果</th>
</tr></thead>
<tbody>
${ics.map((ic, i) => {
const result = (ic.user_result || ic.userResult || '').toLowerCase();
const badgeColor = result === 'perfect' ? 'bg-emerald-50 text-emerald-600 border-emerald-200' : result === 'good' ? 'bg-blue-50 text-blue-600 border-blue-200' : 'bg-orange-50 text-orange-600 border-orange-200';
const kpText = ic.knowledge || '-';
const audioBtn = ic.audio ? `<span class="audio-btn opacity-60 ml-1.5 text-base" onclick="playAudio('${ic.audio}', this)" title="播放录音">🔊</span>` : '';
return `<tr class="border-b border-gray-50 hover:bg-gray-50/50"><td class="py-2 px-3 text-gray-400">${i + 1}</td><td class="py-2 px-3">${ic.type}</td><td class="py-2 px-3 text-gray-500">${kpText}</td><td class="py-2 px-3 text-gray-500 hidden sm:table-cell">${ic.detail}</td><td class="py-2 px-3"><span class="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full border ${badgeColor}">${result}</span>${audioBtn}</td></tr>`;
}).join('')}
</tbody>
</table>
</div>
</div>
<div class="mb-5">
<h3 class="text-sm font-semibold text-gray-500 mb-3">📝 学习巩固 (${ces.length}题)</h3>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr class="border-b-2 border-gray-100">
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs">#</th>
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs">题型</th>
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs">知识点</th>
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs hidden sm:table-cell">详情</th>
<th class="text-left py-2.5 px-3 font-semibold text-gray-400 bg-gray-50/60 text-xs">结果</th>
</tr></thead>
<tbody>
${ces.map((ce, i) => {
const correct = ce.is_correct === true || ce.isCorrect === true;
const badgeColor = correct ? 'bg-emerald-50 text-emerald-600 border-emerald-200' : 'bg-red-50 text-red-500 border-red-200';
const kpText = ce.knowledge || '-';
const audioBtn = ce.audio ? `<span class="audio-btn opacity-60 ml-1.5 text-base" onclick="playAudio('${ce.audio}', this)" title="播放录音">🔊</span>` : '';
return `<tr class="border-b border-gray-50 hover:bg-gray-50/50"><td class="py-2 px-3 text-gray-400">${i + 1}</td><td class="py-2 px-3">${ce.type}</td><td class="py-2 px-3 text-gray-500">${kpText}</td><td class="py-2 px-3 text-gray-500 hidden sm:table-cell">${ce.detail}</td><td class="py-2 px-3"><span class="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full border ${badgeColor}">${correct ? '✓ 正确' : '✗ 错误'}</span>${audioBtn}</td></tr>`;
}).join('')}
</tbody>
</table>
</div>
</div>
`;
detail.innerHTML = html;
detailContainer.appendChild(detail);
});
let currentAudio = null;
function playAudio(url, el) {
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
document.querySelectorAll('.audio-btn.playing').forEach(b => b.classList.remove('playing'));
}
const audio = new Audio(url);
currentAudio = audio;
el.classList.add('playing');
audio.play().catch(() => {});
audio.onended = () => { el.classList.remove('playing'); currentAudio = null; };
audio.onerror = () => { el.classList.remove('playing'); currentAudio = null; };
}
function switchTab(idx) {
document.querySelectorAll('.lesson-card').forEach((c, i) => c.classList.toggle('active', i === idx));
document.querySelectorAll('.lesson-detail').forEach((d, i) => d.classList.toggle('active', i === idx));
}
function formatTime(t) {
if (!t || t === '-') return '-';
try {
const d = new Date(t);
return d.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
} catch { return t; }
}
// ==================== 能力训练展示 ====================
const abilityTbody = document.getElementById('ability-training-tbody');
abilityTraining.forEach((at, idx) => {
const tr = document.createElement('tr');
tr.className = 'border-b border-gray-50 hover:bg-gray-50/50';
const result = (at.result || '').toLowerCase();
const badgeColor = result === 'perfect' ? 'bg-emerald-50 text-emerald-600 border-emerald-200' : result === 'good' ? 'bg-blue-50 text-blue-600 border-blue-200' : 'bg-orange-50 text-orange-600 border-orange-200';
const subQuestionInfo = `${at.sub_question_correct || 0}/${at.sub_question_count || 0}`;
const atAudioBtn = at.audio ? `<span class="audio-btn opacity-60 ml-1.5 text-base" onclick="playAudio('${at.audio}', this)" title="播放录音">🔊</span>` : '';
tr.innerHTML = `
<td class="py-2.5 px-3 text-gray-400">${idx + 1}</td>
<td class="py-2.5 px-3">${at.source || '-'}</td>
<td class="py-2.5 px-3">${at.type || '-'}</td>
<td class="py-2.5 px-3">${at.title || '-'}</td>
<td class="py-2.5 px-3 text-gray-500 hidden sm:table-cell">${at.detail || '-'}</td>
<td class="py-2.5 px-3">${subQuestionInfo}</td>
<td class="py-2.5 px-3"><span class="inline-block text-xs font-semibold px-2.5 py-0.5 rounded-full border ${badgeColor}">${result}</span>${atAudioBtn}</td>
`;
abilityTbody.appendChild(tr);
});
// 响应式
window.addEventListener('resize', () => {
interactiveChart.resize();
exerciseChart.resize();
abilityChart.resize();
});
</script>
</body>
</html>