feat: 前端优化 - 明暗模式、题型Tab分隔、移除统计

- 支持明暗主题切换,用户选择持久化到 localStorage
- 题型通过 Tab 按钮分隔展示,支持过滤
- 移除正确/错误统计和进度条
- 题型颜色通过 CSS 变量集中管理(--type-*)
- JS 抽取 QUESTION_TYPES 常量,便于扩展
- 更新架构文档反映上述变更

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-02 18:40:50 +08:00
parent d3cba0e23c
commit 273b88219a
2 changed files with 130 additions and 77 deletions
+93 -52
View File
@@ -5,19 +5,40 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🎯 CS 知识应试强化</title>
<style>
:root {
:root, :root[data-theme="dark"] {
--bg: #0f172a; --surface: #1e293b; --surface2: #334155;
--text: #e2e8f0; --text2: #94a3b8; --accent: #6366f1;
--accent2: #818cf8; --green: #22c55e; --red: #ef4444;
--yellow: #eab308; --radius: 12px;
--header-bg: linear-gradient(135deg, #312e81, #1e1b4b);
}
:root[data-theme="light"] {
--bg: #f1f5f9; --surface: #ffffff; --surface2: #e2e8f0;
--text: #1e293b; --text2: #64748b; --accent: #6366f1;
--accent2: #4f46e5; --green: #16a34a; --red: #dc2626;
--yellow: #ca8a04; --radius: 12px;
--header-bg: linear-gradient(135deg, #6366f1, #4f46e5);
}
/* Question type colors */
:root {
--type-fill_blank: #818cf8;
--type-single_choice: #22c55e;
--type-multiple_choice: #ec4899;
--type-true_false: #eab308;
--type-short_answer: #3b82f6;
--type-code_reading: #f97316;
--type-scenario: #a855f7;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; min-height: 100vh; }
/* Header */
.header { background: linear-gradient(135deg, #312e81, #1e1b4b); padding: 28px 24px; text-align: center; border-bottom: 1px solid var(--surface2); }
.header { background: var(--header-bg); padding: 28px 24px; text-align: center; border-bottom: 1px solid var(--surface2); position: relative; }
.header h1 { font-size: 26px; font-weight: 700; margin-bottom: 6px; }
.header p { color: var(--text2); font-size: 14px; }
.header p { color: rgba(255,255,255,0.7); font-size: 14px; }
.theme-toggle { position: absolute; top: 16px; right: 16px; background: rgba(255,255,255,0.15); border: none; color: white; width: 36px; height: 36px; border-radius: 8px; cursor: pointer; font-size: 18px; transition: background 0.2s; }
.theme-toggle:hover { background: rgba(255,255,255,0.25); }
/* Layout */
.app { display: flex; min-height: calc(100vh - 100px); }
@@ -40,14 +61,14 @@ body { font-family: -apple-system, 'Segoe UI', sans-serif; background: var(--bg)
.import-btn { width: 100%; padding: 10px; border-radius: 8px; border: 1px dashed var(--surface2); background: transparent; color: var(--text2); cursor: pointer; font-size: 13px; transition: all 0.2s; }
.import-btn:hover { border-color: var(--accent); color: var(--accent); }
/* Tabs */
.tabs { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 20px; padding: 4px; background: var(--surface); border-radius: var(--radius); }
.tab { padding: 8px 16px; border-radius: 8px; border: none; background: transparent; color: var(--text2); cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.2s; }
.tab:hover { background: var(--surface2); color: var(--text); }
.tab.active { background: var(--accent); color: white; }
/* Toolbar */
.toolbar { display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between; align-items: center; padding: 16px 20px; background: var(--surface); border-radius: var(--radius); margin-bottom: 20px; }
.stats { display: flex; gap: 16px; font-size: 13px; color: var(--text2); }
.stats span { display: flex; align-items: center; gap: 4px; }
.stats .num { font-weight: 700; font-size: 16px; }
.stats .num.green { color: var(--green); }
.stats .num.red { color: var(--red); }
.stats .num.yellow { color: var(--yellow); }
.toolbar { display: flex; flex-wrap: wrap; gap: 12px; justify-content: flex-end; align-items: center; margin-bottom: 16px; }
.toolbar-actions { display: flex; gap: 8px; }
/* Buttons */
@@ -58,20 +79,20 @@ body { font-family: -apple-system, 'Segoe UI', sans-serif; background: var(--bg)
.btn-outline:hover { border-color: var(--accent); color: var(--accent); }
.btn-sm { padding: 5px 12px; font-size: 12px; }
/* Progress */
.progress-bar { height: 4px; background: var(--surface2); border-radius: 2px; margin-bottom: 20px; overflow: hidden; }
.progress-bar .fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--green)); transition: width 0.3s ease; }
/* Question Card */
.q-card { background: var(--surface); border-radius: var(--radius); padding: 20px; margin-bottom: 16px; border: 1px solid var(--surface2); transition: border-color 0.3s; }
.q-card.correct { border-color: var(--green); }
.q-card.wrong { border-color: var(--red); }
.q-card.type-fill_blank { border-color: color-mix(in srgb, var(--type-fill_blank) 40%, transparent); }
.q-card.type-single_choice { border-color: color-mix(in srgb, var(--type-single_choice) 40%, transparent); }
.q-card.type-multiple_choice { border-color: color-mix(in srgb, var(--type-multiple_choice) 40%, transparent); }
.q-card.type-true_false { border-color: color-mix(in srgb, var(--type-true_false) 40%, transparent); }
.q-card.type-short_answer { border-color: color-mix(in srgb, var(--type-short_answer) 40%, transparent); }
.q-card.type-code_reading { border-color: color-mix(in srgb, var(--type-code_reading) 40%, transparent); }
.q-card.type-scenario { border-color: color-mix(in srgb, var(--type-scenario) 40%, transparent); }
.q-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; gap: 8px; flex-wrap: wrap; }
.q-number { font-size: 13px; color: var(--text2); font-weight: 600; }
.q-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 6px; font-size: 12px; font-weight: 600; }
.q-badge.fill_blank { background: rgba(99,102,241,0.15); color: var(--accent2); }
.q-badge.single_choice { background: rgba(34,197,94,0.15); color: var(--green); }
.q-difficulty { display: flex; gap: 2px; }
.q-difficulty .star { width: 8px; height: 8px; border-radius: 50%; background: var(--surface2); }
.q-difficulty .star.active { background: var(--yellow); }
@@ -144,6 +165,7 @@ body { font-family: -apple-system, 'Segoe UI', sans-serif; background: var(--bg)
<div class="header">
<h1>🎯 CS 知识应试强化</h1>
<p>选择主题,开始训练</p>
<button class="theme-toggle" onclick="toggleTheme()" title="切换主题">🌙</button>
</div>
<div class="app">
@@ -181,15 +203,32 @@ body { font-family: -apple-system, 'Segoe UI', sans-serif; background: var(--bg)
</div>
<script>
// ─── Constants ───
const QUESTION_TYPES = {
fill_blank: { label: '填空', color: '--type-fill_blank' },
single_choice: { label: '选择', color: '--type-single_choice' },
multiple_choice: { label: '多选', color: '--type-multiple_choice' },
true_false: { label: '判断', color: '--type-true_false' },
short_answer: { label: '简答', color: '--type-short_answer' },
code_reading: { label: '代码', color: '--type-code_reading' },
scenario: { label: '场景', color: '--type-scenario' },
};
// ─── State ───
let topics = [];
let currentTopic = null;
let currentQuestions = []; // flattened from all type files
let answers = {}; // qId -> { value, correct, revealed }
let customQuestions = []; // imported
let activeTab = 'all'; // 'all' or type name
// ─── Init ───
async function init() {
// Theme init
const saved = localStorage.getItem('theme') || 'dark';
document.documentElement.dataset.theme = saved;
updateThemeToggle();
try {
const res = await fetch('topics/index.json');
const data = await res.json();
@@ -200,6 +239,20 @@ async function init() {
}
}
// ─── Theme ───
function toggleTheme() {
const current = document.documentElement.dataset.theme;
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
updateThemeToggle();
}
function updateThemeToggle() {
const btn = document.querySelector('.theme-toggle');
if (btn) btn.textContent = document.documentElement.dataset.theme === 'dark' ? '🌙' : '☀️';
}
// ─── Topic List ───
function renderTopicList() {
const ul = document.getElementById('topicList');
@@ -223,14 +276,14 @@ function renderTopicList() {
}
function typeLabel(type) {
const map = { fill_blank: '填空', single_choice: '选择', multiple_choice: '多选', true_false: '判断', short_answer: '简答', code_reading: '代码', scenario: '场景' };
return map[type] || type;
return QUESTION_TYPES[type]?.label || type;
}
// ─── Select Topic ───
async function selectTopic(slug) {
currentTopic = slug;
answers = {};
activeTab = 'all';
renderTopicList();
if (slug === '__custom__') {
@@ -260,6 +313,11 @@ async function selectTopic(slug) {
}
// ─── Render Quiz ───
function getFilteredQuestions() {
if (activeTab === 'all') return currentQuestions;
return currentQuestions.filter(q => q.type === activeTab);
}
function renderQuiz() {
const main = document.getElementById('mainContent');
if (!currentQuestions.length) {
@@ -267,31 +325,36 @@ function renderQuiz() {
return;
}
const topicName = currentTopic === '__custom__' ? '自定义导入' : (topics.find(t => t.slug === currentTopic)?.name || '');
// Build tabs
const types = [...new Set(currentQuestions.map(q => q.type))];
let tabsHtml = `<button class="tab ${activeTab === 'all' ? 'active' : ''}" onclick="switchTab('all')">全部 ${currentQuestions.length}</button>`;
types.forEach(type => {
const count = currentQuestions.filter(q => q.type === type).length;
tabsHtml += `<button class="tab ${activeTab === type ? 'active' : ''}" onclick="switchTab('${type}')">${typeLabel(type)} ${count}</button>`;
});
let html = `
<div class="tabs">${tabsHtml}</div>
<div class="toolbar">
<div class="stats">
<span>📊 进度 <span class="num" id="answeredCount">0</span>/${currentQuestions.length}</span>
<span>✅ <span class="num green" id="correctCount">0</span></span>
<span>❌ <span class="num red" id="wrongCount">0</span></span>
<span>⏳ <span class="num yellow" id="pendingCount">${currentQuestions.length}</span></span>
</div>
<div class="toolbar-actions">
<button class="btn btn-outline btn-sm" onclick="resetAll()">🔄 重置</button>
<button class="btn btn-outline btn-sm" onclick="showAllAnswers()">👁 全部显示</button>
<button class="btn btn-primary btn-sm" onclick="submitAll()">📝 提交全部</button>
</div>
</div>
<div class="progress-bar"><div class="fill" id="progressFill" style="width:0%"></div></div>
`;
currentQuestions.forEach((q, i) => {
const filtered = getFilteredQuestions();
filtered.forEach((q, i) => {
html += renderQuestionCard(q, i);
});
main.innerHTML = html;
updateStats();
}
function switchTab(tab) {
activeTab = tab;
renderQuiz();
}
function renderQuestionCard(q, i) {
@@ -305,10 +368,9 @@ function renderQuestionCard(q, i) {
stars += `<div class="star ${s <= q.difficulty ? 'active' : ''}"></div>`;
}
let html = `<div class="q-card ${cardClass}" id="card-${q.id}">
let html = `<div class="q-card ${cardClass} type-${q.type}" id="card-${q.id}">
<div class="q-header">
<span class="q-number">#${i + 1}</span>
<span class="q-badge ${q.type}">${typeLabel(q.type)}</span>
<div class="q-difficulty">${stars}</div>
</div>
<div class="q-text">`;
@@ -403,7 +465,6 @@ function checkFill(qId) {
const correct = matchFillAnswer(val, q.answer, q.answer_rule || 'any');
answers[qId] = { value: val, correct, revealed: true };
updateStats();
renderQuiz();
}
@@ -436,7 +497,6 @@ function revealAnswer(qId) {
answers[qId].correct = answers[qId].value === q.answer;
}
answers[qId].revealed = true;
updateStats();
renderQuiz();
}
@@ -450,7 +510,6 @@ function showAllAnswers() {
answers[q.id].revealed = true;
}
});
updateStats();
renderQuiz();
}
@@ -466,32 +525,14 @@ function submitAll() {
answers[q.id].revealed = true;
}
});
updateStats();
renderQuiz();
}
function resetAll() {
answers = {};
updateStats();
renderQuiz();
}
function updateStats() {
let answered = 0, correct = 0, wrong = 0;
currentQuestions.forEach(q => {
if (answers[q.id]?.revealed) {
answered++;
if (answers[q.id].correct) correct++; else wrong++;
}
});
const el = id => document.getElementById(id);
if (el('answeredCount')) el('answeredCount').textContent = answered;
if (el('correctCount')) el('correctCount').textContent = correct;
if (el('wrongCount')) el('wrongCount').textContent = wrong;
if (el('pendingCount')) el('pendingCount').textContent = currentQuestions.length - answered;
if (el('progressFill')) el('progressFill').style.width = (currentQuestions.length ? (answered / currentQuestions.length * 100) : 0) + '%';
}
// ─── Import ───
function openImportModal() { document.getElementById('importModal').classList.add('show'); }
function closeImportModal() { document.getElementById('importModal').classList.remove('show'); }