math-tutor/templates/quiz.html

449 lines
12 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>📜 历史小卧龙 — 背诵大挑战</title>
<style>
:root {
--bg: #FFF8F0;
--card: #FFFFFF;
--primary: #E67E22;
--secondary: #27AE60;
--accent: #8E44AD;
--danger: #E74C3C;
--text: #2D3436;
--light: #F8F9FA;
--border: #E9ECEF;
--correct: #27AE60;
--wrong: #E74C3C;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
header {
background: linear-gradient(135deg, var(--primary), #D35400);
color: white;
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
header .logo { font-size: 28px; }
header h1 { font-size: 18px; font-weight: 600; }
header .stats { font-size: 13px; opacity: 0.9; }
header a { color: white; text-decoration: none; opacity: 0.85; font-size: 13px; }
/* 单元选择器 */
.unit-selector {
display: flex;
gap: 8px;
padding: 12px 16px;
overflow-x: auto;
background: white;
border-bottom: 1px solid var(--border);
}
.unit-btn {
padding: 6px 14px;
border: 2px solid var(--border);
border-radius: 20px;
background: white;
cursor: pointer;
font-size: 13px;
white-space: nowrap;
transition: all 0.2s;
}
.unit-btn:hover { border-color: var(--primary); color: var(--primary); }
.unit-btn.active { background: var(--primary); color: white; border-color: var(--primary); }
.unit-btn .count { font-size: 11px; opacity: 0.7; margin-left: 4px; }
/* 主区域 */
main {
max-width: 680px;
margin: 0 auto;
padding: 20px 16px;
}
/* 题目卡片 */
.question-card {
background: var(--card);
border-radius: 16px;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
padding: 24px;
margin-bottom: 16px;
transition: transform 0.2s;
}
.question-card.correct {
border-left: 4px solid var(--correct);
background: #F0FFF0;
}
.question-card.wrong {
border-left: 4px solid var(--wrong);
background: #FFF5F5;
}
.question-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 13px;
color: #888;
}
.question-meta .type-badge {
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.type-short_answer { background: #EBF5FB; color: #2980B9; }
.type-fill_blank { background: #FEF9E7; color: #F39C12; }
.question-text {
font-size: 18px;
font-weight: 600;
line-height: 1.6;
margin-bottom: 16px;
color: var(--text);
}
.answer-input {
display: flex;
gap: 8px;
}
.answer-input input {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 16px;
font-family: inherit;
transition: border-color 0.2s;
}
.answer-input input:focus { outline: none; border-color: var(--primary); }
.answer-input button {
padding: 12px 24px;
background: var(--primary);
color: white;
border: none;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s;
}
.answer-input button:hover { transform: scale(1.03); }
.answer-input button:active { transform: scale(0.97); }
/* 反馈区 */
.feedback {
margin-top: 14px;
padding: 12px 16px;
border-radius: 10px;
font-size: 15px;
line-height: 1.5;
display: none;
}
.feedback.show { display: block; }
.feedback.correct-fb {
background: #D5F5E3;
color: #1E8449;
}
.feedback.wrong-fb {
background: #FADBD8;
color: #922B21;
}
.feedback .correct-answer {
font-weight: 600;
margin-top: 6px;
}
.feedback .hint {
font-style: italic;
color: #888;
margin-top: 6px;
}
/* 操作栏 */
.actions {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
.actions button {
padding: 10px 28px;
border-radius: 10px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-next { background: var(--secondary); color: white; }
.btn-next:hover { transform: scale(1.03); }
.btn-hint { background: #FEF3C7; color: #92400E; border: 1px solid #FCD34D !important; }
.btn-skip { background: var(--light); color: #666; border: 1px solid var(--border) !important; }
/* 进度条 */
.progress-bar {
width: 100%;
height: 6px;
background: var(--border);
border-radius: 3px;
margin-top: 20px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--secondary));
border-radius: 3px;
transition: width 0.5s ease;
}
/* 统计面板 */
.stats-panel {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 16px;
}
.stat-card {
background: white;
border-radius: 10px;
padding: 12px;
text-align: center;
box-shadow: 0 1px 4px rgba(0,0,0,0.05);
}
.stat-card .num { font-size: 24px; font-weight: 700; }
.stat-card .label { font-size: 12px; color: #888; margin-top: 2px; }
.stat-correct .num { color: var(--correct); }
.stat-wrong .num { color: var(--wrong); }
.stat-total .num { color: var(--primary); }
/* 批量模式 */
.batch-info {
text-align: center;
font-size: 14px;
color: #888;
margin-bottom: 12px;
}
/* 动画 */
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.question-card { animation: fadeIn 0.3s ease; }
@media (max-width: 600px) {
main { padding: 12px; }
.question-text { font-size: 16px; }
.stats-panel { grid-template-columns: repeat(3, 1fr); gap: 6px; }
}
</style>
</head>
<body>
<header>
<div style="display:flex;align-items:center;gap:10px;">
<span class="logo">📜</span>
<h1>历史小卧龙 — 背诵大挑战</h1>
</div>
<div class="stats" id="headerStats">已答 0 题</div>
</header>
<div class="unit-selector" id="unitSelector">
<button class="unit-btn active" data-unit="">全部单元 <span class="count"></span></button>
</div>
<main>
<div class="stats-panel">
<div class="stat-card stat-correct"><div class="num" id="correctCount">0</div><div class="label">✅ 答对</div></div>
<div class="stat-card stat-wrong"><div class="num" id="wrongCount">0</div><div class="label">❌ 答错</div></div>
<div class="stat-card stat-total"><div class="num" id="totalCount">0</div><div class="label">📊 总题</div></div>
</div>
<div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
<div id="questionArea" style="margin-top:20px;"></div>
<div class="actions">
<button class="btn-hint" id="btnHint" onclick="showHint()">💡 提示</button>
<button class="btn-skip" id="btnSkip" onclick="nextQuestion()">⏭️ 跳过</button>
<button class="btn-next" id="btnNext" onclick="nextQuestion()" style="display:none;">下一题 →</button>
</div>
</main>
<script>
const API = '/quiz';
let currentUnit = '';
let currentQuestion = null;
let stats = { correct: 0, wrong: 0, total: 0 };
let answered = false;
// 加载单元列表
async function loadUnits() {
try {
const resp = await fetch(API + '/units');
const data = await resp.json();
const selector = document.getElementById('unitSelector');
const totalBtn = selector.querySelector('.unit-btn');
let totalCount = 0;
data.units.forEach(u => {
totalCount += u.question_count;
const btn = document.createElement('button');
btn.className = 'unit-btn';
btn.dataset.unit = u.unit_id;
btn.innerHTML = u.alias + ' <span class="count">(' + u.question_count + ')</span>';
btn.onclick = () => selectUnit(u.unit_id, btn);
selector.appendChild(btn);
});
totalBtn.querySelector('.count').textContent = '(' + totalCount + ')';
} catch(e) {
console.error('加载单元失败:', e);
}
}
function selectUnit(unitId, btn) {
document.querySelectorAll('.unit-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentUnit = unitId;
nextQuestion();
}
// 获取随机题目
async function nextQuestion() {
answered = false;
document.getElementById('btnNext').style.display = 'none';
document.getElementById('btnHint').style.display = '';
document.getElementById('btnSkip').style.display = '';
try {
const url = API + '/random' + (currentUnit ? '?unit=' + currentUnit : '');
const resp = await fetch(url);
const data = await resp.json();
if (data.error) {
document.getElementById('questionArea').innerHTML = '<div class="question-card"><p>没有找到题目 😅</p></div>';
return;
}
currentQuestion = data;
renderQuestion(data);
} catch(e) {
document.getElementById('questionArea').innerHTML = '<div class="question-card"><p>加载失败,请重试</p></div>';
}
}
function renderQuestion(q) {
const typeLabel = q.type === 'short_answer' ? '名词解释' : '填空题';
const typeClass = 'type-' + q.type;
document.getElementById('questionArea').innerHTML = `
<div class="question-card" id="qCard">
<div class="question-meta">
<span class="type-badge ${typeClass}">${typeLabel}</span>
<span>${q.unit_alias || q.unit_id}</span>
</div>
<div class="question-text">${q.question}</div>
<div class="answer-input">
<input type="text" id="answerInput" placeholder="输入你的答案..."
onkeydown="if(event.key==='Enter')submitAnswer()" autofocus>
<button onclick="submitAnswer()">提交</button>
</div>
<div class="feedback" id="feedback"></div>
</div>`;
document.getElementById('answerInput').focus();
}
// 提交答案
async function submitAnswer() {
if (answered || !currentQuestion) return;
const input = document.getElementById('answerInput');
const studentAnswer = input.value.trim();
if (!studentAnswer) return;
answered = true;
stats.total++;
// 本地判断(与后端逻辑一致)
const correct = currentQuestion.answer.trim();
let isCorrect = false;
let matchType = 'none';
if (studentAnswer === correct) {
isCorrect = true; matchType = 'exact';
} else if (studentAnswer.length >= 2 && (studentAnswer.includes(correct) || correct.includes(studentAnswer))) {
isCorrect = true; matchType = 'partial';
}
if (isCorrect) {
stats.correct++;
showFeedback(true, correct, matchType);
} else {
stats.wrong++;
const hint = correct.length > 1
? '提示:答案以「' + correct[0] + '」开头,共' + correct.length + '个字'
: '';
showFeedback(false, correct, '', hint);
}
updateStats();
// 后台同步异步不阻塞UI
fetch(API + '/check', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({question_id: currentQuestion.id, answer: studentAnswer})
}).catch(() => {});
}
function showFeedback(correct, answer, matchType, hint) {
const card = document.getElementById('qCard');
const fb = document.getElementById('feedback');
card.classList.add(correct ? 'correct' : 'wrong');
fb.className = 'feedback show ' + (correct ? 'correct-fb' : 'wrong-fb');
if (correct) {
fb.innerHTML = '✅ 正确!' + (matchType === 'exact' ? '完美!' : '基本正确(' + matchType + '')
+ '<div class="correct-answer">标准答案:' + answer + '</div>';
} else {
fb.innerHTML = '❌ 再想想~'
+ '<div class="correct-answer">正确答案:' + answer + '</div>'
+ (hint ? '<div class="hint">' + hint + '</div>' : '');
}
document.getElementById('answerInput').disabled = true;
document.getElementById('btnHint').style.display = 'none';
document.getElementById('btnSkip').style.display = 'none';
document.getElementById('btnNext').style.display = '';
}
function showHint() {
if (!currentQuestion || answered) return;
const correct = currentQuestion.answer.trim();
const hint = correct.length > 1
? '💡 答案以「' + correct[0] + '」开头,共' + correct.length + '个字'
: '💡 答案很短,再想想?';
const fb = document.getElementById('feedback');
fb.className = 'feedback show';
fb.style.background = '#FEF3C7';
fb.style.color = '#92400E';
fb.innerHTML = hint;
}
function updateStats() {
document.getElementById('correctCount').textContent = stats.correct;
document.getElementById('wrongCount').textContent = stats.wrong;
document.getElementById('totalCount').textContent = stats.total;
document.getElementById('headerStats').textContent = '已答 ' + stats.total + ' 题';
const pct = stats.total > 0 ? Math.round(stats.correct / stats.total * 100) : 0;
document.getElementById('progressFill').style.width = pct + '%';
}
// 启动
loadUnits();
nextQuestion();
</script>
</body>
</html>