402 lines
22 KiB
HTML
402 lines
22 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>学情分析报告 - 用户12345 Level1 Unit1</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;
|
||
gap: 20px;
|
||
}
|
||
@media (max-width: 768px) {
|
||
.chart-row { grid-template-columns: 1fr; }
|
||
}
|
||
.lesson-tab {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.lesson-tab .tab-btn {
|
||
padding: 8px 20px;
|
||
border: 2px solid #667eea;
|
||
border-radius: 20px;
|
||
background: white;
|
||
color: #667eea;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
.lesson-tab .tab-btn.active, .lesson-tab .tab-btn:hover {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
.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; }
|
||
.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:12345 | Level 1 | Unit 1 | 生成时间:2026-04-02 16:12:43
|
||
</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>
|
||
|
||
<div class="card">
|
||
<h2>📖 课程详情</h2>
|
||
<div class="lesson-tab" id="lesson-tabs"></div>
|
||
<div id="lesson-details"></div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>🔍 原始数据</h2>
|
||
<div class="raw-data" id="raw-data"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const data = {"role_id": 12345, "level": 1, "unit": 1, "unit_name": "Unit 1: Hello World", "lessons": [{"lesson_id": 1, "lesson_name": "Lesson 1: Greetings", "entry_time": "2024-01-15T09:00:00Z", "completion_time": "2024-01-15T09:25:30Z", "knowledge_points": {"words": [{"word": "hello", "meaning": "你好", "pos": "int."}, {"word": "hi", "meaning": "嗨", "pos": "int."}, {"word": "goodbye", "meaning": "再见", "pos": "int."}, {"word": "name", "meaning": "名字", "pos": "n."}], "sentence_patterns": [{"pattern": "Hello, my name is [name].", "meaning": "你好,我的名字是[名字]。", "example": "Hello, my name is Tom."}, {"pattern": "Nice to meet you.", "meaning": "很高兴认识你。", "example": "Nice to meet you too."}]}, "interactive_components": [{"component_id": "ic_001", "title": "单词跟读 - hello", "detail": "请跟读单词 hello,注意发音准确", "user_result": "perfect"}, {"component_id": "ic_002", "title": "单词跟读 - hi", "detail": "请跟读单词 hi,注意语调自然", "user_result": "good"}, {"component_id": "ic_003", "title": "单词听选 - goodbye", "detail": "听录音,选择正确的单词 goodbye", "user_result": "perfect"}, {"component_id": "ic_004", "title": "单词拼写 - name", "detail": "根据发音,拼写出单词 name", "user_result": "good"}, {"component_id": "ic_005", "title": "句型跟读 - Hello, my name is...", "detail": "请跟读句型:Hello, my name is [name]", "user_result": "perfect"}, {"component_id": "ic_006", "title": "句型替换练习", "detail": "替换名字,完成句子:Hello, my name is ______", "user_result": "oops"}, {"component_id": "ic_007", "title": "情景对话 - 打招呼", "detail": "在情景中与角色进行打招呼对话", "user_result": "good"}, {"component_id": "ic_008", "title": "听力理解 - 问候语", "detail": "听录音,选择正确的问候回应", "user_result": "perfect"}, {"component_id": "ic_009", "title": "语音识别 - hi", "detail": "请说出单词 hi", "user_result": "good"}, {"component_id": "ic_010", "title": "图文匹配 - 单词", "detail": "将单词与对应的图片进行匹配", "user_result": "perfect"}, {"component_id": "ic_011", "title": "句型听选 - Nice to meet you", "detail": "听录音,选择正确的回应", "user_result": "good"}, {"component_id": "ic_012", "title": "角色扮演 - 自我介绍", "detail": "扮演角色进行自我介绍对话", "user_result": "perfect"}, {"component_id": "ic_013", "title": "单词排序 - 句子组成", "detail": "将单词按正确顺序排列成句子", "user_result": "oops"}, {"component_id": "ic_014", "title": "发音练习 - 语调训练", "detail": "练习疑问句和陈述句的语调区别", "user_result": "good"}, {"component_id": "ic_015", "title": "单词填空 - 补全句子", "detail": "在句子中填入正确的单词", "user_result": "perfect"}, {"component_id": "ic_016", "title": "情景判断 - 选择回应", "detail": "根据情景选择最合适的回应", "user_result": "good"}, {"component_id": "ic_017", "title": "综合对话 - 完整对话", "detail": "完成完整的打招呼和自我介绍对话", "user_result": "perfect"}, {"component_id": "ic_018", "title": "复习测验 - 本课回顾", "detail": "完成本课知识点的综合复习测验", "user_result": "good"}], "consolidation_exercises": [{"exercise_id": "ce_001", "type": "单词听选", "detail": "听录音,从A、B、C三个选项中选择听到的单词", "is_correct": true}, {"exercise_id": "ce_002", "type": "单词拼写", "detail": "根据中文意思,写出对应的英文单词:你好", "is_correct": true}, {"exercise_id": "ce_003", "type": "图文匹配", "detail": "将单词 goodbye 与对应的图片匹配", "is_correct": false}, {"exercise_id": "ce_004", "type": "句型填空", "detail": "用正确的单词填空:Hello, my ______ is Tom.", "is_correct": true}, {"exercise_id": "ce_005", "type": "情景选择", "detail": "初次见面时,应该说:A. Hello B. Goodbye C. Hi", "is_correct": true}, {"exercise_id": "ce_006", "type": "句子排序", "detail": "将下列单词排列成正确的句子:name / my / is / Hello", "is_correct": true}, {"exercise_id": "ce_007", "type": "听力理解", "detail": "听对话,回答问题:他们正在做什么?", "is_correct": false}, {"exercise_id": "ce_008", "type": "发音选择", "detail": "听录音,选择包含 /eɪ/ 音的单词", "is_correct": true}, {"exercise_id": "ce_009", "type": "翻译练习", "detail": "将下列句子翻译成英文:很高兴认识你。", "is_correct": true}, {"exercise_id": "ce_010", "type": "对话补全", "detail": "补全对话:A: Hello! B: ______!", "is_correct": true}, {"exercise_id": "ce_011", "type": "单词分类", "detail": "将单词按问候语和非问候语分类", "is_correct": false}, {"exercise_id": "ce_012", "type": "句型转换", "detail": "将 Hello 转换为同义词", "is_correct": true}, {"exercise_id": "ce_013", "type": "连线匹配", "detail": "将单词与对应的中文意思连线", "is_correct": true}, {"exercise_id": "ce_014", "type": "判断正误", "detail": "判断句子是否正确:Goodbye 用于告别时说。", "is_correct": true}, {"exercise_id": "ce_015", "type": "选词填空", "detail": "选择合适的单词填空:______ to meet you. (Nice/Fine/Good)", "is_correct": true}, {"exercise_id": "ce_016", "type": "语音辨识", "detail": "听录音,判断是哪个单词", "is_correct": false}, {"exercise_id": "ce_017", "type": "完形填空", "detail": "阅读短文,选择合适的问候语填入空格", "is_correct": true}, {"exercise_id": "ce_018", "type": "综合测验", "detail": "完成本课知识的综合测验题", "is_correct": true}]}], "summary": {"total_lessons": 5, "completed_lessons": 1, "total_interactive_components": 18, "total_consolidation_exercises": 18, "mastery_rate": 0.85, "study_duration_minutes": 25}};
|
||
|
||
// 兼容: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, totalPerfect = 0, totalGood = 0, totalOops = 0;
|
||
let totalExercises = 0, totalCorrect = 0;
|
||
|
||
lessons.forEach(lesson => {
|
||
const ics = lesson.interactive_components || lesson.interactiveComponents || [];
|
||
const ces = lesson.consolidation_exercises || lesson.consolidationExercises || [];
|
||
totalInteractive += ics.length;
|
||
ics.forEach(ic => {
|
||
const r = (ic.user_result || ic.userResult || '').toLowerCase();
|
||
if (r === 'perfect') totalPerfect++;
|
||
else if (r === 'good') totalGood++;
|
||
else if (r === 'oops') totalOops++;
|
||
});
|
||
totalExercises += ces.length;
|
||
ces.forEach(ce => {
|
||
if (ce.is_correct === true || ce.isCorrect === true) totalCorrect++;
|
||
});
|
||
});
|
||
|
||
const exerciseAccuracy = totalExercises > 0 ? Math.round(totalCorrect / totalExercises * 100) : 0;
|
||
const interactiveScore = totalInteractive > 0 ? Math.round((totalPerfect * 3 + totalGood * 2 + totalOops * 1) / (totalInteractive * 3) * 100) : 0;
|
||
|
||
const statsItems = [
|
||
{ label: '课程数', value: totalLessons, cls: 'blue' },
|
||
{ label: '互动组件总数', value: totalInteractive, cls: 'purple' },
|
||
{ label: 'Perfect 次数', value: totalPerfect, cls: 'green' },
|
||
{ label: 'Good 次数', value: totalGood, cls: 'blue' },
|
||
{ label: 'Oops 次数', value: totalOops, cls: 'orange' },
|
||
{ label: '巩固题总数', value: totalExercises, cls: 'purple' },
|
||
{ label: '巩固正确率', value: exerciseAccuracy + '%', cls: 'green' },
|
||
{ label: '互动综合得分', value: interactiveScore + '%', cls: 'blue' }
|
||
];
|
||
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'));
|
||
interactiveChart.setOption({
|
||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||
legend: { bottom: 10 },
|
||
color: ['#52c41a', '#1890ff', '#fa541c'],
|
||
series: [{
|
||
type: 'pie',
|
||
radius: ['40%', '65%'],
|
||
avoidLabelOverlap: true,
|
||
itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 },
|
||
label: { show: true, fontSize: 14 },
|
||
data: [
|
||
{ value: totalPerfect, name: 'Perfect' },
|
||
{ value: totalGood, name: 'Good' },
|
||
{ value: totalOops, name: 'Oops' }
|
||
]
|
||
}]
|
||
});
|
||
|
||
// ==================== 巩固练习柱状图(按课程) ====================
|
||
const exerciseChart = echarts.init(document.getElementById('exercise-chart'));
|
||
const lessonLabels = lessons.map((l, i) => l.lesson_name || l.lessonName || `Lesson ${i + 1}`);
|
||
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 },
|
||
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: [4, 4, 0, 0] } },
|
||
{ name: '错误', type: 'bar', stack: 'total', data: wrongCounts, itemStyle: { borderRadius: [4, 4, 0, 0] } }
|
||
]
|
||
});
|
||
|
||
// ==================== 课程详情 tabs ====================
|
||
const tabContainer = document.getElementById('lesson-tabs');
|
||
const detailContainer = document.getElementById('lesson-details');
|
||
|
||
lessons.forEach((lesson, idx) => {
|
||
// tab 按钮
|
||
const btn = document.createElement('button');
|
||
btn.className = 'tab-btn' + (idx === 0 ? ' active' : '');
|
||
btn.textContent = lesson.lesson_name || lesson.lessonName || `Lesson ${idx + 1}`;
|
||
btn.onclick = () => switchTab(idx);
|
||
tabContainer.appendChild(btn);
|
||
|
||
// 详情区域
|
||
const detail = document.createElement('div');
|
||
detail.className = 'lesson-detail' + (idx === 0 ? ' active' : '');
|
||
detail.id = `lesson-${idx}`;
|
||
|
||
const entryTime = lesson.entry_time || lesson.entryTime || '-';
|
||
const completionTime = lesson.completion_time || lesson.completionTime || '-';
|
||
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="time-info">
|
||
<span>🕐 进入时间:${formatTime(entryTime)}</span>
|
||
<span>✅ 完成时间:${formatTime(completionTime)}</span>
|
||
</div>
|
||
<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></tr></thead>
|
||
<tbody>
|
||
${ics.map((ic, i) => {
|
||
const result = (ic.user_result || ic.userResult || '').toLowerCase();
|
||
const cls = result === 'perfect' ? 'perfect' : result === 'good' ? 'good' : 'oops';
|
||
return `<tr><td>${i + 1}</td><td>${ic.title}</td><td>${ic.detail}</td><td><span class="badge ${cls}">${result}</span></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></tr></thead>
|
||
<tbody>
|
||
${ces.map((ce, i) => {
|
||
const correct = ce.is_correct === true || ce.isCorrect === true;
|
||
return `<tr><td>${i + 1}</td><td>${ce.type}</td><td>${ce.detail}</td><td><span class="badge ${correct ? 'correct' : 'wrong'}">${correct ? '✓ 正确' : '✗ 错误'}</span></td></tr>`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
detail.innerHTML = html;
|
||
detailContainer.appendChild(detail);
|
||
});
|
||
|
||
function switchTab(idx) {
|
||
document.querySelectorAll('.tab-btn').forEach((b, i) => b.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; }
|
||
}
|
||
|
||
// 原始数据
|
||
document.getElementById('raw-data').textContent = JSON.stringify(data, null, 2);
|
||
|
||
// 响应式
|
||
window.addEventListener('resize', () => {
|
||
interactiveChart.resize();
|
||
exerciseChart.resize();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|