466 lines
26 KiB
HTML
466 lines
26 KiB
HTML
<!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>
|