ai_member_xiaoban/skills/study-analysis/assets/template copy.html
2026-04-04 08:00:09 +08:00

631 lines
27 KiB
HTML
Raw Permalink 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>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
background-color: #f0f2f5;
padding: 24px;
color: #333;
}
.container { max-width: 1200px; margin: 0 auto; }
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 32px;
border-radius: 16px;
margin-bottom: 24px;
}
.header h1 { font-size: 28px; margin-bottom: 8px; }
.header .meta { font-size: 14px; opacity: 0.9; }
.card {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.card h2 {
font-size: 18px;
color: #333;
margin-bottom: 20px;
border-left: 4px solid #667eea;
padding-left: 12px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-item {
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
padding: 20px;
border-radius: 12px;
text-align: center;
}
.stat-item .label { font-size: 13px; color: #888; margin-bottom: 8px; }
.stat-item .value { font-size: 28px; font-weight: 700; color: #333; }
.stat-item .value.green { color: #52c41a; }
.stat-item .value.orange { color: #fa8c16; }
.stat-item .value.blue { color: #1890ff; }
.stat-item .value.purple { color: #722ed1; }
.chart-container { height: 400px; width: 100%; }
.chart-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
@media (max-width: 1024px) {
.chart-row { grid-template-columns: 1fr; }
}
.lesson-nav {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.lesson-card {
background: white;
border: 2px solid #e8e8e8;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.lesson-card:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
}
.lesson-card.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.lesson-card .lesson-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
}
.lesson-card .lesson-time {
font-size: 13px;
opacity: 0.9;
line-height: 1.6;
}
.lesson-card .lesson-time .time-label {
opacity: 0.7;
}
.lesson-card .lesson-stats {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(102, 126, 234, 0.2);
font-size: 13px;
}
.lesson-card.active .lesson-stats {
border-top-color: rgba(255, 255, 255, 0.3);
}
.lesson-card .lesson-stats .stat {
display: flex;
flex-direction: column;
align-items: center;
}
.lesson-card .lesson-stats .stat-label {
font-size: 11px;
opacity: 0.7;
margin-bottom: 2px;
}
.lesson-card .lesson-stats .stat-value {
font-weight: 600;
font-size: 14px;
}
.lesson-card .lesson-stats .stat-value.oops { color: #fa541c; }
.lesson-card .lesson-stats .stat-value.wrong { color: #f5222d; }
.lesson-card.active .lesson-stats .stat-value.oops,
.lesson-card.active .lesson-stats .stat-value.wrong { color: #ffccc7; }
.lesson-detail { display: none; }
.lesson-detail.active { display: block; }
.knowledge-section { margin-bottom: 20px; }
.knowledge-section h3 {
font-size: 15px;
color: #555;
margin-bottom: 12px;
}
.word-list, .pattern-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.word-tag {
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
}
.word-tag .en { font-weight: 600; color: #1890ff; }
.word-tag .cn { color: #666; margin-left: 6px; }
.word-tag .pos { color: #999; font-size: 12px; margin-left: 4px; }
.pattern-tag {
background: #fff7e6;
border: 1px solid #ffd591;
border-radius: 8px;
padding: 10px 16px;
font-size: 14px;
flex: 1 1 300px;
}
.pattern-tag .en { font-weight: 600; color: #fa8c16; }
.pattern-tag .cn { color: #666; display: block; margin-top: 4px; font-size: 13px; }
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
table th {
background: #f5f7fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #555;
border-bottom: 2px solid #e8e8e8;
}
table td {
padding: 10px 12px;
border-bottom: 1px solid #f0f0f0;
}
table tr:hover td { background: #fafafa; }
.badge {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
}
.badge.perfect { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.badge.good { background: #e6f7ff; color: #1890ff; border: 1px solid #91d5ff; }
.badge.oops { background: #fff2e8; color: #fa541c; border: 1px solid #ffbb96; }
.badge.correct { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.badge.wrong { background: #fff1f0; color: #f5222d; border: 1px solid #ffa39e; }
.audio-btn {
cursor: pointer;
margin-left: 6px;
font-size: 16px;
vertical-align: middle;
opacity: 0.7;
transition: opacity 0.2s;
}
.audio-btn:hover { opacity: 1; }
.audio-btn.playing { opacity: 1; color: #1890ff; }
.time-info {
display: flex;
gap: 20px;
margin-bottom: 16px;
font-size: 14px;
color: #666;
}
.time-info span { display: flex; align-items: center; gap: 6px; }
.raw-data {
background: #f8f9fa;
padding: 16px;
border-radius: 8px;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 12px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📚 学情分析报告</h1>
<div class="meta">
用户ID{{ROLE_ID}} &nbsp;|&nbsp; Level {{LEVEL}} &nbsp;|&nbsp; Unit {{UNIT}} &nbsp;|&nbsp; 生成时间:{{GENERATE_TIME}}
</div>
</div>
<div class="card">
<h2>📊 总览</h2>
<div class="stats-grid" id="stats-grid"></div>
</div>
<div class="chart-row">
<div class="card">
<h2>📈 互动组件表现</h2>
<div id="interactive-chart" class="chart-container"></div>
</div>
<div class="card">
<h2>📝 巩固练习正确率</h2>
<div id="exercise-chart" class="chart-container"></div>
</div>
<div class="card">
<h2>🎯 能力训练正确率</h2>
<div id="ability-chart" class="chart-container"></div>
</div>
</div>
<div class="card">
<h2>📖 课程详情</h2>
<div class="lesson-nav" id="lesson-nav"></div>
<div id="lesson-details"></div>
</div>
<div class="card">
<h2>🎯 能力训练</h2>
<table id="ability-training-table">
<thead>
<tr>
<th>#</th>
<th>来源</th>
<th>题型</th>
<th>标题</th>
<th>题目详情</th>
<th>子题目情况</th>
<th>结果</th>
</tr>
</thead>
<tbody id="ability-training-tbody"></tbody>
</table>
</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, cls: 'purple' },
{ label: '总互动次数', value: totalInteractive, cls: 'blue' },
{ label: '总练习题数', value: totalExercises, cls: 'green' },
{ label: '单元长度', value: unitLengthStr, cls: 'orange' }
];
statsItems.forEach(item => {
const div = document.createElement('div');
div.className = 'stat-item';
div.innerHTML = `<div class="label">${item.label}</div><div class="value ${item.cls}">${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'] },
grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true },
xAxis: { type: 'category', data: lessonLabels, axisLabel: { rotate: 20, fontSize: 12 } },
yAxis: { type: 'value' },
color: ['#52c41a', '#1890ff', '#fa541c'],
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: ['正确', '错误'] },
grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true },
xAxis: { type: 'category', data: lessonLabels, axisLabel: { rotate: 20, fontSize: 12 } },
yAxis: { type: 'value' },
color: ['#52c41a', '#f5222d'],
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'] },
color: ['#52c41a', '#1890ff', '#fa541c'],
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' + (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="lesson-title">${lessonName}</div>
<div class="lesson-time">
<div><span class="time-label">进入:</span>${formatTime(entryTime)}</div>
<div><span class="time-label">完成:</span>${formatTime(completionTime)}</div>
</div>
<div class="lesson-stats">
<div class="stat">
<span class="stat-label">Oops率</span>
<span class="stat-value oops">${oopsRate}%</span>
</div>
<div class="stat">
<span class="stat-label">错误率</span>
<span class="stat-value wrong">${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="knowledge-section">
<h3>📗 单词 (${words.length}个)</h3>
<div class="word-list">
${words.map(w => `<div class="word-tag"><span class="en">${w.word}</span><span class="cn">${w.meaning}</span><span class="pos">${w.pos || ''}</span></div>`).join('')}
</div>
</div>
<div class="knowledge-section">
<h3>📘 句型 (${patterns.length}个)</h3>
<div class="pattern-list">
${patterns.map(p => `<div class="pattern-tag"><span class="en">${p.pattern}</span><span class="cn">${p.meaning}</span></div>`).join('')}
</div>
</div>
<div class="knowledge-section">
<h3>🎯 互动组件 (${ics.length}个)</h3>
<table>
<thead><tr><th>#</th><th>题型</th><th>知识点</th><th>详情</th><th>结果</th></tr></thead>
<tbody>
${ics.map((ic, i) => {
const result = (ic.user_result || ic.userResult || '').toLowerCase();
const cls = result === 'perfect' ? 'perfect' : result === 'good' ? 'good' : 'oops';
const kpText = ic.knowledge || '-';
const audioBtn = ic.audio ? `<span class="audio-btn" onclick="playAudio('${ic.audio}', this)" title="播放录音">🔊</span>` : '';
return `<tr><td>${i + 1}</td><td>${ic.type}</td><td>${kpText}</td><td>${ic.detail}</td><td><span class="badge ${cls}">${result}</span>${audioBtn}</td></tr>`;
}).join('')}
</tbody>
</table>
</div>
<div class="knowledge-section">
<h3>📝 学习巩固 (${ces.length}题)</h3>
<table>
<thead><tr><th>#</th><th>题型</th><th>知识点</th><th>详情</th><th>结果</th></tr></thead>
<tbody>
${ces.map((ce, i) => {
const correct = ce.is_correct === true || ce.isCorrect === true;
const kpText = ce.knowledge || '-';
const audioBtn = ce.audio ? `<span class="audio-btn" onclick="playAudio('${ce.audio}', this)" title="播放录音">🔊</span>` : '';
return `<tr><td>${i + 1}</td><td>${ce.type}</td><td>${kpText}</td><td>${ce.detail}</td><td><span class="badge ${correct ? 'correct' : 'wrong'}">${correct ? '✓ 正确' : '✗ 错误'}</span>${audioBtn}</td></tr>`;
}).join('')}
</tbody>
</table>
</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');
const result = (at.result || '').toLowerCase();
const badgeCls = result === 'perfect' ? 'perfect' : result === 'good' ? 'good' : 'oops';
const subQuestionInfo = `${at.sub_question_correct || 0}/${at.sub_question_count || 0}`;
const atAudioBtn = at.audio ? `<span class="audio-btn" onclick="playAudio('${at.audio}', this)" title="播放录音">🔊</span>` : '';
tr.innerHTML = `
<td>${idx + 1}</td>
<td>${at.source || '-'}</td>
<td>${at.type || '-'}</td>
<td>${at.title || '-'}</td>
<td>${at.detail || '-'}</td>
<td>${subQuestionInfo}</td>
<td><span class="badge ${badgeCls}">${result}</span>${atAudioBtn}</td>
`;
abilityTbody.appendChild(tr);
});
// 响应式
window.addEventListener('resize', () => {
interactiveChart.resize();
exerciseChart.resize();
abilityChart.resize();
});
</script>
</body>
</html>