Files
mantle-ai-trader/skills/quiz-html/examples/demo.html
2026-06-06 05:21:10 +00:00

1350 lines
66 KiB
HTML
Executable File
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">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<title>📚 StudyBuddy 题库示例</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',Roboto,sans-serif;background:#f5f7fb;color:#2d3142;height:100vh;overflow:hidden;display:flex;flex-direction:column;transition:background .2s,color .2s}
/* ============ 顶部条 ============ */
.topbar{background:#fff;border-bottom:1px solid #e8eaf2;padding:10px 20px;display:flex;flex-direction:column;gap:8px;flex-shrink:0;box-shadow:0 1px 3px rgba(0,0,0,0.03)}
.topbar-row1{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.topbar-row2{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding-top:8px;border-top:1px dashed #f0f2f7}
.topbar h1{font-size:16px;color:#5b8def;white-space:nowrap;font-weight:700}
.topbar h1 .subtitle{font-size:12px;color:#8a8fa3;font-weight:400;margin-left:6px}
.cat-filter{display:flex;align-items:center;gap:6px;flex-wrap:wrap;width:100%}
.cat-filter-label{font-size:12px;color:#8a8fa3;white-space:nowrap;font-weight:600}
.cat-chip{font-size:12px;padding:4px 12px;border-radius:14px;border:1px solid #e0e3ed;background:#fff;cursor:pointer;user-select:none;transition:all .15s;color:#8a8fa3;white-space:nowrap}
.cat-chip:hover{border-color:#5b8def}
.cat-chip.active{background:#5b8def;color:#fff;border-color:#5b8def}
.cat-chip .chip-count{font-size:10px;opacity:.75;margin-left:4px}
.stat-filters{display:flex;gap:6px;align-items:center}
.sf-btn{display:flex;align-items:center;gap:5px;padding:5px 12px;border-radius:18px;border:1px solid #e0e3ed;background:#fff;cursor:pointer;transition:all .15s;user-select:none;font-size:12px;color:#8a8fa3}
.sf-btn:hover{border-color:#5b8def}
.sf-btn .dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;transition:opacity .15s}
.sf-btn .dot.green{background:#52c785}
.sf-btn .dot.gray{background:#b8bdcc}
.sf-btn .dot.red{background:#ff7a90}
.sf-btn .dot.yellow{background:#ffc857}
.sf-btn .num{font-weight:700;min-width:16px;color:#2d3142}
.sf-btn.off{opacity:.4}
.io-btns{display:flex;gap:5px;margin-left:auto;flex-wrap:wrap}
.io-btns button{font-size:12px;padding:5px 10px;border-radius:7px;border:1px solid #e0e3ed;background:#fff;color:#5b6172;cursor:pointer;transition:all .15s}
.io-btns button:hover{border-color:#5b8def;color:#5b8def}
.io-btns button.exam{background:#fef0f3;border-color:#ffd1d9;color:#ff5d77}
.io-btns button.exam:hover{background:#ff5d77;color:#fff;border-color:#ff5d77}
.io-btns button.danger:hover{border-color:#ff7a90;color:#ff7a90}
.io-btns .theme-toggle{padding:5px 9px;border-radius:50%;font-size:14px}
/* ============ 主体 ============ */
.main{display:flex;flex:1;overflow:hidden}
.left{width:300px;min-width:300px;background:#fff;border-right:1px solid #e8eaf2;display:flex;flex-direction:column;flex-shrink:0}
.left-header{padding:12px 16px;border-bottom:1px solid #e8eaf2;font-size:13px;color:#8a8fa3;display:flex;justify-content:space-between;align-items:center}
.left-header .progress{font-weight:700;color:#5b8def}
.left-scroll{flex:1;overflow-y:auto;padding:6px 0}
.left-scroll::-webkit-scrollbar{width:6px}
.left-scroll::-webkit-scrollbar-thumb{background:#d8dce8;border-radius:3px}
.cat-group{margin-bottom:2px}
.cat-top{padding:8px 16px;font-size:13px;font-weight:700;color:#2d3142;background:#fafbfd;display:flex;align-items:center;gap:6px;position:sticky;top:0;z-index:1;border-bottom:1px solid #f0f2f7}
.cat-count{font-size:11px;color:#a8adbd;font-weight:normal}
.cat-sub{padding:6px 16px 8px 16px}
.cat-sub-name{font-size:11px;color:#a8adbd;margin-bottom:5px;line-height:1.4}
.cat-nums{display:flex;flex-wrap:wrap;gap:3px}
.qnum{display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:26px;padding:0 6px;border-radius:5px;font-size:12px;cursor:pointer;transition:all .15s;border:1.5px solid transparent;background:#f5f6fa;color:#5b6172;font-weight:600}
.qnum:hover{border-color:#5b8def;color:#5b8def;transform:translateY(-1px)}
.qnum.active{border-color:#5b8def!important;color:#fff!important;background:#5b8def!important;box-shadow:0 2px 6px rgba(91,141,239,0.3)}
.qnum.mastered{color:#3fb371;background:rgba(82,199,133,0.15)}
.qnum.unstudied{color:#8a8fa3;background:#f0f2f7}
.qnum.error{color:#ff5d77;background:rgba(255,122,144,0.15)}
.qnum.filtered-out{display:none!important}
/* ============ 右侧 ============ */
.right{flex:1;display:flex;flex-direction:column;overflow:hidden;min-width:0}
.right-inner{flex:1;overflow-y:auto;padding:24px 32px}
.right-inner::-webkit-scrollbar{width:8px}
.right-inner::-webkit-scrollbar-thumb{background:#d8dce8;border-radius:4px}
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:#a8adbd;font-size:15px;gap:12px;text-align:center}
.empty-state .icon{font-size:54px}
.empty-state .hint{font-size:13px;color:#b8bdcc}
.q-card{max-width:780px;margin:0 auto}
.q-meta{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap}
.q-id{font-size:18px;font-weight:800;color:#5b8def}
.q-tag{font-size:11px;padding:3px 9px;border-radius:11px;font-weight:600;white-space:nowrap}
.q-tag.type-choice{background:#e8f0ff;color:#5b8def}
.q-tag.type-tf{background:#e8f6ed;color:#52c785}
.q-tag.type-blank{background:#fff4e0;color:#e9a82c}
.q-tag.type-short{background:#f0e8ff;color:#9168e8}
.q-tag.level{background:#f0f2f7;color:#8a8fa3}
.q-state-btn{font-size:11px;padding:4px 10px 4px 12px;border-radius:11px;border:1px solid #e0e3ed;background:#fff;cursor:pointer;color:#8a8fa3;transition:all .15s;font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,0.04)}
.q-state-btn:hover{border-color:#5b8def;color:#5b8def;box-shadow:0 2px 6px rgba(91,141,239,0.15);transform:translateY(-1px)}
.q-state-btn.mastered{color:#3fb371;border-color:#a3dcbb;background:#f0faf3}
.q-state-btn.error{color:#ff5d77;border-color:#ffb8c4;background:#fff5f6}
.q-knowledge{font-size:12px;color:#8a8fa3;margin-left:auto;max-width:50%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.q-text{font-size:16px;line-height:1.85;color:#2d3142;margin-bottom:20px;font-weight:500;white-space:pre-wrap}
.opts{display:flex;flex-direction:column;gap:9px;margin-bottom:16px}
.opt{display:flex;align-items:flex-start;gap:10px;padding:13px 16px;border-radius:10px;border:1.5px solid #e8eaf2;background:#fff;cursor:pointer;transition:all .15s;font-size:14px;line-height:1.6}
.opt:hover{border-color:#5b8def;background:#f8faff}
.opt .letter{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:50%;background:#f0f2f7;color:#5b6172;font-weight:700;flex-shrink:0;font-size:12px;transition:all .15s}
.opt:hover .letter{background:#5b8def;color:#fff}
.opt .text{flex:1;padding-top:2px;color:#2d3142}
.opt.selected{border-color:#5b8def;background:#f0f5ff}
.opt.selected .letter{background:#5b8def;color:#fff}
.opt.correct{border-color:#52c785;background:#f0faf3}
.opt.correct .letter{background:#52c785;color:#fff}
.opt.wrong{border-color:#ff7a90;background:#fff5f6}
.opt.wrong .letter{background:#ff7a90;color:#fff}
.opt.disabled{pointer-events:none}
.q-input{width:100%;padding:13px 15px;border-radius:10px;border:1.5px solid #e8eaf2;font-size:15px;font-family:inherit;background:#fff;color:#2d3142;outline:none;transition:all .15s;resize:vertical;margin-bottom:14px}
.q-input:focus{border-color:#5b8def;background:#f8faff}
.q-input.short{min-height:48px}
.q-input.long{min-height:120px}
.q-input:disabled{background:#f5f6fa;color:#8a8fa3}
.q-actions{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap}
.q-actions button{padding:9px 18px;border-radius:8px;border:none;cursor:pointer;font-size:13px;font-weight:600;transition:all .15s}
.btn-submit{background:#5b8def;color:#fff}
.btn-submit:hover{background:#4a7adb}
.btn-submit:disabled{background:#b8bdcc;cursor:not-allowed}
.btn-reveal{background:#f0f2f7;color:#5b6172}
.btn-reveal:hover{background:#e0e3ed}
.btn-reset{background:#fff;color:#8a8fa3;border:1.5px solid #e0e3ed!important}
.btn-reset:hover{border-color:#ff7a90!important;color:#ff7a90}
.auto-state-hint{font-size:13px;padding:9px 14px;border-radius:8px;margin-bottom:14px;display:none;font-weight:600}
.auto-state-hint.mastered{display:block;background:rgba(82,199,133,0.12);color:#3fb371;border:1px solid rgba(82,199,133,0.3)}
.auto-state-hint.error{display:block;background:rgba(255,122,144,0.12);color:#ff5d77;border:1px solid rgba(255,122,144,0.3)}
.auto-state-hint.retry{display:block;background:rgba(255,200,87,0.18);color:#d68b00;border:1px solid rgba(255,200,87,0.4);animation:slideIn .25s}
@keyframes slideIn{from{transform:translateY(-4px);opacity:0}to{transform:translateY(0);opacity:1}}
.opt.wrong-shake{animation:shake .5s;border-color:#ff7a90;background:#fff5f6}
@keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-6px)}40%,80%{transform:translateX(6px)}}
.analysis{display:none;margin-top:14px;padding:14px 18px;background:#fafbfd;border-radius:10px;border-left:4px solid #5b8def}
.analysis.visible{display:block}
.analysis.correct{border-left-color:#52c785;background:#f0faf3}
.analysis.wrong{border-left-color:#ff7a90;background:#fff5f6}
.ans-line{font-size:14px;font-weight:700;margin-bottom:8px;display:flex;align-items:center;gap:6px}
.analysis .ans-line{color:#3fb371}
.analysis.wrong .ans-line{color:#ff5d77}
.ans-text{font-size:13px;color:#5b6172;line-height:1.75;margin-bottom:6px}
.ans-text strong{color:#2d3142}
.ans-text em{color:#5b8def;font-style:normal;font-weight:600}
.ans-text code{background:rgba(91,141,239,0.1);padding:1px 6px;border-radius:4px;font-size:12px;color:#5b8def}
.tip{font-size:13px;color:#d68b00;line-height:1.7;padding:8px 12px;background:rgba(255,200,87,0.15);border-radius:6px;margin-top:8px;display:none}
.tip.visible{display:block}
.kw{font-size:12px;color:#a8adbd;margin-top:8px}
/* ============ 导航条 ============ */
.nav-bar{padding:12px 32px;border-top:1px solid #e8eaf2;display:flex;align-items:center;justify-content:space-between;background:#fafbfd;flex-shrink:0}
.nav-btn{padding:7px 18px;border-radius:7px;border:1px solid #e0e3ed;background:#fff;color:#5b6172;cursor:pointer;font-size:13px;transition:all .15s}
.nav-btn:hover:not(:disabled){border-color:#5b8def;color:#5b8def}
.nav-btn:disabled{opacity:.4;cursor:default}
.nav-info{font-size:12px;color:#8a8fa3}
.kbd-hint{font-size:11px;color:#a8adbd;flex:1;text-align:center}
@media(max-width:900px){.kbd-hint{display:none}}
/* ============ Modal ============ */
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1000;align-items:center;justify-content:center}
.modal-overlay.visible{display:flex}
.modal{background:#fff;border-radius:14px;padding:24px 28px;max-width:480px;width:90%;box-shadow:0 10px 40px rgba(0,0,0,0.15)}
.modal h3{font-size:17px;color:#2d3142;margin-bottom:10px}
.modal p{font-size:13px;color:#8a8fa3;line-height:1.6;margin-bottom:16px}
.modal-row{margin-bottom:14px}
.modal-row label{display:block;font-size:12px;color:#5b6172;margin-bottom:6px;font-weight:600}
.modal-row .opts-row{display:flex;flex-wrap:wrap;gap:6px}
.modal-btn{padding:7px 14px;border-radius:7px;border:1px solid #e0e3ed;background:#fff;color:#5b6172;cursor:pointer;font-size:13px;transition:all .15s}
.modal-btn:hover{border-color:#5b8def;color:#5b8def}
.modal-btn.selected{border-color:#5b8def;background:#5b8def;color:#fff}
.modal-textarea{width:100%;min-height:140px;padding:10px;border-radius:8px;border:1.5px solid #e8eaf2;font-family:monospace;font-size:12px;resize:vertical;outline:none}
.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
.modal-actions .primary{background:#5b8def;color:#fff;border:none;padding:8px 18px;border-radius:7px;cursor:pointer;font-weight:600}
.modal-actions .primary:hover{background:#4a7adb}
.modal-actions .danger{border-color:#ff7a90;color:#ff5d77}
/* ============ 模拟考模式 ============ */
.exam-overlay{display:none;position:fixed;inset:0;background:#f5f7fb;z-index:2000;flex-direction:column;overflow:hidden}
.exam-overlay.visible{display:flex}
.exam-topbar{background:#fff;border-bottom:1px solid #e8eaf2;padding:12px 20px;display:flex;align-items:center;gap:14px;flex-shrink:0;flex-wrap:wrap;box-shadow:0 1px 3px rgba(0,0,0,0.04)}
.exam-topbar h2{font-size:15px;color:#5b8def;font-weight:700}
.exam-progress{flex:1;display:flex;align-items:center;gap:8px;min-width:120px}
.exam-progress-bar{flex:1;height:6px;background:#e8eaf2;border-radius:3px;overflow:hidden}
.exam-progress-fill{height:100%;background:linear-gradient(90deg,#5b8def,#7a9fff);border-radius:3px;transition:width .3s}
.exam-progress-text{font-size:12px;color:#8a8fa3;white-space:nowrap}
.exam-timer{font-size:13px;color:#e9a82c;font-weight:700}
.exam-body{flex:1;overflow-y:auto;padding:20px 28px}
.exam-q-card{max-width:780px;margin:0 auto 16px;background:#fff;border:1px solid #e8eaf2;border-radius:12px;padding:18px 22px;box-shadow:0 2px 6px rgba(0,0,0,0.03)}
.exam-q-num{font-size:13px;color:#5b8def;font-weight:700;margin-bottom:8px}
.exam-q-text{font-size:15px;line-height:1.75;color:#2d3142;margin-bottom:12px;white-space:pre-wrap}
.exam-opts{display:flex;flex-direction:column;gap:6px}
.exam-opt{padding:9px 14px;border-radius:8px;border:1.5px solid #e8eaf2;background:#fff;cursor:pointer;transition:all .15s;display:flex;align-items:flex-start;gap:8px;font-size:13px;line-height:1.55}
.exam-opt:hover{border-color:#5b8def;background:#f8faff}
.exam-opt.selected{border-color:#5b8def;background:#f0f5ff}
.exam-opt .letter{font-weight:700;min-width:22px;color:#5b6172;flex-shrink:0}
.exam-opt.selected .letter{color:#5b8def}
.exam-input{width:100%;padding:9px 12px;border-radius:7px;border:1.5px solid #e8eaf2;font-size:13px;outline:none;font-family:inherit;resize:vertical;min-height:42px}
.exam-input:focus{border-color:#5b8def}
.exam-submit-bar{padding:14px 28px;border-top:1px solid #e8eaf2;background:#fff;display:flex;align-items:center;justify-content:center;gap:10px;flex-shrink:0}
.exam-submit-btn{padding:9px 28px;border-radius:8px;border:none;background:#5b8def;color:#fff;cursor:pointer;font-size:14px;font-weight:700}
.exam-submit-btn:hover{background:#4a7adb}
.exam-quit-btn{padding:9px 18px;border-radius:8px;border:1px solid #e0e3ed;background:#fff;color:#8a8fa3;cursor:pointer;font-size:13px}
.exam-quit-btn:hover{border-color:#ff7a90;color:#ff5d77}
/* 结果页 */
.exam-results{display:none;position:fixed;inset:0;background:#f5f7fb;z-index:2000;flex-direction:column;overflow:hidden}
.exam-results.visible{display:flex}
.results-header{background:#fff;border-bottom:1px solid #e8eaf2;padding:24px;text-align:center;flex-shrink:0}
.results-score{font-size:54px;font-weight:900;margin-bottom:4px;line-height:1}
.results-score.pass{color:#52c785}
.results-score.fail{color:#ff5d77}
.results-detail{font-size:14px;color:#8a8fa3;margin-bottom:14px}
.results-actions{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
.results-actions button{padding:8px 18px;border-radius:7px;border:1px solid #e0e3ed;background:#fff;color:#5b6172;cursor:pointer;font-size:13px}
.results-actions button:hover{border-color:#5b8def;color:#5b8def}
.results-actions button.primary{background:#5b8def;color:#fff;border-color:#5b8def}
.results-body{flex:1;overflow-y:auto;padding:18px 24px}
.result-item{max-width:780px;margin:0 auto 12px;background:#fff;border:1px solid #e8eaf2;border-radius:10px;padding:14px 18px}
.result-item.is-correct{border-left:4px solid #52c785}
.result-item.is-wrong{border-left:4px solid #ff7a90}
.result-top{display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap}
.result-badge{font-size:11px;padding:2px 9px;border-radius:10px;font-weight:700}
.result-badge.correct{background:#f0faf3;color:#3fb371}
.result-badge.wrong{background:#fff5f6;color:#ff5d77}
.result-qid{font-size:12px;color:#5b8def;font-weight:700}
.result-text{font-size:13px;line-height:1.7;color:#2d3142;margin-bottom:6px;white-space:pre-wrap}
.result-answers{font-size:12px;color:#5b6172;margin-bottom:6px}
.result-answers .correct-ans{color:#3fb371;font-weight:700}
.result-answers .wrong-ans{color:#ff5d77;text-decoration:line-through}
.result-analysis{font-size:12px;color:#5b6172;line-height:1.7;padding:7px 11px;background:#fafbfd;border-radius:6px;margin-bottom:6px}
.result-tip{font-size:12px;color:#d68b00;line-height:1.6;padding:6px 10px;background:rgba(255,200,87,0.15);border-radius:6px;margin-bottom:6px}
/* ============ 移动端 ============ */
.sidebar-toggle{display:none;position:fixed;bottom:20px;right:20px;z-index:999;width:48px;height:48px;border-radius:50%;background:#5b8def;color:#fff;border:none;font-size:20px;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.2)}
.sidebar-toggle:active{transform:scale(0.92)}
@media(max-width:768px){
.left{width:100%;min-width:100%;max-height:50vh;border-right:none;border-bottom:1px solid #e8eaf2;transition:max-height .3s ease}
.left.hidden{max-height:0;overflow:hidden;border-bottom:none}
.main{flex-direction:column}
.topbar{gap:8px;padding:8px 12px}
.topbar h1{font-size:14px}
.cat-filter{order:10;width:100%}
.io-btns{margin-left:0;width:100%;justify-content:flex-end}
.right-inner{padding:14px}
.sidebar-toggle{display:flex;align-items:center;justify-content:center}
.nav-bar{padding:10px 14px}
}
/* ============ Dark Mode ============ */
body.dark{background:#1a1a2e;color:#e0e3ed}
body.dark .topbar{background:#16213e;border-bottom-color:#0f3460;box-shadow:none}
body.dark .topbar h1{color:#e88ca5}
body.dark .cat-filter-label,body.dark .cat-sub-name,body.dark .left-header,body.dark .nav-info,body.dark .empty-state,body.dark .ans-text,body.dark .modal p{color:#888}
body.dark .cat-chip{background:transparent;border-color:#333;color:#888}
body.dark .cat-chip.active{background:#e88ca5;color:#fff;border-color:#e88ca5}
body.dark .sf-btn{background:transparent;border-color:#333;color:#aaa}
body.dark .sf-btn:hover{border-color:#e88ca5}
body.dark .sf-btn .num{color:#e0e3ed}
body.dark .io-btns button{background:transparent;border-color:#333;color:#aaa}
body.dark .io-btns button:hover{border-color:#e88ca5;color:#e88ca5}
body.dark .io-btns button.exam{background:rgba(232,140,165,0.15);border-color:#e88ca5;color:#e88ca5}
body.dark .left{background:#16213e;border-right-color:#0f3460}
body.dark .left-scroll::-webkit-scrollbar-thumb{background:#333}
body.dark .cat-top{background:#16213e;color:#e0e3ed;border-bottom-color:#0f3460}
body.dark .cat-count{color:#666}
body.dark .qnum{background:rgba(100,100,100,0.15);color:#777;border-color:transparent}
body.dark .qnum:hover{border-color:#e88ca5;color:#fff}
body.dark .qnum.active{background:#e88ca5!important;border-color:#e88ca5!important;color:#fff!important}
body.dark .qnum.mastered{background:rgba(110,197,168,0.15);color:#6ec5a8}
body.dark .qnum.unstudied{background:rgba(100,100,100,0.15);color:#888}
body.dark .qnum.error{background:rgba(232,140,165,0.15);color:#e88ca5}
body.dark .right-inner::-webkit-scrollbar-thumb{background:#333}
body.dark .q-text{color:#e0e3ed}
body.dark .q-id{color:#e88ca5}
body.dark .q-tag.type-choice{background:rgba(91,141,239,0.15);color:#7a9fff}
body.dark .q-tag.type-tf{background:rgba(110,197,168,0.15);color:#6ec5a8}
body.dark .q-tag.type-blank{background:rgba(255,200,87,0.15);color:#f4a261}
body.dark .q-tag.type-short{background:rgba(145,104,232,0.15);color:#b08fff}
body.dark .q-tag.level{background:rgba(100,100,100,0.2);color:#888}
body.dark .q-state-btn{background:transparent;border-color:#333;color:#aaa}
body.dark .q-knowledge{color:#666}
body.dark .opt{background:#1f2541;border-color:#333;color:#e0e3ed}
body.dark .opt:hover{background:rgba(232,140,165,0.05);border-color:#555}
body.dark .opt .text{color:#e0e3ed}
body.dark .opt .letter{background:#333;color:#888}
body.dark .opt.selected{background:rgba(232,140,165,0.1);border-color:#e88ca5}
body.dark .opt.correct{background:rgba(110,197,168,0.1);border-color:#6ec5a8}
body.dark .opt.wrong{background:rgba(232,140,165,0.1);border-color:#e88ca5}
body.dark .q-input{background:#1f2541;border-color:#333;color:#e0e3ed}
body.dark .q-input:focus{background:#1a1a2e;border-color:#e88ca5}
body.dark .btn-submit{background:#e88ca5}
body.dark .btn-submit:hover{background:#d97a93}
body.dark .btn-reveal{background:#333;color:#aaa}
body.dark .btn-reset{background:transparent;color:#888}
body.dark .analysis{background:#1a1a2e;border-left-color:#e88ca5}
body.dark .analysis.correct{background:#1a1a2e;border-left-color:#6ec5a8}
body.dark .analysis.wrong{background:#1a1a2e;border-left-color:#e88ca5}
body.dark .nav-bar{background:#16213e;border-top-color:#0f3460}
body.dark .nav-btn{background:transparent;border-color:#333;color:#aaa}
body.dark .kbd-hint{color:#555}
body.dark .modal{background:#16213e;color:#e0e3ed}
body.dark .modal h3{color:#e0e3ed}
body.dark .modal-btn{background:transparent;border-color:#333;color:#aaa}
body.dark .modal-btn.selected{background:#e88ca5;border-color:#e88ca5;color:#fff}
body.dark .modal-textarea{background:#1f2541;border-color:#333;color:#e0e3ed}
body.dark .exam-overlay,body.dark .exam-results{background:#1a1a2e}
body.dark .exam-topbar,body.dark .exam-submit-bar,body.dark .results-header{background:#16213e;border-color:#0f3460}
body.dark .exam-q-card,body.dark .result-item{background:#16213e;border-color:#0f3460}
body.dark .exam-q-text,body.dark .result-text{color:#e0e3ed}
body.dark .exam-opt{background:#1f2541;border-color:#333;color:#e0e3ed}
body.dark .exam-opt.selected{background:rgba(232,140,165,0.1);border-color:#e88ca5}
body.dark .result-analysis{background:#1a1a2e;color:#aaa}
body.dark .auto-state-hint.mastered{background:rgba(110,197,168,0.1);color:#6ec5a8;border-color:rgba(110,197,168,0.3)}
body.dark .auto-state-hint.error{background:rgba(232,140,165,0.1);color:#e88ca5;border-color:rgba(232,140,165,0.3)}
</style>
</head>
<body class="light">
<div class="topbar">
<div class="topbar-row1">
<h1>📚 StudyBuddy 题库示例 <span class="subtitle">K12 多学科 · 8 道题</span></h1>
<div class="stat-filters" id="statFilters">
<div class="sf-btn" data-f="mastered" onclick="toggleFilter('mastered',this)"><span class="dot green"></span><span class="num" id="numMastered">0</span><span>已掌握</span></div>
<div class="sf-btn" data-f="unstudied" onclick="toggleFilter('unstudied',this)"><span class="dot gray"></span><span class="num" id="numUnstudied">0</span><span>未做</span></div>
<div class="sf-btn" data-f="error" onclick="toggleFilter('error',this)"><span class="dot red"></span><span class="num" id="numError">0</span><span>错题</span></div>
</div>
<div class="io-btns">
<button class="exam" onclick="showExamModal()">📝 模拟考</button>
<button onclick="randomQ()">🎲 随机</button>
<button onclick="exportProgress()">💾 导出进度</button>
<button class="danger" onclick="showResetModal()">🗑️ 重置</button>
<button class="theme-toggle" onclick="toggleTheme()" title="切换主题">☀️</button>
</div>
</div>
<div class="topbar-row2">
<div class="cat-filter" id="catFilter"><span class="cat-filter-label">📂 分类:</span></div>
</div>
</div>
<button class="sidebar-toggle" onclick="toggleSidebar()">📋</button>
<div class="main">
<div class="left" id="sidebar">
<div class="left-header"><span>📚 题号目录</span><span class="progress" id="progressTxt">0/0</span></div>
<div class="left-scroll" id="leftScroll"></div>
</div>
<div class="right">
<div class="right-inner" id="rightInner">
<div class="empty-state">
<div class="icon">📖</div>
<div>选择左侧题号开始学习</div>
<div class="hint">点击题号查看,做完会自动记录到本地</div>
</div>
</div>
<div class="nav-bar" id="navBar" style="display:none;">
<button class="nav-btn" id="prevBtn" onclick="goPrev()" title="← 上一题">← 上一题</button>
<span class="nav-info" id="navInfo"></span>
<span class="kbd-hint">⌨️ A/B/C/D 选项 · Enter 提交 · ← → 切题 · Space 看答案</span>
<button class="nav-btn" id="nextBtn" onclick="goNext()" title="→ 下一题">下一题 →</button>
</div>
</div>
</div>
<!-- ===== 模拟考 modal ===== -->
<div class="modal-overlay" id="examModal">
<div class="modal">
<h3>📝 开始模拟考</h3>
<p id="examPoolSize">从当前题池中随机抽题作答</p>
<div class="modal-row">
<label>题量</label>
<div class="opts-row">
<button class="modal-btn" data-n="5" onclick="selectExamCount(5)">5题</button>
<button class="modal-btn selected" data-n="10" onclick="selectExamCount(10)">10题</button>
<button class="modal-btn" data-n="20" onclick="selectExamCount(20)">20题</button>
<button class="modal-btn" data-n="30" onclick="selectExamCount(30)">30题</button>
<button class="modal-btn" data-n="50" onclick="selectExamCount(50)">50题</button>
</div>
</div>
<div class="modal-row">
<label>每题限时</label>
<div class="opts-row">
<button class="modal-btn selected" data-t="0" onclick="selectExamTime(0)">不限时</button>
<button class="modal-btn" data-t="30" onclick="selectExamTime(30)">30秒</button>
<button class="modal-btn" data-t="60" onclick="selectExamTime(60)">1分钟</button>
<button class="modal-btn" data-t="90" onclick="selectExamTime(90)">1.5分钟</button>
<button class="modal-btn" data-t="120" onclick="selectExamTime(120)">2分钟</button>
</div>
</div>
<div class="modal-actions">
<button class="modal-btn" onclick="hideExamModal()">取消</button>
<button class="primary" onclick="startExam()">🚀 开考</button>
</div>
</div>
</div>
<!-- ===== 导入 modal ===== -->
<div class="modal-overlay" id="importModal">
<div class="modal">
<h3 id="importTitle">📥 导入</h3>
<p id="importHint">粘贴 JSON 内容</p>
<textarea class="modal-textarea" id="importText" placeholder=""></textarea>
<div class="modal-actions">
<button class="modal-btn" onclick="hideImportModal()">取消</button>
<button class="primary" onclick="doImport()">导入</button>
</div>
</div>
</div>
<!-- ===== 重置 modal ===== -->
<div class="modal-overlay" id="resetModal">
<div class="modal">
<h3>🗑️ 重置答题记录</h3>
<p>这会清空所有"已掌握 / 错题 / 答题状态",但题目本身不会删除。确定吗?</p>
<div class="modal-actions">
<button class="modal-btn" onclick="hideResetModal()">取消</button>
<button class="primary danger" style="background:#ff5d77;" onclick="doReset()">确认重置</button>
</div>
</div>
</div>
<!-- ===== 模拟考答题界面 ===== -->
<div class="exam-overlay" id="examOverlay">
<div class="exam-topbar">
<h2>📝 模拟考</h2>
<div class="exam-progress">
<div class="exam-progress-bar"><div class="exam-progress-fill" id="examProgressFill" style="width:0%"></div></div>
<span class="exam-progress-text" id="examProgressText">0 / 0</span>
</div>
<span class="exam-timer" id="examTimer">⏱ 0:00</span>
<button class="exam-quit-btn" onclick="quitExam()">退出</button>
</div>
<div class="exam-body" id="examBody"></div>
<div class="exam-submit-bar">
<button class="exam-submit-btn" id="examSubmitBtn" onclick="submitExam()">📤 提交答卷</button>
</div>
</div>
<!-- ===== 成绩页 ===== -->
<div class="exam-results" id="examResults">
<div class="results-header">
<div class="results-score" id="resultsScore">0</div>
<div class="results-detail" id="resultsDetail"></div>
<div class="results-actions">
<button class="primary" onclick="closeResults()">返回</button>
<button onclick="retakeExam()">🔄 再考一次</button>
</div>
</div>
<div class="results-body" id="resultsBody"></div>
</div>
<!-- ===== 题库数据(脚本注入) ===== -->
<script>
window.__QUIZ_DATA__ = [{"type": "single_choice", "prompt": "下列关于并联电路电流规律的说法,正确的是( )。", "options": ["A. 干路电流等于各支路电流之差", "B. 干路电流等于各支路电流之和", "C. 各支路电流相等", "D. 干路电流大于任一支路电流的两倍"], "answer": "B", "explanation": "并联电路中,**干路电流等于各支路电流之和**I = I₁ + I₂ + ...。各支路电流不一定相等(取决于电阻)。", "knowledge_point": "并联电路电流规律", "category": "物理 / 电学", "level": 1}, {"type": "single_choice", "prompt": "汽油机燃烧不充分时(冒黑烟),发动机的热机效率会( )。", "options": ["A. 升高", "B. 降低", "C. 不变", "D. 先升高后降低"], "answer": "B", "explanation": "燃烧不充分意味着燃料中的化学能没有完全转化为内能,更多能量以未燃烧的碳颗粒(黑烟)形式损失,所以**有效利用的能量比例下降**,效率**降低**。\n\n📌 经典易错点:凡是\"不充分\",效率一定降低!", "knowledge_point": "热机效率", "category": "物理 / 热学", "level": 2}, {"type": "true_false", "prompt": "串联电路中,每个用电器两端的电压相等。", "answer": "False", "explanation": "错误。串联电路中**总电压等于各部分电压之和**U = U₁ + U₂各用电器两端的电压**不一定相等**,与电阻成正比。只有阻值相同的用电器电压才相等。", "knowledge_point": "串联电路电压规律", "category": "物理 / 电学", "level": 1}, {"type": "fill_blank", "prompt": "一辆汽车做功冲程对外做功 120J飞轮转速 1500 r/min则 1 分钟内做功 ____ 次,功率为 ____ W。", "answer": "750次, 1500W", "explanation": "四冲程汽油机每 2 圈完成 1 个工作循环(即做功 1 次)。\n- 1 分钟内圈数 = 1500\n- 做功次数 = 1500 ÷ 2 = **750** 次\n- 功率 P = W/t = 120 × 750 / 60 = **1500 W**", "knowledge_point": "热机做功与功率计算", "category": "物理 / 热学", "level": 3}, {"type": "single_choice", "prompt": "下列分数中,与 3/4 大小相等的是( )。", "options": ["A. 6/12", "B. 9/12", "C. 12/15", "D. 15/24"], "answer": "B", "explanation": "分数的基本性质:分子分母同时乘以同一个数,分数大小不变。\n3/4 = 3×3 / 4×3 = **9/12**", "knowledge_point": "分数的基本性质", "category": "数学 / 分数", "level": 1}, {"type": "fill_blank", "prompt": "把 0.75 化成分数是 ____最简分数。", "answer": "3/4", "explanation": "0.75 = 75/100 = **3/4**(分子分母同时除以最大公因数 25", "knowledge_point": "小数与分数互化", "category": "数学 / 分数", "level": 1}, {"type": "short_answer", "prompt": "请用自己的话解释\"并联电路中,干路电流为什么等于各支路电流之和\"。", "answer": "因为电流不会凭空产生也不会凭空消失(电荷守恒)。在并联电路的交点处,从干路流入的电荷必须等于流向各支路的电荷总和,所以干路电流 = 各支路电流之和。可以用\"水流分叉\"来类比:主管道的水量 = 各分支管道水量之和。", "explanation": "这是简答题,答案要点:① 电荷守恒思想;② 交点处电荷收支平衡;③ 能用例子说明加分。", "knowledge_point": "并联电路电流规律", "category": "物理 / 电学", "level": 3}, {"type": "single_choice", "prompt": "下面哪个词语用得不恰当?\n\n小明的作文写得很**生动**,老师读完后**心旷神怡**。", "options": ["A. 生动 用得恰当", "B. 心旷神怡 用得不恰当", "C. 两个词都恰当", "D. 两个词都不恰当"], "answer": "B", "explanation": "**心旷神怡**指看到美景或好事,心情舒畅;多用于描述风景、环境。\n用来形容读作文的感受不太合适应该用\"赞不绝口\"\"拍案叫绝\"之类的词。", "knowledge_point": "成语运用", "category": "语文 / 词语", "level": 2}];
window.__META__ = {"id": "demo", "title": "StudyBuddy 题库示例", "subtitle": "K12 多学科 · 8 道题"};
</script>
<script>
'use strict';
/* ============================================
* Quiz Engine - K12 StudyBuddy 4.0
* 受 v15 启发,针对 K12 多学科多题型优化
* ============================================ */
const META = window.__META__ || {};
const STORAGE_KEY = 'k12_quiz_' + (META.id || 'default');
const FILTER_KEY = STORAGE_KEY + '_filter';
const CATS_KEY = STORAGE_KEY + '_cats';
const THEME_KEY = 'k12_quiz_theme';
const TYPE_LABELS = {
single_choice: { txt: '选择', cls: 'type-choice' },
true_false: { txt: '判断', cls: 'type-tf' },
fill_blank: { txt: '填空', cls: 'type-blank' },
short_answer: { txt: '简答', cls: 'type-short' }
};
const LEVEL_LABELS = { 1: 'L1·识记', 2: 'L2·理解', 3: 'L3·应用' };
const STATE_LABELS = { mastered: '✅ 已掌握', unstudied: '⬜ 未做', error: '❌ 错题' };
let QS = []; // 全部题目(含 id
let states = {}; // {id: 'mastered'|'unstudied'|'error'}
let answered = {}; // {id: userAnswer}
let activeFilters = { mastered: true, unstudied: true, error: true };
let activeCats = new Set(); // 当前激活的学科
let currentId = null;
let filteredIds = [];
/* ===== 启动 ===== */
function init() {
// 主题恢复(默认 light
try {
const t = localStorage.getItem(THEME_KEY);
if (t === 'dark') {
document.body.classList.remove('light');
document.body.classList.add('dark');
document.querySelector('.theme-toggle').textContent = '🌙';
}
} catch (e) {}
QS = (window.__QUIZ_DATA__ || []).map((q, i) => Object.assign({ id: i + 1 }, q));
if (QS.length === 0) {
document.getElementById('leftScroll').innerHTML = '<div style="padding:20px;color:#a8adbd;font-size:13px;text-align:center;">题库为空,请先导入题目</div>';
return;
}
loadState();
loadFilters();
buildCatChips();
buildSidebar();
applyFilters();
updateStats();
}
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const obj = JSON.parse(raw);
states = obj.states || {};
answered = obj.answered || {};
}
} catch (e) { states = {}; answered = {}; }
QS.forEach(q => { if (!states[q.id]) states[q.id] = 'unstudied'; });
}
function saveState() {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ states, answered })); } catch (e) {}
}
function loadFilters() {
try {
const raw = localStorage.getItem(FILTER_KEY);
if (raw) activeFilters = Object.assign(activeFilters, JSON.parse(raw));
} catch (e) {}
document.querySelectorAll('.sf-btn').forEach(btn => {
if (!activeFilters[btn.dataset.f]) btn.classList.add('off');
});
// 学科:默认全选
const cats = [...new Set(QS.map(q => (q.category || '通用').trim()))];
try {
const raw = localStorage.getItem(CATS_KEY);
const arr = raw ? JSON.parse(raw) : null;
activeCats = new Set(Array.isArray(arr) && arr.length ? arr : cats);
} catch (e) { activeCats = new Set(cats); }
}
/* ===== 分类 chips按"学科/子分类"完整路径展示) ===== */
function getCatKey(q) {
// category 字段已含 "物理 / 电学" 这种完整路径,直接用即可
return (q.category || '通用').trim();
}
function buildCatChips() {
const cats = [...new Set(QS.map(getCatKey))];
const counts = {};
cats.forEach(c => { counts[c] = QS.filter(q => getCatKey(q) === c).length; });
const wrap = document.getElementById('catFilter');
wrap.querySelectorAll('.cat-chip').forEach(el => el.remove());
cats.forEach(c => {
const chip = document.createElement('span');
chip.className = 'cat-chip' + (activeCats.has(c) ? ' active' : '');
chip.dataset.cat = c;
chip.innerHTML = `${escapeHtml(c)}<span class="chip-count">${counts[c]}</span>`;
chip.onclick = () => toggleCat(c, chip);
wrap.appendChild(chip);
});
}
function toggleCat(cat, el) {
if (activeCats.has(cat)) {
if (activeCats.size <= 1) return; // 至少保留一个
activeCats.delete(cat);
el.classList.remove('active');
} else {
activeCats.add(cat);
el.classList.add('active');
}
try { localStorage.setItem(CATS_KEY, JSON.stringify([...activeCats])); } catch (e) {}
buildSidebar();
applyFilters();
}
/* ===== 侧栏 ===== */
function buildSidebar() {
const groups = {};
QS.forEach(q => {
const cat = getCatKey(q);
if (!activeCats.has(cat)) return;
const sub = q.knowledge_point || q.knowledge || '未分类';
if (!groups[cat]) groups[cat] = {};
if (!groups[cat][sub]) groups[cat][sub] = [];
groups[cat][sub].push(q);
});
const container = document.getElementById('leftScroll');
container.innerHTML = '';
Object.keys(groups).forEach(cat => {
const subs = groups[cat];
const total = Object.values(subs).reduce((s, arr) => s + arr.length, 0);
let html = `<div class="cat-group"><div class="cat-top">📂 ${escapeHtml(cat)} <span class="cat-count">${total}题</span></div>`;
Object.keys(subs).forEach(sub => {
html += `<div class="cat-sub"><div class="cat-sub-name">${escapeHtml(sub)}</div><div class="cat-nums">`;
subs[sub].forEach(q => {
const st = states[q.id] || 'unstudied';
html += `<span class="qnum ${st}" data-id="${q.id}" onclick="selectQ(${q.id})">${q.id}</span>`;
});
html += `</div></div>`;
});
html += `</div>`;
container.insertAdjacentHTML('beforeend', html);
});
}
function applyStatesToDOM() {
document.querySelectorAll('.qnum').forEach(el => {
el.classList.remove('mastered', 'unstudied', 'error');
const id = parseInt(el.dataset.id, 10);
el.classList.add(states[id] || 'unstudied');
});
}
function updateStats() {
let m = 0, u = 0, e = 0;
QS.forEach(q => {
if (!activeCats.has(getCatKey(q))) return;
const s = states[q.id] || 'unstudied';
if (s === 'mastered') m++;
else if (s === 'error') e++;
else u++;
});
document.getElementById('numMastered').textContent = m;
document.getElementById('numUnstudied').textContent = u;
document.getElementById('numError').textContent = e;
const total = m + u + e;
document.getElementById('progressTxt').textContent = `${m}/${total}`;
}
/* ===== 过滤 ===== */
function toggleFilter(type, btn) {
activeFilters[type] = !activeFilters[type];
btn.classList.toggle('off', !activeFilters[type]);
try { localStorage.setItem(FILTER_KEY, JSON.stringify(activeFilters)); } catch (e) {}
applyFilters();
}
function applyFilters() {
filteredIds = [];
document.querySelectorAll('.qnum').forEach(el => {
const id = parseInt(el.dataset.id, 10);
const st = states[id] || 'unstudied';
const visible = activeFilters[st];
el.classList.toggle('filtered-out', !visible);
if (visible) filteredIds.push(id);
});
updateStats();
updateNav();
}
/* ===== 选题 + 渲染 ===== */
window.selectQ = function (id) {
currentId = id;
document.querySelectorAll('.qnum').forEach(el => {
el.classList.toggle('active', parseInt(el.dataset.id, 10) === id);
});
const q = QS.find(x => x.id === id);
if (!q) return;
renderQuestion(q);
document.getElementById('navBar').style.display = 'flex';
updateNav();
// 移动端:选完自动收起侧栏
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.add('hidden');
}
};
function renderQuestion(q) {
const typeInfo = TYPE_LABELS[q.type] || { txt: '题', cls: 'type-choice' };
const lvl = LEVEL_LABELS[q.level] || '';
const kp = q.knowledge_point || q.knowledge || '';
const st = states[q.id] || 'unstudied';
let html = `<div class="q-card">
<div class="q-meta">
<span class="q-id">Q${q.id}</span>
<span class="q-tag ${typeInfo.cls}">${typeInfo.txt}</span>
${lvl ? `<span class="q-tag level">${lvl}</span>` : ''}
<button class="q-state-btn ${st}" id="qStateBtn" onclick="cycleState()" title="点击手动切换:未做 → 已掌握 → 错题">${STATE_LABELS[st]} <span style="opacity:.5;font-weight:400;">▾</span></button>
${kp ? `<span class="q-knowledge" title="${escapeHtml(kp)}">🏷️ ${escapeHtml(kp)}</span>` : ''}
</div>
<div class="q-text">${renderMD(q.prompt || q.question)}</div>`;
if (q.type === 'single_choice') {
html += `<div class="opts" id="qOpts">`;
(q.options || []).forEach((opt, i) => {
const m = opt.match(/^([A-Z])[\.\、]\s*(.+)/);
const letter = m ? m[1] : String.fromCharCode(65 + i);
const text = m ? m[2] : opt;
html += `<div class="opt" data-key="${letter}" onclick="chooseOpt(this,'${letter}',${q.id})">
<span class="letter">${letter}</span><span class="text">${renderMD(text)}</span>
</div>`;
});
html += `</div>`;
} else if (q.type === 'true_false') {
html += `<div class="opts" id="qOpts">
<div class="opt" data-key="True" onclick="chooseOpt(this,'True',${q.id})"><span class="letter">✓</span><span class="text">正确</span></div>
<div class="opt" data-key="False" onclick="chooseOpt(this,'False',${q.id})"><span class="letter">✗</span><span class="text">错误</span></div>
</div>`;
} else if (q.type === 'fill_blank') {
html += `<input class="q-input short" id="qInput" placeholder="在这里输入你的答案..." />`;
} else if (q.type === 'short_answer') {
html += `<textarea class="q-input long" id="qInput" placeholder="在这里写下你的回答..."></textarea>`;
}
if (q.type === 'fill_blank' || q.type === 'short_answer') {
html += `<div class="q-actions">
<button class="btn-submit" id="btnSubmit" onclick="submitTextAnswer(${q.id})">提交 (Enter)</button>
<button class="btn-reveal" onclick="revealAnswer(${q.id})">查看答案</button>
<button class="btn-reset" onclick="resetQ(${q.id})">↻ 重做</button>
</div>`;
} else {
html += `<div class="q-actions">
<button class="btn-submit" id="btnSubmit" onclick="submitChoice(${q.id})" disabled>提交 (Enter)</button>
<button class="btn-reveal" onclick="revealAnswer(${q.id})">查看答案</button>
<button class="btn-reset" onclick="resetQ(${q.id})">↻ 重做</button>
</div>`;
}
html += `<div class="auto-state-hint" id="autoHint"></div>
<div class="analysis" id="analysis">
<div class="ans-line" id="ansLine"></div>
<div class="ans-text" id="ansText"></div>
<div class="tip" id="tipBox"></div>
<div class="kw" id="kwDetail"></div>
</div>
</div>`;
document.getElementById('rightInner').innerHTML = html;
// 恢复已答状态
if (answered[q.id] !== undefined) {
if (q.type === 'single_choice' || q.type === 'true_false') {
const el = document.querySelector(`#qOpts .opt[data-key="${answered[q.id]}"]`);
if (el) showChoiceAnswer(q, answered[q.id]);
} else {
const inp = document.getElementById('qInput');
if (inp) { inp.value = answered[q.id]; inp.disabled = true; }
showTextAnswer(q, answered[q.id]);
}
}
}
/* ===== 选择题 / 判断题作答 =====
* 新逻辑:选择 ≠ 提交
* - 点选项:仅高亮,不锁定,不判分
* - 点【提交】或按 Enter判分答错首次仅提示「再想想」并允许重选
* - 答错第二次:才显示答案 + 标错题
*/
window.__pendingChoice = null;
window.__wrongTriedOnce = {}; // {id: true} 记录这题是否已经错过一次
window.chooseOpt = function (el, key, id) {
// 已最终提交过的不能再选
if (answered[id] !== undefined) return;
window.__pendingChoice = key;
document.querySelectorAll('#qOpts .opt').forEach(o => {
o.classList.remove('selected');
});
el.classList.add('selected');
// 启用提交按钮
const btn = document.getElementById('btnSubmit');
if (btn) btn.disabled = false;
};
window.submitChoice = function (id) {
const q = QS.find(x => x.id === id);
if (!q) return;
const key = window.__pendingChoice;
if (!key) { showRetryHint('先选一个选项呀~ 😊'); return; }
const stdU = String(q.answer || '').toUpperCase();
const correct = key.toUpperCase() === stdU;
if (correct) {
answered[id] = key;
window.__wrongTriedOnce[id] = false;
showChoiceAnswer(q, key, true);
autoUpdateState(q, true);
} else {
if (!window.__wrongTriedOnce[id]) {
// 第一次错:给一次重试机会,不锁,不标错题
window.__wrongTriedOnce[id] = true;
const errOpt = document.querySelector(`#qOpts .opt[data-key="${key}"]`);
if (errOpt) {
errOpt.classList.add('wrong-shake');
setTimeout(() => errOpt.classList.remove('wrong-shake'), 600);
}
showRetryHint('🤔 不对哦,再想想?(这次错不算,可以重选)');
window.__pendingChoice = null;
if (errOpt) errOpt.classList.remove('selected');
return;
}
// 第二次错:真正锁定 + 标错题
answered[id] = key;
showChoiceAnswer(q, key, false);
autoUpdateState(q, false);
}
};
function showRetryHint(msg) {
const hint = document.getElementById('autoHint');
hint.className = 'auto-state-hint retry';
hint.textContent = msg;
// 自动消失
clearTimeout(window.__retryTimer);
window.__retryTimer = setTimeout(() => {
if (hint.classList.contains('retry')) {
hint.className = 'auto-state-hint';
hint.textContent = '';
}
}, 3500);
}
function showChoiceAnswer(q, chosen, correct) {
const stdU = String(q.answer || '').toUpperCase();
document.querySelectorAll('#qOpts .opt').forEach(el => {
el.classList.add('disabled');
el.classList.remove('selected');
const k = el.dataset.key.toUpperCase();
if (k === stdU) el.classList.add('correct');
if (k === String(chosen).toUpperCase() && k !== stdU) el.classList.add('wrong');
});
// 提交按钮锁死
const btn = document.getElementById('btnSubmit');
if (btn) { btn.disabled = true; btn.textContent = '已提交'; }
showAnalysis(q, chosen, correct);
}
/* ===== 填空 / 简答作答 ===== */
window.submitTextAnswer = function (id) {
const q = QS.find(x => x.id === id);
if (!q) return;
const inp = document.getElementById('qInput');
const userAns = (inp && inp.value || '').trim();
if (!userAns) { alert('先写点答案再提交呀~ ✍️'); return; }
answered[id] = userAns;
inp.disabled = true;
document.getElementById('btnSubmit').disabled = true;
document.getElementById('btnSubmit').textContent = '已提交';
if (q.type === 'fill_blank') {
const correct = fuzzyMatch(userAns, q.answer);
showAnalysis(q, userAns, correct);
autoUpdateState(q, correct);
} else {
// 简答题:不强判
showAnalysis(q, userAns, null);
// 状态不动,由用户手动判定(点 q-state-btn
saveState();
}
};
window.revealAnswer = function (id) {
const q = QS.find(x => x.id === id);
if (!q) return;
showAnalysis(q, answered[id] || '', null);
};
function showTextAnswer(q, userAns) {
if (q.type === 'fill_blank') {
const correct = fuzzyMatch(userAns, q.answer);
showAnalysis(q, userAns, correct);
} else {
showAnalysis(q, userAns, null);
}
const btn = document.getElementById('btnSubmit');
if (btn) { btn.disabled = true; btn.textContent = '已提交'; }
}
function fuzzyMatch(a, b) {
if (a === null || b === null || a === undefined || b === undefined) return false;
const norm = s => String(s).replace(/[\s\.,,。、;;:!?""''""()]/g, '').toLowerCase();
return norm(a) === norm(b);
}
/* ===== 显示解析 ===== */
function showAnalysis(q, userAns, correct) {
const an = document.getElementById('analysis');
const line = document.getElementById('ansLine');
const txt = document.getElementById('ansText');
const tip = document.getElementById('tipBox');
const kw = document.getElementById('kwDetail');
an.classList.remove('correct', 'wrong');
let lineHtml = '';
if (q.type === 'short_answer') {
lineHtml = `📝 参考答案:${renderMD(String(q.answer || ''))}`;
} else if (correct) {
an.classList.add('correct');
lineHtml = `🎉 答对啦!正确答案:<strong>${renderMD(String(q.answer || ''))}</strong>`;
} else if (correct === false) {
an.classList.add('wrong');
lineHtml = `❌ 你的答案是 <span style="text-decoration:line-through;opacity:.7;">${escapeHtml(userAns)}</span>,正确答案是 <strong>${renderMD(String(q.answer || ''))}</strong>`;
} else {
lineHtml = `📌 正确答案:<strong>${renderMD(String(q.answer || ''))}</strong>`;
}
line.innerHTML = lineHtml;
txt.innerHTML = q.explanation ? '<strong>💡 解析:</strong><br>' + renderMD(q.explanation) : '';
if (q.memory_tip) { tip.innerHTML = '🧠 ' + renderMD(q.memory_tip); tip.classList.add('visible'); }
else { tip.classList.remove('visible'); tip.innerHTML = ''; }
const knp = q.knowledge_point || q.knowledge;
kw.innerHTML = knp ? '📚 知识点:' + escapeHtml(knp) : '';
an.classList.add('visible');
}
/* ===== 状态自动 / 手动 ===== */
function autoUpdateState(q, correct) {
const prev = states[q.id] || 'unstudied';
if (prev !== 'unstudied') { saveState(); return; } // 已有状态不覆盖
states[q.id] = correct ? 'mastered' : 'error';
saveState();
const hint = document.getElementById('autoHint');
if (correct) {
hint.className = 'auto-state-hint mastered';
hint.textContent = '✅ 回答正确,状态自动更新为「已掌握」';
} else {
hint.className = 'auto-state-hint error';
hint.textContent = '❌ 这题答错了,已加入「错题本」~ 别灰心,多看看解析就掌握啦';
}
document.getElementById('qStateBtn').className = 'q-state-btn ' + states[q.id];
document.getElementById('qStateBtn').textContent = STATE_LABELS[states[q.id]];
applyStatesToDOM();
applyFilters();
}
window.cycleState = function () {
if (!currentId) return;
const order = ['unstudied', 'mastered', 'error'];
const cur = states[currentId] || 'unstudied';
const next = order[(order.indexOf(cur) + 1) % order.length];
states[currentId] = next;
saveState();
document.getElementById('qStateBtn').className = 'q-state-btn ' + next;
document.getElementById('qStateBtn').textContent = STATE_LABELS[next];
applyStatesToDOM();
applyFilters();
};
window.resetQ = function (id) {
states[id] = 'unstudied';
delete answered[id];
saveState();
selectQ(id);
applyStatesToDOM();
applyFilters();
};
/* ===== 导航 ===== */
function updateNav() {
const info = document.getElementById('navInfo');
const prev = document.getElementById('prevBtn');
const next = document.getElementById('nextBtn');
if (!currentId || filteredIds.length === 0) { info.textContent = ''; prev.disabled = true; next.disabled = true; return; }
const idx = filteredIds.indexOf(currentId);
if (idx < 0) { info.textContent = `当前题在筛选外`; prev.disabled = true; next.disabled = true; return; }
info.textContent = `${idx + 1} / ${filteredIds.length}(当前筛选)`;
prev.disabled = idx <= 0;
next.disabled = idx >= filteredIds.length - 1;
}
window.goPrev = function () { const i = filteredIds.indexOf(currentId); if (i > 0) selectQ(filteredIds[i - 1]); };
window.goNext = function () { const i = filteredIds.indexOf(currentId); if (i >= 0 && i < filteredIds.length - 1) selectQ(filteredIds[i + 1]); };
/* ===== 随机抽题 ===== */
window.randomQ = function () {
if (filteredIds.length === 0) { alert('当前筛选下没有题目 🙃'); return; }
selectQ(filteredIds[Math.floor(Math.random() * filteredIds.length)]);
};
/* ===== 导入导出 ===== */
window.exportProgress = function () {
const data = { meta: META, states, answered, exportedAt: new Date().toISOString() };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `quiz_progress_${(META.id || 'quiz')}_${Date.now()}.json`; a.click();
URL.revokeObjectURL(url);
};
let importMode = 'questions';
window.showImportModal = function (mode) {
importMode = mode;
document.getElementById('importTitle').textContent = mode === 'questions' ? '📥 导入题库' : '📂 导入进度';
document.getElementById('importHint').textContent = mode === 'questions'
? '粘贴题目 JSON 数组(会覆盖当前题库)'
: '粘贴之前导出的进度 JSON';
document.getElementById('importText').value = '';
document.getElementById('importText').placeholder = mode === 'questions'
? '[\n {"type":"single_choice","prompt":"...","options":["A. ...","B. ..."],"answer":"A","explanation":"...","category":"数学","knowledge_point":"分数"}\n]'
: '{"states":{...},"answered":{...}}';
document.getElementById('importModal').classList.add('visible');
};
window.hideImportModal = function () { document.getElementById('importModal').classList.remove('visible'); };
window.doImport = function () {
const txt = document.getElementById('importText').value.trim();
if (!txt) { alert('先粘贴点东西嘛 😊'); return; }
try {
const obj = JSON.parse(txt);
if (importMode === 'questions') {
if (!Array.isArray(obj)) { alert('题目必须是数组格式'); return; }
window.__QUIZ_DATA__ = obj;
states = {}; answered = {};
localStorage.removeItem(STORAGE_KEY);
QS = obj.map((q, i) => Object.assign({ id: i + 1 }, q));
QS.forEach(q => { states[q.id] = 'unstudied'; });
buildCatChips();
buildSidebar();
applyFilters();
updateStats();
hideImportModal();
alert(`✅ 导入 ${obj.length} 道题`);
} else {
if (!obj.states) { alert('进度文件格式不对'); return; }
states = obj.states;
answered = obj.answered || {};
saveState();
applyStatesToDOM();
applyFilters();
hideImportModal();
alert('✅ 进度已恢复');
}
} catch (e) {
alert('❌ JSON 解析失败:' + e.message);
}
};
window.showResetModal = function () { document.getElementById('resetModal').classList.add('visible'); };
window.hideResetModal = function () { document.getElementById('resetModal').classList.remove('visible'); };
window.doReset = function () {
states = {}; answered = {};
QS.forEach(q => { states[q.id] = 'unstudied'; });
saveState();
applyStatesToDOM();
applyFilters();
hideResetModal();
document.getElementById('rightInner').innerHTML = '<div class="empty-state"><div class="icon">✨</div><div>已重置,重新开始!</div></div>';
document.getElementById('navBar').style.display = 'none';
currentId = null;
};
/* ===== 主题 ===== */
window.toggleTheme = function () {
const isDark = document.body.classList.contains('dark');
document.body.classList.toggle('dark', !isDark);
document.body.classList.toggle('light', isDark);
document.querySelector('.theme-toggle').textContent = isDark ? '☀️' : '🌙';
try { localStorage.setItem(THEME_KEY, isDark ? 'light' : 'dark'); } catch (e) {}
};
/* ===== 侧栏(移动) ===== */
window.toggleSidebar = function () {
document.getElementById('sidebar').classList.toggle('hidden');
};
/* ============================================
* 模拟考模式
* ============================================ */
let examConfig = { count: 10, timeLimit: 0 };
let examQuestions = [];
let examAnswers = {};
let examTimerInterval = null;
let examTimeLeft = 0;
let examStartTime = 0;
window.selectExamCount = function (n) {
examConfig.count = n;
document.querySelectorAll('#examModal [data-n]').forEach(b => {
b.classList.toggle('selected', parseInt(b.dataset.n, 10) === n);
});
};
window.selectExamTime = function (t) {
examConfig.timeLimit = t;
document.querySelectorAll('#examModal [data-t]').forEach(b => {
b.classList.toggle('selected', parseInt(b.dataset.t, 10) === t);
});
};
window.showExamModal = function () {
const pool = QS.filter(q => activeCats.has(getCatKey(q)));
document.getElementById('examPoolSize').textContent = `当前题池:${pool.length} 题(按学科筛选)`;
document.getElementById('examModal').classList.add('visible');
};
window.hideExamModal = function () { document.getElementById('examModal').classList.remove('visible'); };
window.startExam = function () {
hideExamModal();
const pool = QS.filter(q => activeCats.has(getCatKey(q))).slice();
if (pool.length === 0) { alert('题池为空'); return; }
// shuffle
for (let i = pool.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[pool[i], pool[j]] = [pool[j], pool[i]];
}
examQuestions = pool.slice(0, Math.min(examConfig.count, pool.length));
examAnswers = {};
examStartTime = Date.now();
let html = '';
examQuestions.forEach((q, idx) => {
const typeInfo = TYPE_LABELS[q.type] || { txt: '', cls: '' };
html += `<div class="exam-q-card" id="eq-${q.id}">
<div class="exam-q-num">第 ${idx + 1} 题 · ${typeInfo.txt}</div>
<div class="exam-q-text">${renderMD(q.prompt || q.question)}</div>`;
if (q.type === 'single_choice') {
html += `<div class="exam-opts">`;
(q.options || []).forEach((opt, i) => {
const m = opt.match(/^([A-Z])[\.\、]\s*(.+)/);
const letter = m ? m[1] : String.fromCharCode(65 + i);
const text = m ? m[2] : opt;
html += `<div class="exam-opt" data-qid="${q.id}" data-key="${letter}" onclick="examChoose(this,${q.id},'${letter}')">
<span class="letter">${letter}</span><span>${renderMD(text)}</span>
</div>`;
});
html += `</div>`;
} else if (q.type === 'true_false') {
html += `<div class="exam-opts">
<div class="exam-opt" data-qid="${q.id}" data-key="True" onclick="examChoose(this,${q.id},'True')"><span class="letter">✓</span><span>正确</span></div>
<div class="exam-opt" data-qid="${q.id}" data-key="False" onclick="examChoose(this,${q.id},'False')"><span class="letter">✗</span><span>错误</span></div>
</div>`;
} else {
const tag = q.type === 'short_answer' ? 'textarea class="exam-input" rows="3"' : 'input class="exam-input"';
html += `<${tag} data-qid="${q.id}" oninput="examTextInput(${q.id},this.value)" placeholder="输入答案..."></${q.type === 'short_answer' ? 'textarea' : 'input'}>`;
}
html += `</div>`;
});
document.getElementById('examBody').innerHTML = html;
updateExamProgress();
document.getElementById('examOverlay').classList.add('visible');
document.getElementById('examSubmitBtn').disabled = false;
document.getElementById('examSubmitBtn').textContent = '📤 提交答卷';
// 计时
if (examTimerInterval) clearInterval(examTimerInterval);
if (examConfig.timeLimit > 0) {
examTimeLeft = examConfig.timeLimit * examQuestions.length;
updateExamTimer();
examTimerInterval = setInterval(() => {
examTimeLeft--;
updateExamTimer();
if (examTimeLeft <= 0) { clearInterval(examTimerInterval); submitExam(); }
}, 1000);
} else {
updateElapsed();
examTimerInterval = setInterval(updateElapsed, 1000);
}
};
function updateExamTimer() {
const el = document.getElementById('examTimer');
const m = Math.floor(examTimeLeft / 60), s = examTimeLeft % 60;
el.textContent = '⏱ ' + m + ':' + (s < 10 ? '0' : '') + s;
el.style.color = examTimeLeft <= 10 ? '#ff5d77' : '';
}
function updateElapsed() {
const e = Math.floor((Date.now() - examStartTime) / 1000);
const m = Math.floor(e / 60), s = e % 60;
document.getElementById('examTimer').textContent = '⏱ ' + m + ':' + (s < 10 ? '0' : '') + s;
}
window.examChoose = function (el, qid, key) {
examAnswers[qid] = key;
document.querySelectorAll(`#eq-${qid} .exam-opt`).forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
updateExamProgress();
};
window.examTextInput = function (qid, val) {
examAnswers[qid] = val;
updateExamProgress();
};
function updateExamProgress() {
const total = examQuestions.length;
const done = Object.keys(examAnswers).filter(k => String(examAnswers[k] || '').trim() !== '').length;
document.getElementById('examProgressText').textContent = `${done} / ${total}`;
document.getElementById('examProgressFill').style.width = (total > 0 ? (done / total * 100) : 0) + '%';
}
window.submitExam = function () {
if (examTimerInterval) clearInterval(examTimerInterval);
document.getElementById('examOverlay').classList.remove('visible');
showResults();
};
window.quitExam = function () {
if (!confirm('确定退出本次模拟考?答题进度会丢失')) return;
if (examTimerInterval) clearInterval(examTimerInterval);
document.getElementById('examOverlay').classList.remove('visible');
};
function showResults() {
let correct = 0, gradable = 0;
const items = [];
examQuestions.forEach((q, idx) => {
const userAns = examAnswers[q.id];
let isCorrect = null;
if (q.type === 'single_choice' || q.type === 'true_false') {
gradable++;
isCorrect = String(userAns || '').toUpperCase() === String(q.answer || '').toUpperCase();
if (isCorrect) correct++;
} else if (q.type === 'fill_blank') {
gradable++;
isCorrect = fuzzyMatch(userAns, q.answer);
if (isCorrect) correct++;
}
// 同步主答题状态
if (isCorrect === true) states[q.id] = 'mastered';
else if (isCorrect === false) states[q.id] = 'error';
if (userAns !== undefined) answered[q.id] = userAns;
items.push({ q, userAns, isCorrect, idx });
});
saveState();
applyStatesToDOM();
applyFilters();
const score = gradable > 0 ? Math.round(correct / gradable * 100) : 0;
const passed = score >= 60;
const scoreEl = document.getElementById('resultsScore');
scoreEl.textContent = score;
scoreEl.className = 'results-score ' + (passed ? 'pass' : 'fail');
document.getElementById('resultsDetail').textContent =
`${examQuestions.length} 题 · 答对 ${correct}/${gradable}${gradable < examQuestions.length ? ` · ${examQuestions.length - gradable} 题需自评` : ''}`;
let html = '';
items.forEach(it => {
const { q, userAns, isCorrect, idx } = it;
const cls = isCorrect === true ? 'is-correct' : isCorrect === false ? 'is-wrong' : '';
const badge = isCorrect === true
? '<span class="result-badge correct">✓ 正确</span>'
: isCorrect === false
? '<span class="result-badge wrong">✗ 错误</span>'
: '<span class="result-badge" style="background:#fff4e0;color:#e9a82c;">📝 待自评</span>';
let ansHtml = '';
if (q.type === 'single_choice' || q.type === 'true_false' || q.type === 'fill_blank') {
if (isCorrect) {
ansHtml = `<div class="result-answers">✓ 你答:<span class="correct-ans">${escapeHtml(String(userAns || ''))}</span></div>`;
} else {
ansHtml = `<div class="result-answers">你答:<span class="wrong-ans">${escapeHtml(String(userAns || '(未作答)'))}</span> · 正确答案:<span class="correct-ans">${renderMD(String(q.answer || ''))}</span></div>`;
}
} else {
ansHtml = `<div class="result-answers">你答:${escapeHtml(String(userAns || '(未作答)'))}</div>
<div class="result-answers">参考:<span class="correct-ans">${renderMD(String(q.answer || ''))}</span></div>`;
}
html += `<div class="result-item ${cls}">
<div class="result-top">
<span class="result-qid">第${idx + 1}题 · Q${q.id}</span>
${badge}
</div>
<div class="result-text">${renderMD(q.prompt || q.question)}</div>
${ansHtml}
${q.explanation ? `<div class="result-analysis">💡 ${renderMD(q.explanation)}</div>` : ''}
${q.memory_tip ? `<div class="result-tip">🧠 ${renderMD(q.memory_tip)}</div>` : ''}
</div>`;
});
document.getElementById('resultsBody').innerHTML = html;
document.getElementById('examResults').classList.add('visible');
}
window.closeResults = function () { document.getElementById('examResults').classList.remove('visible'); };
window.retakeExam = function () { closeResults(); startExam(); };
/* ===== 工具 ===== */
function escapeHtml(s) {
if (s === null || s === undefined) return '';
return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
function renderMD(s) {
if (s === null || s === undefined) return '';
return escapeHtml(s)
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code>$1</code>')
.replace(/\n/g, '<br>')
.replace(/____+/g, '<span style="display:inline-block;min-width:80px;border-bottom:2px solid currentColor;margin:0 4px;opacity:.5;">&nbsp;</span>');
}
/* ===== 键盘快捷键 ===== */
document.addEventListener('keydown', (e) => {
// 在 input/textarea 内时,只允许 Enter 触发提交,其它不拦截
const inExam = document.getElementById('examOverlay').classList.contains('visible');
const inResults = document.getElementById('examResults').classList.contains('visible');
const inModal = document.querySelector('.modal-overlay.visible');
if (inExam || inResults || inModal) {
// 模拟考也支持 Enter 提交
if (inExam && e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
submitExam();
}
return;
}
const tag = (e.target.tagName || '').toLowerCase();
const inField = tag === 'input' || tag === 'textarea';
// Enter 提交(在文本框里只有 Ctrl/Cmd+Enter
if (e.key === 'Enter') {
if (inField && !(e.ctrlKey || e.metaKey)) return;
e.preventDefault();
const btn = document.getElementById('btnSubmit');
if (btn && !btn.disabled) btn.click();
return;
}
if (inField) return; // 文本框内不再处理其它快捷键
// A/B/C/D 选项
if (/^[a-dA-D]$/.test(e.key)) {
const letter = e.key.toUpperCase();
const el = document.querySelector(`#qOpts .opt[data-key="${letter}"]`);
if (el && !el.classList.contains('disabled')) { el.click(); e.preventDefault(); }
return;
}
// T/F 判断题
if (e.key === 't' || e.key === 'T') {
const el = document.querySelector('#qOpts .opt[data-key="True"]');
if (el && !el.classList.contains('disabled')) { el.click(); e.preventDefault(); }
return;
}
if (e.key === 'f' || e.key === 'F') {
const el = document.querySelector('#qOpts .opt[data-key="False"]');
if (el && !el.classList.contains('disabled')) { el.click(); e.preventDefault(); }
return;
}
// 上下题
if (e.key === 'ArrowLeft') { goPrev(); e.preventDefault(); return; }
if (e.key === 'ArrowRight') { goNext(); e.preventDefault(); return; }
// Space: 查看答案
if (e.key === ' ') {
if (currentId !== null) { revealAnswer(currentId); e.preventDefault(); }
return;
}
});
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>