Files
TermiNexus 14902a404d chore(ccgo): 以 Vue3 做题站替换 TempLate/CCGO 原生页面并清理 www 残留
CCGO 构建产物部署至 TempLate/CCGO(旧文件移至 backup/ccgo);GVSite 已迁移残留的 Invite/Errors 旧页移至 backup/www;.gitignore 排除新工程
2026-08-11 01:41:39 +08:00

1856 lines
61 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>计算机素养评测</title>
<style>
:root {
--bg-primary: #ffffff;
--bg-secondary: #f0f4f8;
--bg-tertiary: #e8edf3;
--text-primary: #0d1b2a;
--text-secondary: #415a77;
--text-muted: #778da9;
--accent: #1b3a6b;
--accent-bright: #2563eb;
--accent-light: #dbeafe;
--accent-hover: #1d4ed8;
--border: #c9d1db;
--border-light: #e2e8f0;
--card-bg: #ffffff;
--card-shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.06);
--card-shadow-hover: 0 4px 12px rgba(0,0,0,0.1);
--option-bg: #f8fafc;
--option-hover: #eff6ff;
--option-selected: #dbeafe;
--option-selected-border: #2563eb;
--success: #16a34a;
--success-light: #dcfce7;
--warning: #d97706;
--warning-light: #fef3c7;
--danger: #dc2626;
--danger-light: #fee2e2;
--section-header-bg: #1b3a6b;
--section-header-text: #ffffff;
--qnum-bg: #f1f5f9;
--qnum-border: #cbd5e1;
--qnum-active-bg: #2563eb;
--qnum-active-text: #ffffff;
--qnum-answered-bg: #dbeafe;
--qnum-answered-border: #93c5fd;
--qnum-answered-text: #1d4ed8;
--scrollbar-track: #f1f5f9;
--scrollbar-thumb: #94a3b8;
--canvas-bg: #ffffff;
--code-bg: #1e293b;
--code-text: #e2e8f0;
--input-bg: #ffffff;
--input-border: #cbd5e1;
--input-focus: #2563eb;
--matching-line: #2563eb;
--tag-bg: #eff6ff;
--tag-text: #1d4ed8;
}
[data-theme="dark"] {
--bg-primary: #0b1120;
--bg-secondary: #111827;
--bg-tertiary: #1e293b;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent: #3b82f6;
--accent-bright: #60a5fa;
--accent-light: #1e3a5f;
--accent-hover: #2563eb;
--border: #334155;
--border-light: #1e293b;
--card-bg: #111827;
--card-shadow: 0 1px 3px rgba(0,0,0,0.3);
--card-shadow-hover: 0 4px 12px rgba(0,0,0,0.4);
--option-bg: #1e293b;
--option-hover: #1e3a5f;
--option-selected: #1e3a5f;
--option-selected-border: #3b82f6;
--success: #22c55e;
--success-light: #14532d;
--warning: #f59e0b;
--warning-light: #451a03;
--danger: #ef4444;
--danger-light: #450a0a;
--section-header-bg: #1e3a5f;
--section-header-text: #e2e8f0;
--qnum-bg: #1e293b;
--qnum-border: #334155;
--qnum-active-bg: #2563eb;
--qnum-active-text: #ffffff;
--qnum-answered-bg: #1e3a5f;
--qnum-answered-border: #3b82f6;
--qnum-answered-text: #60a5fa;
--scrollbar-track: #1e293b;
--scrollbar-thumb: #475569;
--canvas-bg: #1e293b;
--code-bg: #0f172a;
--code-text: #e2e8f0;
--input-bg: #1e293b;
--input-border: #334155;
--input-focus: #3b82f6;
--matching-line: #60a5fa;
--tag-bg: #1e3a5f;
--tag-text: #60a5fa;
}
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
html{font-size:14px;scroll-behavior:smooth}
body{
font-family:"Microsoft YaHei","PingFang SC","Noto Sans SC",sans-serif;
background:var(--bg-secondary);
color:var(--text-primary);
line-height:1.6;
min-height:100vh;
transition:background-color .3s,color .3s;
}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-track{background:var(--scrollbar-track)}
::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb);border-radius:3px}
.header{
position:sticky;top:0;z-index:100;
background:var(--accent);
color:#fff;
padding:0 24px;
height:52px;
display:flex;align-items:center;justify-content:space-between;
box-shadow:0 2px 8px rgba(0,0,0,0.15);
border-bottom:3px solid var(--accent-bright);
}
.header-left{display:flex;align-items:center;gap:16px}
.header-title{font-size:18px;font-weight:700;letter-spacing:1px}
.header-sub{font-size:12px;opacity:.8;font-weight:400}
.header-right{display:flex;align-items:center;gap:12px}
.timer{
display:flex;align-items:center;gap:6px;
background:rgba(255,255,255,.15);
padding:4px 14px;border-radius:2px;
font-size:13px;font-variant-numeric:tabular-nums;
}
.timer-icon{font-size:16px}
.btn-tilt{
display:inline-block;
transform:skewX(-8deg);
border:none;cursor:pointer;
transition:all .15s;
}
.btn-tilt-inner{
display:inline-block;
transform:skewX(8deg);
font-size:13px;font-weight:600;
white-space:nowrap;
}
.btn-tilt-primary{
background:#fff;color:var(--accent);
padding:6px 20px;
clip-path:polygon(4px 0,100% 0,calc(100% - 4px) 100%,0 100%);
}
.btn-tilt-primary:hover{background:#e0e7ff}
.btn-tilt-danger{
background:rgba(239,68,68,.9);color:#fff;
padding:6px 20px;
clip-path:polygon(4px 0,100% 0,calc(100% - 4px) 100%,0 100%);
}
.btn-tilt-danger:hover{background:rgba(220,38,38,.95)}
.btn-tilt-ghost{
background:rgba(255,255,255,.1);color:#fff;
padding:6px 16px;
clip-path:polygon(3px 0,100% 0,calc(100% - 3px) 100%,0 100%);
}
.btn-tilt-ghost:hover{background:rgba(255,255,255,.2)}
.theme-toggle{
width:36px;height:36px;border-radius:2px;
background:rgba(255,255,255,.12);border:none;
color:#fff;cursor:pointer;font-size:18px;
display:flex;align-items:center;justify-content:center;
transition:background .15s;
}
.theme-toggle:hover{background:rgba(255,255,255,.25)}
.main-layout{
display:flex;
max-width:1400px;
margin:0 auto;
padding:20px 24px;
gap:20px;
min-height:calc(100vh - 52px);
}
.answer-area{
flex:1;
min-width:0;
}
.question-card{
width:260px;
flex-shrink:0;
position:sticky;
top:72px;
max-height:calc(100vh - 92px);
overflow-y:auto;
background:var(--card-bg);
border:1px solid var(--border);
border-radius:2px;
box-shadow:var(--card-shadow);
}
.card-header{
background:var(--accent);
color:#fff;
padding:10px 16px;
font-size:14px;font-weight:700;
display:flex;align-items:center;justify-content:space-between;
clip-path:polygon(0 0,100% 0,100% calc(100% - 4px),calc(100% - 12px) 100%,0 100%);
position:relative;
}
.card-header::after{
content:'';position:absolute;bottom:0;right:0;
width:12px;height:4px;background:rgba(255,255,255,.3);
}
.card-stat{font-size:12px;opacity:.85;font-weight:400}
.card-section{padding:10px 12px 6px}
.card-section-title{
font-size:12px;font-weight:700;
color:var(--text-secondary);
padding:4px 0;
border-bottom:1px solid var(--border-light);
margin-bottom:8px;
display:flex;align-items:center;justify-content:space-between;
}
.card-section-count{font-weight:400;color:var(--text-muted);font-size:11px}
.card-nums{display:flex;flex-wrap:wrap;gap:6px;padding-bottom:6px}
.card-num{
width:30px;height:30px;
display:flex;align-items:center;justify-content:center;
font-size:12px;font-weight:600;
background:var(--qnum-bg);
border:1px solid var(--qnum-border);
border-radius:2px;
cursor:pointer;
transition:all .15s;
color:var(--text-primary);
clip-path:polygon(2px 0,100% 0,calc(100% - 2px) 100%,0 100%);
}
.card-num:hover{border-color:var(--accent-bright);background:var(--option-hover)}
.card-num.active{
background:var(--qnum-active-bg);
color:var(--qnum-active-text);
border-color:var(--qnum-active-bg);
}
.card-num.answered{
background:var(--qnum-answered-bg);
color:var(--qnum-answered-text);
border-color:var(--qnum-answered-border);
}
.card-progress{
padding:8px 12px 12px;
border-top:1px solid var(--border-light);
}
.progress-bar{
height:4px;background:var(--bg-tertiary);border-radius:2px;overflow:hidden;
}
.progress-fill{height:100%;background:var(--accent-bright);border-radius:2px;transition:width .3s}
.progress-text{font-size:11px;color:var(--text-muted);margin-top:4px;text-align:right}
.section-block{margin-bottom:24px}
.section-header{
background:var(--section-header-bg);
color:var(--section-header-text);
padding:10px 20px;
font-size:14px;font-weight:700;
display:flex;align-items:center;justify-content:space-between;
clip-path:polygon(0 0,100% 0,100% calc(100% - 3px),calc(100% - 16px) 100%,0 100%);
margin-bottom:2px;
position:relative;
}
.section-header::before{
content:'';
position:absolute;left:0;top:0;bottom:0;
width:4px;
background:var(--accent-bright);
}
.section-info{font-size:12px;font-weight:400;opacity:.85}
.question-item{
background:var(--card-bg);
border:1px solid var(--border-light);
padding:20px 24px;
margin-bottom:2px;
transition:background .15s,border-color .15s;
}
.question-item:last-child{margin-bottom:0}
.question-item.active-question{border-color:var(--accent-bright);border-left:3px solid var(--accent-bright)}
.question-item.current-view{
box-shadow:inset 3px 0 0 var(--accent-bright);
}
.q-header{
display:flex;align-items:flex-start;gap:10px;
margin-bottom:14px;
}
.q-number{
flex-shrink:0;
min-width:28px;height:28px;
display:flex;align-items:center;justify-content:center;
background:var(--accent);
color:#fff;
font-size:13px;font-weight:700;
clip-path:polygon(3px 0,100% 0,calc(100% - 3px) 100%,0 100%);
}
.q-title{flex:1;font-size:14px;line-height:1.7;font-weight:500}
.q-score{
flex-shrink:0;
font-size:12px;
color:var(--text-muted);
background:var(--bg-tertiary);
padding:2px 8px;
border-radius:2px;
font-weight:600;
}
.q-body{padding-left:38px}
.option-item{
display:flex;align-items:center;gap:10px;
padding:10px 14px;
margin-bottom:4px;
background:var(--option-bg);
border:1px solid var(--border-light);
border-radius:2px;
cursor:pointer;
transition:all .15s;
user-select:none;
clip-path:polygon(3px 0,100% 0,calc(100% - 3px) 100%,0 100%);
}
.option-item:hover{background:var(--option-hover);border-color:var(--accent-bright)}
.option-item.selected{
background:var(--option-selected);
border-color:var(--option-selected-border);
}
.option-marker{
flex-shrink:0;
width:24px;height:24px;
display:flex;align-items:center;justify-content:center;
border:2px solid var(--border);
border-radius:50%;
font-size:12px;font-weight:700;
color:var(--text-secondary);
transition:all .15s;
}
.option-item.selected .option-marker{
background:var(--accent-bright);
border-color:var(--accent-bright);
color:#fff;
}
.option-item[data-type="checkbox"] .option-marker{border-radius:2px}
.option-text{flex:1;font-size:13px;line-height:1.5}
.tf-group{display:flex;gap:12px;padding-left:38px}
.tf-btn{
flex:1;max-width:200px;
padding:12px 20px;
border:2px solid var(--border);
background:var(--option-bg);
border-radius:2px;
cursor:pointer;
font-size:14px;font-weight:600;
text-align:center;
transition:all .15s;
clip-path:polygon(4px 0,100% 0,calc(100% - 4px) 100%,0 100%);
color:var(--text-primary);
}
.tf-btn:hover{border-color:var(--accent-bright);background:var(--option-hover)}
.tf-btn.selected{border-color:var(--accent-bright);background:var(--option-selected);color:var(--accent-bright)}
.fill-input{
display:inline-block;
width:120px;
border:none;
border-bottom:2px solid var(--accent-bright);
background:transparent;
text-align:center;
font-size:14px;
padding:2px 4px;
color:var(--text-primary);
outline:none;
font-family:inherit;
}
.fill-input:focus{border-bottom-color:var(--accent-hover)}
.matching-area{padding-left:38px}
.matching-container{display:flex;gap:40px;position:relative}
.matching-col{flex:1;display:flex;flex-direction:column;gap:8px}
.matching-col-title{
font-size:12px;font-weight:700;
color:var(--text-muted);
text-transform:uppercase;
letter-spacing:1px;
margin-bottom:4px;
}
.match-item{
padding:10px 14px;
background:var(--option-bg);
border:1px solid var(--border);
border-radius:2px;
cursor:pointer;
transition:all .15s;
font-size:13px;
clip-path:polygon(2px 0,100% 0,calc(100% - 2px) 100%,0 100%);
}
.match-item:hover{border-color:var(--accent-bright);background:var(--option-hover)}
.match-item.match-selected{
border-color:var(--accent-bright);
background:var(--option-selected);
box-shadow:0 0 0 2px rgba(37,99,235,.2);
}
.match-item.match-paired{opacity:.5;pointer-events:none}
.match-pairs{
margin-top:12px;
padding:10px;
background:var(--bg-tertiary);
border-radius:2px;
font-size:12px;
}
.match-pair-row{
display:flex;align-items:center;gap:8px;
padding:4px 0;
border-bottom:1px solid var(--border-light);
}
.match-pair-row:last-child{border-bottom:none}
.match-pair-arrow{color:var(--accent-bright);font-weight:700}
.match-pair-remove{
margin-left:auto;cursor:pointer;
color:var(--danger);font-size:12px;
padding:2px 6px;
}
.match-pair-remove:hover{background:var(--danger-light);border-radius:2px}
.sorting-area{padding-left:38px}
.sort-list{display:flex;flex-direction:column;gap:6px}
.sort-item{
display:flex;align-items:center;gap:10px;
padding:10px 14px;
background:var(--option-bg);
border:1px solid var(--border);
border-radius:2px;
cursor:grab;
transition:all .15s;
font-size:13px;
user-select:none;
}
.sort-item:hover{border-color:var(--accent-bright);background:var(--option-hover)}
.sort-item.sort-dragging{opacity:.5;border-style:dashed}
.sort-handle{color:var(--text-muted);font-size:16px;cursor:grab}
.sort-index{
width:22px;height:22px;
display:flex;align-items:center;justify-content:center;
background:var(--accent);
color:#fff;
font-size:11px;font-weight:700;
border-radius:2px;
flex-shrink:0;
}
.categorize-area{padding-left:38px}
.categorize-items{
display:flex;flex-wrap:wrap;gap:8px;
margin-bottom:16px;
padding:12px;
background:var(--bg-tertiary);
border-radius:2px;
min-height:40px;
}
.categorize-chip{
padding:6px 14px;
background:var(--option-bg);
border:1px solid var(--border);
border-radius:2px;
font-size:12px;
cursor:pointer;
transition:all .15s;
clip-path:polygon(2px 0,100% 0,calc(100% - 2px) 100%,0 100%);
}
.categorize-chip:hover{border-color:var(--accent-bright)}
.categorize-chip.cat-selected{
border-color:var(--accent-bright);
background:var(--option-selected);
}
.categorize-chip.cat-placed{opacity:.4;pointer-events:none}
.categorize-groups{display:flex;flex-direction:column;gap:10px}
.categorize-group{
border:1px solid var(--border);
border-radius:2px;
overflow:hidden;
}
.categorize-group-header{
padding:8px 14px;
background:var(--accent);
color:#fff;
font-size:12px;font-weight:700;
}
.categorize-group-body{
padding:10px;
min-height:40px;
display:flex;flex-wrap:wrap;gap:6px;
background:var(--option-bg);
}
.categorize-group-chip{
padding:4px 10px;
background:var(--tag-bg);
color:var(--tag-text);
border-radius:2px;
font-size:11px;
cursor:pointer;
}
.categorize-group-chip:hover{text-decoration:line-through}
.text-answer{
width:100%;min-height:100px;
padding:10px 14px;
border:1px solid var(--input-border);
border-radius:2px;
background:var(--input-bg);
color:var(--text-primary);
font-size:13px;
font-family:inherit;
line-height:1.6;
resize:vertical;
outline:none;
transition:border-color .15s;
}
.text-answer:focus{border-color:var(--input-focus)}
.text-answer.tall{min-height:200px}
.text-answer.essay{min-height:300px}
.material-block{
padding:14px 18px;
background:var(--bg-tertiary);
border-left:3px solid var(--accent-bright);
border-radius:0 2px 2px 0;
margin-bottom:16px;
font-size:13px;
line-height:1.8;
}
.material-label{
font-size:11px;font-weight:700;
color:var(--accent-bright);
text-transform:uppercase;
letter-spacing:1px;
margin-bottom:6px;
}
.sub-question{
margin-top:16px;
padding-top:12px;
border-top:1px dashed var(--border);
}
.sub-q-label{
font-size:12px;font-weight:700;
color:var(--accent-bright);
margin-bottom:8px;
}
.drawing-area{padding-left:38px}
.drawing-toolbar{
display:flex;gap:6px;margin-bottom:8px;
flex-wrap:wrap;align-items:center;
}
.draw-tool{
padding:4px 12px;
background:var(--option-bg);
border:1px solid var(--border);
border-radius:2px;
font-size:12px;
cursor:pointer;
transition:all .15s;
color:var(--text-primary);
}
.draw-tool:hover{border-color:var(--accent-bright)}
.draw-tool.tool-active{background:var(--accent-bright);color:#fff;border-color:var(--accent-bright)}
.draw-color{
width:24px;height:24px;
border:2px solid var(--border);
border-radius:2px;
cursor:pointer;
padding:0;
}
.draw-color.color-active{border-color:var(--accent-bright);box-shadow:0 0 0 2px rgba(37,99,235,.3)}
.drawing-canvas{
border:1px solid var(--border);
background:var(--canvas-bg);
cursor:crosshair;
display:block;
border-radius:2px;
max-width:100%;
}
[data-theme="dark"] .draw-color{border-color:var(--border)}
[data-theme="dark"] .drawing-canvas{background:#1a2332}
.code-area{padding-left:38px}
.code-editor{
width:100%;min-height:200px;
padding:14px;
background:var(--code-bg);
color:var(--code-text);
border:1px solid var(--border);
border-radius:2px;
font-family:"Cascadia Code","Fira Code","JetBrains Mono","Consolas",monospace;
font-size:13px;
line-height:1.6;
resize:vertical;
outline:none;
tab-size:4;
}
.code-desc{
padding:12px 16px;
background:var(--bg-tertiary);
border-radius:2px;
margin-bottom:12px;
font-size:13px;
line-height:1.7;
}
.switch-nav{
display:flex;align-items:center;justify-content:center;
gap:12px;
padding:16px;
background:var(--card-bg);
border:1px solid var(--border-light);
margin-top:8px;
}
.switch-nav .btn-tilt{clip-path:polygon(4px 0,100% 0,calc(100% - 4px) 100%,0 100%)}
.switch-nav .btn-tilt-accent{
background:var(--accent-bright);color:#fff;
padding:8px 24px;
}
.switch-nav .btn-tilt-accent:hover{background:var(--accent-hover)}
.switch-nav .btn-tilt-outline{
background:var(--option-bg);color:var(--text-primary);
padding:8px 20px;
border:1px solid var(--border);
}
.switch-nav .btn-tilt-outline:hover{border-color:var(--accent-bright)}
.switch-indicator{
font-size:12px;color:var(--text-muted);
font-variant-numeric:tabular-nums;
}
.btn-tilt-outline{
background:var(--option-bg);color:var(--text-primary);
padding:6px 16px;
clip-path:polygon(3px 0,100% 0,calc(100% - 3px) 100%,0 100%);
border:1px solid var(--border);
}
.btn-tilt-outline:hover{border-color:var(--accent-bright)}
.submit-bar{
position:sticky;bottom:0;
background:var(--card-bg);
border-top:1px solid var(--border);
padding:10px 24px;
display:flex;align-items:center;justify-content:space-between;
z-index:50;
}
.submit-info{font-size:12px;color:var(--text-muted)}
.submit-actions{display:flex;gap:10px}
.modal-overlay{
display:none;
position:fixed;top:0;left:0;right:0;bottom:0;
background:rgba(0,0,0,.5);
z-index:200;
justify-content:center;align-items:center;
}
.modal-overlay.show{display:flex}
.modal-box{
background:var(--card-bg);
border:1px solid var(--border);
border-radius:2px;
width:90%;max-width:500px;
box-shadow:0 20px 60px rgba(0,0,0,.3);
clip-path:polygon(0 0,100% 0,100% calc(100% - 6px),calc(100% - 20px) 100%,0 100%);
}
.modal-header{
padding:14px 20px;
background:var(--accent);
color:#fff;
font-size:14px;font-weight:700;
}
.modal-body{padding:20px;font-size:13px;line-height:1.7}
.modal-footer{
padding:12px 20px;
display:flex;justify-content:flex-end;gap:10px;
border-top:1px solid var(--border-light);
}
.word-count{
font-size:11px;color:var(--text-muted);
text-align:right;margin-top:4px;
}
@media(max-width:900px){
.main-layout{flex-direction:column;padding:12px}
.question-card{width:100%;position:static;max-height:none}
.header{padding:0 12px}
.header-sub{display:none}
}
</style>
</head>
<body>
<header class="header">
<div class="header-left">
<div>
<div class="header-title" id="examTitle">计算机素养评测</div>
<div class="header-sub" id="examSubtitle">全国计算机应用水平考试</div>
</div>
</div>
<div class="header-right">
<div class="timer">
<span class="timer-icon">&#9201;</span>
<span id="timerDisplay">02:00:00</span>
</div>
<button class="btn-tilt btn-tilt-ghost" onclick="toggleMode()">
<span class="btn-tilt-inner" id="modeToggleText">切换模式</span>
</button>
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn" title="切换主题">&#9790;</button>
<button class="btn-tilt btn-tilt-danger" onclick="showSubmitModal()">
<span class="btn-tilt-inner">交卷</span>
</button>
</div>
</header>
<div class="main-layout">
<div class="answer-area" id="answerArea"></div>
<aside class="question-card" id="questionCard"></aside>
</div>
<div class="modal-overlay" id="submitModal">
<div class="modal-box">
<div class="modal-header">确认交卷</div>
<div class="modal-body" id="submitModalBody">确定要提交试卷吗?</div>
<div class="modal-footer">
<button class="btn-tilt btn-tilt-outline" onclick="closeSubmitModal()">
<span class="btn-tilt-inner">继续答题</span>
</button>
<button class="btn-tilt btn-tilt-danger" onclick="submitExam()">
<span class="btn-tilt-inner">确认交卷</span>
</button>
</div>
</div>
</div>
<script>
const EXAM_DATA = {
title: "计算机素养评测",
subtitle: "全国计算机应用水平考试",
mode: "switch",
timeLimit: 120,
sections: [
{
type: "single_choice",
title: "单项选择题",
description: "每题只有一个正确答案",
questions: [
{
id: 1, question: "以下哪个不是计算机的基本组成部分?",
options: ["中央处理器(CPU)", "内存(RAM)", "硬盘(HDD)", "电源适配器"],
score: 2, answer: "D"
},
{
id: 2, question: "Python是一种什么类型的编程语言?",
options: ["编译型语言", "解释型语言", "机器语言", "汇编语言"],
score: 2, answer: "B"
},
{
id: 3, question: "在Windows操作系统中,哪个快捷键可以复制选中的内容?",
options: ["Ctrl+X", "Ctrl+C", "Ctrl+V", "Ctrl+Z"],
score: 2, answer: "B"
},
{
id: 4, question: "在计算机中,1KB等于多少字节?",
options: ["1000", "1024", "10000", "1048576"],
score: 2, answer: "B"
}
]
},
{
type: "multi_choice",
title: "多项选择题",
description: "每题有两个或以上正确答案",
questions: [
{
id: 5, question: "以下哪些是计算机的输入设备?",
options: ["键盘", "鼠标", "显示器", "打印机", "扫描仪"],
score: 3, answer: ["A", "B", "E"]
},
{
id: 6, question: "以下哪些是Python的可变数据类型?",
options: ["int", "list", "tuple", "dict", "str"],
score: 3, answer: ["B", "D"]
}
]
},
{
type: "indefinite_choice",
title: "不定项选择题",
description: "正确答案数量不确定(1个或多个)",
questions: [
{
id: 7, question: "关于TCP/IP协议,以下说法正确的是?",
options: [
"TCP是面向连接的协议",
"IP负责数据包的路由",
"UDP是可靠传输协议",
"HTTP基于TCP协议"
],
score: null, answer: ["A", "B", "D"]
},
{
id: 8, question: "以下哪些属于操作系统?",
options: ["Windows", "Linux", "Python", "macOS"],
score: null, answer: ["A", "B", "D"]
}
]
},
{
type: "true_false",
title: "判断正误",
description: "判断下列说法是否正确",
questions: [
{ id: 9, question: "计算机病毒是一种生物病毒。", score: 1, answer: false },
{ id: 10, question: "HTTP协议默认使用80端口。", score: 1, answer: true },
{ id: 11, question: "RAM中的数据在断电后会丢失。", score: 1, answer: true }
]
},
{
type: "fill_blank",
title: "填空题",
description: "在横线处填入正确答案",
questions: [
{
id: 12, question: "计算机的三大核心部件是____、____和____。",
blanks: 3, score: 3, answer: ["CPU", "内存", "硬盘"]
},
{
id: 13, question: "IP地址由____位二进制数组成(IPv4),其中C类地址的第一个字节范围是____到____。",
blanks: 3, score: null, answer: ["32", "192", "223"]
}
]
},
{
type: "matching",
title: "连线匹配题",
description: "将左侧项目与右侧正确描述连线",
questions: [
{
id: 14, score: 4,
left: ["CPU", "RAM", "SSD", "GPU"],
right: ["图形处理", "临时存储", "永久存储", "中央运算"],
answer: { "CPU": "中央运算", "RAM": "临时存储", "SSD": "永久存储", "GPU": "图形处理" }
}
]
},
{
type: "sorting",
title: "排序题",
description: "将下列项目按正确顺序排列",
questions: [
{
id: 15, score: 4,
question: "请将计算机启动过程的步骤按正确顺序排列:",
items: ["加载操作系统", "POST自检", "BIOS初始化", "显示桌面"],
answer: [2, 1, 0, 3]
}
]
},
{
type: "categorize",
title: "归类题",
description: "将下列项目归入正确的类别",
questions: [
{
id: 16, score: 5,
categories: ["输入设备", "输出设备", "存储设备"],
items: ["键盘", "显示器", "U盘", "打印机", "鼠标", "硬盘"],
answer: {
"输入设备": ["键盘", "鼠标"],
"输出设备": ["显示器", "打印机"],
"存储设备": ["U盘", "硬盘"]
}
}
]
},
{
type: "short_answer",
title: "简答题",
description: "简要回答下列问题",
questions: [
{
id: 17, question: "简述什么是操作系统及其主要功能。",
score: 5, answer: null
}
]
},
{
type: "material_analysis",
title: "材料分析题",
description: "阅读材料后回答问题",
questions: [
{
id: 18, score: null,
material: "某公司网络系统遭受攻击,攻击者通过钓鱼邮件获取了员工账号密码,进而入侵公司内部服务器,窃取了大量客户数据。事后调查发现,公司未部署防火墙,员工安全意识薄弱,密码策略过于简单。",
subQuestions: [
{ id: "18a", question: "此次攻击使用了哪种社会工程学手段?", type: "short_answer" },
{ id: "18b", question: "请列举至少三项该公司应采取的安全改进措施。", type: "short_answer" },
{ id: "18c", question: "以下哪项不是防范此类攻击的有效措施?", type: "single_choice", options: ["定期更换密码", "部署防火墙", "关闭所有网络端口", "安全意识培训"], answer: "C" }
]
}
]
},
{
type: "drawing",
title: "作图题",
description: "在画布上完成作图",
questions: [
{
id: 19, score: 6,
question: "请绘制一个简单的星型拓扑网络结构图,包含中心节点和5个终端节点。"
}
]
},
{
type: "case_analysis",
title: "案例分析题",
description: "分析案例并回答问题",
questions: [
{
id: 20, score: null,
case: "小明是一名大学生,他发现自己的电脑运行越来越慢。经检查,电脑开机启动项多达30个,C盘剩余空间不足5%,同时安装了多个杀毒软件。此外,他习惯将所有文件保存在桌面,从不整理。",
questions: [
{ id: "20a", question: "分析小明电脑变慢的可能原因。", type: "short_answer" },
{ id: "20b", question: "请给出优化建议。", type: "short_answer" }
]
}
]
},
{
type: "writing",
title: "写作题",
description: "根据要求撰写短文",
questions: [
{
id: 21, score: 10,
question: "请以“人工智能对教育的影响”为主题,撰写一篇不少于200字的短文,阐述你的观点。",
minWords: 200
}
]
},
{
type: "qa",
title: "问答题",
description: "回答下列问题",
questions: [
{
id: 22, question: "什么是云计算?它有哪些主要服务模式?请分别说明。",
score: 8, answer: null
}
]
},
{
type: "essay",
title: "作文",
description: "根据要求写作",
questions: [
{
id: 23, score: 20,
question: "请以“信息技术与未来社会”为题,写一篇议论文。要求:观点明确,论据充分,条理清晰,不少于500字。",
minWords: 500
}
]
},
{
type: "code_practice",
title: "代码实操题",
description: "编写代码完成要求",
questions: [
{
id: 24, score: 15,
question: "编写一个Python函数",
description: "编写一个名为 `is_palindrome` 的函数,接收一个字符串参数,判断该字符串是否为回文字符串(正读和反读相同),返回布尔值。\n\n示例:\nis_palindrome('abcba') → True\nis_palindrome('hello') → False\n\n注意:忽略大小写和空格。",
language: "python"
}
]
}
]
};
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const state = {
mode: EXAM_DATA.mode || 'switch',
currentQId: null,
answers: {},
timer: EXAM_DATA.timeLimit * 60,
timerInterval: null,
theme: localStorage.getItem('exam-theme') || (matchMedia('(prefers-color-scheme:dark)').matches ? 'dark' : 'light'),
allQuestions: [],
questionMap: {},
matchState: {},
sortState: {},
catState: {},
drawingState: {}
};
function init() {
buildQuestionIndex();
applyTheme();
renderAll();
startTimer();
if (state.mode === 'switch') {
navigateToQuestion(state.allQuestions[0].id);
}
if (state.mode === 'continuous') {
setupScrollObserver();
}
}
function setupScrollObserver() {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const qid = parseInt(entry.target.dataset.qid);
if (qid) {
state.currentQId = qid;
document.querySelectorAll('.question-item.current-view').forEach(el => el.classList.remove('current-view'));
entry.target.classList.add('current-view');
renderQuestionCard();
}
}
});
}, { threshold: 0.3, rootMargin: '-60px 0px -40% 0px' });
document.querySelectorAll('.question-item').forEach(el => observer.observe(el));
}
function buildQuestionIndex() {
state.allQuestions = [];
state.questionMap = {};
EXAM_DATA.sections.forEach(sec => {
sec.questions.forEach(q => {
const qData = { ...q, type: sec.type, sectionTitle: sec.title };
state.allQuestions.push(qData);
state.questionMap[q.id] = qData;
if (sec.type === 'material_analysis' && q.subQuestions) {
q.subQuestions.forEach(sq => {
state.questionMap[sq.id] = { ...sq, type: sq.type || 'short_answer', parentType: 'material_analysis', parentId: q.id };
});
}
if (sec.type === 'case_analysis' && q.questions) {
q.questions.forEach(sq => {
state.questionMap[sq.id] = { ...sq, type: sq.type || 'short_answer', parentType: 'case_analysis', parentId: q.id };
});
}
});
});
if (state.allQuestions.length > 0) {
state.currentQId = state.allQuestions[0].id;
}
}
function renderAll() {
renderAnswerArea();
renderQuestionCard();
}
function renderAnswerArea() {
const area = document.getElementById('answerArea');
if (state.mode === 'continuous') {
area.innerHTML = EXAM_DATA.sections.map((sec, si) => renderSection(sec, si)).join('');
} else {
const q = state.questionMap[state.currentQId];
if (!q) { area.innerHTML = ''; return; }
const sec = EXAM_DATA.sections.find(s => s.questions.some(sq => sq.id === q.id));
area.innerHTML = renderSingleQuestion(q, sec);
renderSwitchNav(area);
}
postRender();
}
function renderSection(sec, si) {
const totalScore = sec.questions.reduce((s, q) => s + (q.score || 0), 0);
const scoreText = sec.questions.some(q => q.score === null) ? '' : `,合计${totalScore}分`;
let html = `<div class="section-block" data-section="${si}">
<div class="section-header">
<span>${sec.title}</span>
<span class="section-info">共${sec.questions.length}题${scoreText}${sec.description ? ' · ' + sec.description : ''}</span>
</div>`;
sec.questions.forEach(q => {
html += renderSingleQuestion(q, sec);
});
html += '</div>';
return html;
}
function renderSingleQuestion(q, sec) {
const qData = state.questionMap[q.id] || q;
const type = qData.type || (sec ? sec.type : '');
const scoreHtml = q.score !== null && q.score !== undefined ? `<span class="q-score">${q.score}分</span>` : '';
let bodyHtml = '';
switch (type) {
case 'single_choice': bodyHtml = renderSingleChoice(q); break;
case 'multi_choice': bodyHtml = renderMultiChoice(q); break;
case 'indefinite_choice': bodyHtml = renderIndefiniteChoice(q); break;
case 'true_false': bodyHtml = renderTrueFalse(q); break;
case 'fill_blank': bodyHtml = renderFillBlank(q); break;
case 'matching': bodyHtml = renderMatching(q); break;
case 'sorting': bodyHtml = renderSorting(q); break;
case 'categorize': bodyHtml = renderCategorize(q); break;
case 'short_answer': bodyHtml = renderShortAnswer(q); break;
case 'material_analysis': bodyHtml = renderMaterialAnalysis(q); break;
case 'drawing': bodyHtml = renderDrawing(q); break;
case 'case_analysis': bodyHtml = renderCaseAnalysis(q); break;
case 'writing': bodyHtml = renderWriting(q); break;
case 'qa': bodyHtml = renderQA(q); break;
case 'essay': bodyHtml = renderEssay(q); break;
case 'code_practice': bodyHtml = renderCodePractice(q); break;
default: bodyHtml = '<div class="q-body"><p style="color:var(--text-muted)">未知题型</p></div>';
}
return `<div class="question-item" id="q-${q.id}" data-qid="${q.id}" data-type="${type}">
<div class="q-header">
<span class="q-number">${q.id}</span>
<span class="q-title">${q.question || ''}</span>
${scoreHtml}
</div>
${bodyHtml}
</div>`;
}
function renderSingleChoice(q) {
const ans = state.answers[q.id];
return `<div class="q-body">${q.options.map((opt, i) => {
const letter = LETTERS[i];
const sel = ans === letter ? ' selected' : '';
return `<div class="option-item${sel}" data-qid="${q.id}" data-val="${letter}" data-type="radio" onclick="selectOption(this,'${q.id}','${letter}','single_choice')">
<span class="option-marker">${letter}</span>
<span class="option-text">${opt}</span>
</div>`;
}).join('')}</div>`;
}
function renderMultiChoice(q) {
const ans = state.answers[q.id] || [];
return `<div class="q-body">${q.options.map((opt, i) => {
const letter = LETTERS[i];
const sel = ans.includes(letter) ? ' selected' : '';
return `<div class="option-item${sel}" data-qid="${q.id}" data-val="${letter}" data-type="checkbox" onclick="toggleOption(this,'${q.id}','${letter}','multi_choice')">
<span class="option-marker">${letter}</span>
<span class="option-text">${opt}</span>
</div>`;
}).join('')}</div>`;
}
function renderIndefiniteChoice(q) {
const ans = state.answers[q.id] || [];
return `<div class="q-body">${q.options.map((opt, i) => {
const letter = LETTERS[i];
const sel = ans.includes(letter) ? ' selected' : '';
return `<div class="option-item${sel}" data-qid="${q.id}" data-val="${letter}" data-type="checkbox" onclick="toggleOption(this,'${q.id}','${letter}','indefinite_choice')">
<span class="option-marker">${letter}</span>
<span class="option-text">${opt}</span>
</div>`;
}).join('')}</div>`;
}
function renderTrueFalse(q) {
const ans = state.answers[q.id];
return `<div class="tf-group">
<div class="tf-btn${ans === true ? ' selected' : ''}" onclick="selectTF(${q.id},true)">&#10004; 正确</div>
<div class="tf-btn${ans === false ? ' selected' : ''}" onclick="selectTF(${q.id},false)">&#10008; 错误</div>
</div>`;
}
function renderFillBlank(q) {
const ans = state.answers[q.id] || [];
let parts = q.question.split('____');
let html = '<div class="q-body"><div style="line-height:2.2">';
parts.forEach((part, i) => {
html += part;
if (i < q.blanks) {
const val = ans[i] || '';
html += `<input type="text" class="fill-input" data-qid="${q.id}" data-bidx="${i}" value="${escHtml(val)}" oninput="fillBlankInput(this,${q.id},${i})">`;
}
});
html += '</div></div>';
return html;
}
function renderMatching(q) {
if (!state.matchState[q.id]) state.matchState[q.id] = { selected: null, selectedSide: null, pairs: {} };
const ms = state.matchState[q.id];
const pairedLeft = Object.keys(ms.pairs);
const pairedRight = Object.values(ms.pairs);
let html = `<div class="matching-area">
<div class="matching-container">
<div class="matching-col">
<div class="matching-col-title">项目</div>
${q.left.map((item, i) => {
const isPaired = pairedLeft.includes(item);
const isSel = ms.selected === item && !isPaired;
return `<div class="match-item${isSel ? ' match-selected' : ''}${isPaired ? ' match-paired' : ''}" data-qid="${q.id}" data-side="left" data-val="${escAttr(item)}" onclick="matchClick(${q.id},'left','${escAttr(item)}')">${LETTERS[i]}. ${item}</div>`;
}).join('')}
</div>
<div class="matching-col">
<div class="matching-col-title">描述</div>
${q.right.map((item, i) => {
const isPaired = pairedRight.includes(item);
const isSel = ms.selected === item && !isPaired;
return `<div class="match-item${isSel ? ' match-selected' : ''}${isPaired ? ' match-paired' : ''}" data-qid="${q.id}" data-side="right" data-val="${escAttr(item)}" onclick="matchClick(${q.id},'right','${escAttr(item)}')">${i + 1}. ${item}</div>`;
}).join('')}
</div>
</div>`;
if (Object.keys(ms.pairs).length > 0) {
html += `<div class="match-pairs">`;
for (const [l, r] of Object.entries(ms.pairs)) {
html += `<div class="match-pair-row">
<span>${l}</span>
<span class="match-pair-arrow">&#8594;</span>
<span>${r}</span>
<span class="match-pair-remove" onclick="matchRemove(${q.id},'${escAttr(l)}')">&#10005;</span>
</div>`;
}
html += '</div>';
}
html += '</div>';
return html;
}
function renderSorting(q) {
if (!state.sortState[q.id]) state.sortState[q.id] = { order: q.items.map((_, i) => i), userChanged: false };
const ss = state.sortState[q.id];
let html = `<div class="sorting-area">
<div class="sort-list" data-qid="${q.id}">`;
ss.order.forEach((itemIdx, pos) => {
html += `<div class="sort-item" data-qid="${q.id}" data-idx="${itemIdx}" draggable="true">
<span class="sort-handle">&#9776;</span>
<span class="sort-index">${pos + 1}</span>
<span class="option-text">${q.items[itemIdx]}</span>
</div>`;
});
html += '</div></div>';
return html;
}
function renderCategorize(q) {
if (!state.catState[q.id]) state.catState[q.id] = { placed: {}, selected: null };
const cs = state.catState[q.id];
const placedItems = Object.values(cs.placed).flat();
let html = `<div class="categorize-area">
<div class="categorize-items">`;
q.items.forEach(item => {
const isPlaced = placedItems.includes(item);
const isSel = cs.selected === item;
html += `<span class="categorize-chip${isPlaced ? ' cat-placed' : ''}${isSel ? ' cat-selected' : ''}" data-qid="${q.id}" data-item="${escAttr(item)}" onclick="catSelectItem(${q.id},'${escAttr(item)}')">${item}</span>`;
});
html += '</div><div class="categorize-groups">';
q.categories.forEach(cat => {
const items = cs.placed[cat] || [];
html += `<div class="categorize-group">
<div class="categorize-group-header">${cat}</div>
<div class="categorize-group-body" data-qid="${q.id}" data-cat="${escAttr(cat)}" onclick="catPlaceItem(${q.id},'${escAttr(cat)}')">`;
items.forEach(item => {
html += `<span class="categorize-group-chip" onclick="event.stopPropagation();catRemoveItem(${q.id},'${escAttr(cat)}','${escAttr(item)}')">${item}</span>`;
});
html += '</div></div>';
});
html += '</div></div>';
return html;
}
function renderShortAnswer(q) {
const ans = state.answers[q.id] || '';
return `<div class="q-body"><textarea class="text-answer" data-qid="${q.id}" oninput="textAnswerInput(this,${q.id})">${escHtml(ans)}</textarea></div>`;
}
function renderMaterialAnalysis(q) {
let html = `<div class="q-body">
<div class="material-block">
<div class="material-label">阅读材料</div>
${q.material || q.case || ''}
</div>`;
const subQs = q.subQuestions || q.questions || [];
subQs.forEach((sq, i) => {
html += `<div class="sub-question">
<div class="sub-q-label">(${LETTERS[i]}) ${sq.question}</div>`;
if (sq.type === 'single_choice' && sq.options) {
sq.options.forEach((opt, oi) => {
const letter = LETTERS[oi];
const ans = state.answers[sq.id];
const sel = ans === letter ? ' selected' : '';
html += `<div class="option-item${sel}" data-qid="${sq.id}" data-val="${letter}" data-type="radio" onclick="selectOption(this,'${sq.id}','${letter}','single_choice')">
<span class="option-marker">${letter}</span>
<span class="option-text">${opt}</span>
</div>`;
});
} else {
const ans = state.answers[sq.id] || '';
html += `<textarea class="text-answer" data-qid="${sq.id}" oninput="textAnswerInput(this,'${sq.id}')">${escHtml(ans)}</textarea>`;
}
html += '</div>';
});
html += '</div>';
return html;
}
function renderCaseAnalysis(q) {
return renderMaterialAnalysis(q);
}
function renderDrawing(q) {
if (!state.drawingState[q.id]) state.drawingState[q.id] = { tool: 'pen', color: '#000000', size: 2, hasDrawing: false };
const ds = state.drawingState[q.id];
const colors = ['#000000', '#dc2626', '#2563eb', '#16a34a', '#d97706', '#7c3aed'];
let html = `<div class="drawing-area">
<div class="drawing-toolbar">
<button class="draw-tool${ds.tool === 'pen' ? ' tool-active' : ''}" onclick="setDrawTool(${q.id},'pen')">画笔</button>
<button class="draw-tool${ds.tool === 'eraser' ? ' tool-active' : ''}" onclick="setDrawTool(${q.id},'eraser')">橡皮</button>
<button class="draw-tool" onclick="clearCanvas(${q.id})">清空</button>
<span style="margin:0 6px;color:var(--text-muted)">|</span>`;
colors.forEach(c => {
html += `<button class="draw-color${ds.color === c ? ' color-active' : ''}" style="background:${c}" onclick="setDrawColor(${q.id},'${c}')"></button>`;
});
html += `</div>
<canvas class="drawing-canvas" id="canvas-${q.id}" width="600" height="300" data-qid="${q.id}"></canvas>
</div>`;
return html;
}
function renderWriting(q) {
const ans = state.answers[q.id] || '';
const wc = ans.replace(/\s/g, '').length;
const minW = q.minWords || 0;
return `<div class="q-body">
<textarea class="text-answer tall" data-qid="${q.id}" oninput="textAnswerInput(this,${q.id});updateWordCount(${q.id},${minW})">${escHtml(ans)}</textarea>
<div class="word-count" id="wc-${q.id}">已输入 ${wc} 字${minW ? ' / 不少于 ' + minW + ' 字' : ''}</div>
</div>`;
}
function renderQA(q) {
const ans = state.answers[q.id] || '';
return `<div class="q-body"><textarea class="text-answer" data-qid="${q.id}" oninput="textAnswerInput(this,${q.id})">${escHtml(ans)}</textarea></div>`;
}
function renderEssay(q) {
const ans = state.answers[q.id] || '';
const wc = ans.replace(/\s/g, '').length;
const minW = q.minWords || 0;
return `<div class="q-body">
<textarea class="text-answer essay" data-qid="${q.id}" oninput="textAnswerInput(this,${q.id});updateWordCount(${q.id},${minW})">${escHtml(ans)}</textarea>
<div class="word-count" id="wc-${q.id}">已输入 ${wc} 字${minW ? ' / 不少于 ' + minW + ' 字' : ''}</div>
</div>`;
}
function renderCodePractice(q) {
const ans = state.answers[q.id] || '';
let html = `<div class="code-area">`;
if (q.description) {
html += `<div class="code-desc"><pre style="white-space:pre-wrap;margin:0;font-family:inherit">${escHtml(q.description)}</pre></div>`;
}
html += `<textarea class="code-editor" data-qid="${q.id}" oninput="textAnswerInput(this,${q.id})" spellcheck="false">${escHtml(ans)}</textarea></div>`;
return html;
}
function renderSwitchNav(area) {
const idx = state.allQuestions.findIndex(q => q.id === state.currentQId);
const total = state.allQuestions.length;
const nav = document.createElement('div');
nav.className = 'switch-nav';
nav.innerHTML = `
<button class="btn-tilt btn-tilt-outline" onclick="navigateToQuestion(state.allQuestions[${Math.max(0, idx - 1)}].id)" ${idx === 0 ? 'disabled style="opacity:.4"' : ''}>
<span class="btn-tilt-inner">上一题</span>
</button>
<span class="switch-indicator">${idx + 1} / ${total}</span>
<button class="btn-tilt btn-tilt-accent" onclick="navigateToQuestion(state.allQuestions[${Math.min(total - 1, idx + 1)}].id)" ${idx === total - 1 ? 'disabled style="opacity:.4"' : ''}>
<span class="btn-tilt-inner">下一题</span>
</button>`;
area.appendChild(nav);
}
function renderQuestionCard() {
const card = document.getElementById('questionCard');
let answeredCount = 0;
let totalCount = state.allQuestions.length;
let html = `<div class="card-header"><span>答题卡</span><span class="card-stat" id="cardStat">0/${totalCount}</span></div>`;
EXAM_DATA.sections.forEach(sec => {
html += `<div class="card-section">
<div class="card-section-title"><span>${sec.title}</span><span class="card-section-count">${sec.questions.length}题</span></div>
<div class="card-nums">`;
sec.questions.forEach(q => {
const isAnswered = isQuestionAnswered(q.id, sec.type);
if (isAnswered) answeredCount++;
const isActive = q.id === state.currentQId;
let cls = 'card-num';
if (isActive) cls += ' active';
else if (isAnswered) cls += ' answered';
html += `<span class="${cls}" data-qid="${q.id}" onclick="navigateToQuestion(${q.id})">${q.id}</span>`;
});
html += '</div></div>';
});
const pct = totalCount > 0 ? Math.round(answeredCount / totalCount * 100) : 0;
html += `<div class="card-progress">
<div class="progress-bar"><div class="progress-fill" style="width:${pct}%"></div></div>
<div class="progress-text">${answeredCount}/${totalCount} 已答 (${pct}%)</div>
</div>`;
card.innerHTML = html;
}
function isQuestionAnswered(qid, type) {
const ans = state.answers[qid];
if (ans === undefined || ans === null) return false;
if (type === 'single_choice' || type === 'true_false') return ans !== undefined;
if (type === 'multi_choice' || type === 'indefinite_choice') return Array.isArray(ans) && ans.length > 0;
if (type === 'fill_blank') return Array.isArray(ans) && ans.some(a => a && a.trim());
if (type === 'matching') { const ms = state.matchState[qid]; return ms && Object.keys(ms.pairs).length > 0; }
if (type === 'sorting') { const ss = state.sortState[qid]; return ss && ss.order && ss.userChanged; }
if (type === 'categorize') { const cs = state.catState[qid]; return cs && Object.keys(cs.placed).length > 0; }
if (type === 'short_answer' || type === 'writing' || type === 'qa' || type === 'essay' || type === 'code_practice') return typeof ans === 'string' && ans.trim().length > 0;
if (type === 'material_analysis' || type === 'case_analysis') {
const q = state.questionMap[qid];
const subQs = q.subQuestions || q.questions || [];
return subQs.some(sq => isQuestionAnswered(sq.id, sq.type));
}
if (type === 'drawing') return state.drawingState[qid] && state.drawingState[qid].hasDrawing;
return false;
}
function selectOption(el, qid, val, type) {
state.answers[qid] = val;
const container = el.closest('.q-body') || el.closest('.tf-group') || el.parentElement;
container.querySelectorAll('.option-item').forEach(o => o.classList.remove('selected'));
el.classList.add('selected');
renderQuestionCard();
autoJump(qid);
}
function toggleOption(el, qid, val, type) {
if (!state.answers[qid]) state.answers[qid] = [];
const idx = state.answers[qid].indexOf(val);
if (idx >= 0) state.answers[qid].splice(idx, 1);
else state.answers[qid].push(val);
state.answers[qid].sort();
el.classList.toggle('selected');
renderQuestionCard();
}
function selectTF(qid, val) {
state.answers[qid] = val;
const container = document.querySelector(`#q-${qid} .tf-group`);
if (container) {
container.querySelectorAll('.tf-btn').forEach(b => b.classList.remove('selected'));
if (val) container.querySelector('.tf-btn:first-child').classList.add('selected');
else container.querySelector('.tf-btn:last-child').classList.add('selected');
}
renderQuestionCard();
autoJump(qid);
}
function fillBlankInput(el, qid, bidx) {
if (!state.answers[qid]) state.answers[qid] = [];
state.answers[qid][bidx] = el.value;
renderQuestionCard();
}
function textAnswerInput(el, qid) {
state.answers[qid] = el.value;
renderQuestionCard();
}
function updateWordCount(qid, minWords) {
const ans = state.answers[qid] || '';
const wc = ans.replace(/\s/g, '').length;
const el = document.getElementById('wc-' + qid);
if (el) el.textContent = `已输入 ${wc} 字${minWords ? ' / 不少于 ' + minWords + ' 字' : ''}`;
}
function matchClick(qid, side, val) {
if (!state.matchState[qid]) state.matchState[qid] = { selected: null, pairs: {} };
const ms = state.matchState[qid];
if (ms.pairs[val]) return;
if (!ms.selected) {
ms.selected = val;
ms.selectedSide = side;
} else {
if (ms.selectedSide === side) {
ms.selected = val;
ms.selectedSide = side;
} else {
const leftVal = side === 'right' ? ms.selected : val;
const rightVal = side === 'right' ? val : ms.selected;
delete ms.pairs[Object.entries(ms.pairs).find(([k, v]) => v === rightVal)?.[0]];
delete ms.pairs[leftVal];
ms.pairs[leftVal] = rightVal;
ms.selected = null;
ms.selectedSide = null;
state.answers[qid] = { ...ms.pairs };
}
}
reRenderQuestion(qid);
renderQuestionCard();
}
function matchRemove(qid, leftVal) {
if (state.matchState[qid]) {
delete state.matchState[qid].pairs[leftVal];
state.answers[qid] = { ...state.matchState[qid].pairs };
reRenderQuestion(qid);
renderQuestionCard();
}
}
function reRenderQuestion(qid) {
const q = state.questionMap[qid];
if (!q) return;
const sec = EXAM_DATA.sections.find(s => s.questions.some(sq => sq.id === qid));
const el = document.getElementById('q-' + qid);
if (el) {
const newEl = document.createElement('div');
newEl.innerHTML = renderSingleQuestion(q, sec);
const newQ = newEl.firstElementChild;
el.replaceWith(newQ);
postRenderQuestion(newQ, qid);
} else if (state.mode === 'switch') {
renderAnswerArea();
postRender();
}
}
let dragSrcIdx = null;
function initSorting() {
document.querySelectorAll('.sort-list').forEach(list => {
const qid = parseInt(list.dataset.qid);
list.querySelectorAll('.sort-item').forEach(item => {
item.addEventListener('dragstart', e => {
dragSrcIdx = parseInt(item.dataset.idx);
item.classList.add('sort-dragging');
e.dataTransfer.effectAllowed = 'move';
});
item.addEventListener('dragend', () => {
item.classList.remove('sort-dragging');
dragSrcIdx = null;
});
item.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
item.addEventListener('drop', e => {
e.preventDefault();
const dropIdx = parseInt(item.dataset.idx);
if (dragSrcIdx !== null && dragSrcIdx !== dropIdx) {
if (!state.sortState[qid]) state.sortState[qid] = { order: [], userChanged: false };
const order = state.sortState[qid].order;
const srcPos = order.indexOf(dragSrcIdx);
const dropPos = order.indexOf(dropIdx);
order.splice(srcPos, 1);
order.splice(dropPos, 0, dragSrcIdx);
state.sortState[qid].userChanged = true;
state.answers[qid] = [...order];
reRenderQuestion(qid);
renderQuestionCard();
}
});
});
});
}
function catSelectItem(qid, item) {
if (!state.catState[qid]) state.catState[qid] = { placed: {}, selected: null };
const cs = state.catState[qid];
const placedItems = Object.values(cs.placed).flat();
if (placedItems.includes(item)) return;
cs.selected = cs.selected === item ? null : item;
reRenderQuestion(qid);
}
function catPlaceItem(qid, cat) {
if (!state.catState[qid]) return;
const cs = state.catState[qid];
if (!cs.selected) return;
if (!cs.placed[cat]) cs.placed[cat] = [];
cs.placed[cat].push(cs.selected);
state.answers[qid] = { ...cs.placed };
cs.selected = null;
reRenderQuestion(qid);
renderQuestionCard();
}
function catRemoveItem(qid, cat, item) {
if (!state.catState[qid]) return;
const cs = state.catState[qid];
if (cs.placed[cat]) {
cs.placed[cat] = cs.placed[cat].filter(i => i !== item);
if (cs.placed[cat].length === 0) delete cs.placed[cat];
}
state.answers[qid] = { ...cs.placed };
reRenderQuestion(qid);
renderQuestionCard();
}
function setDrawTool(qid, tool) {
if (!state.drawingState[qid]) state.drawingState[qid] = { tool: 'pen', color: '#000000', size: 2, hasDrawing: false };
state.drawingState[qid].tool = tool;
reRenderQuestion(qid);
setTimeout(() => initCanvas(qid), 50);
}
function setDrawColor(qid, color) {
if (!state.drawingState[qid]) state.drawingState[qid] = { tool: 'pen', color: '#000000', size: 2, hasDrawing: false };
state.drawingState[qid].color = color;
reRenderQuestion(qid);
setTimeout(() => initCanvas(qid), 50);
}
function clearCanvas(qid) {
const canvas = document.getElementById('canvas-' + qid);
if (canvas) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (state.drawingState[qid]) state.drawingState[qid].hasDrawing = false;
state.answers[qid] = null;
renderQuestionCard();
}
}
function initCanvas(qid) {
const canvas = document.getElementById('canvas-' + qid);
if (!canvas) return;
if (!state.drawingState[qid]) state.drawingState[qid] = { tool: 'pen', color: '#000000', size: 2, hasDrawing: false };
const ds = state.drawingState[qid];
const ctx = canvas.getContext('2d');
let drawing = false;
canvas.onmousedown = e => {
drawing = true;
ctx.beginPath();
const rect = canvas.getBoundingClientRect();
ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
};
canvas.onmousemove = e => {
if (!drawing) return;
const rect = canvas.getBoundingClientRect();
if (ds.tool === 'eraser') {
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = 20;
} else {
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = ds.color;
ctx.lineWidth = ds.size;
}
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
ctx.stroke();
if (state.drawingState[qid]) state.drawingState[qid].hasDrawing = true;
state.answers[qid] = 'drawn';
};
canvas.onmouseup = () => { drawing = false; };
canvas.onmouseleave = () => { drawing = false; };
}
function autoJump(qid) {
const autoTypes = ['single_choice', 'true_false'];
const q = state.questionMap[qid];
if (!q || !autoTypes.includes(q.type)) return;
const idx = state.allQuestions.findIndex(aq => aq.id === qid);
if (idx < state.allQuestions.length - 1) {
setTimeout(() => {
const nextId = state.allQuestions[idx + 1].id;
if (state.mode === 'switch') {
navigateToQuestion(nextId);
} else {
const nextEl = document.getElementById('q-' + nextId);
if (nextEl) nextEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
state.currentQId = nextId;
renderQuestionCard();
}
}, 400);
}
}
function navigateToQuestion(qid) {
state.currentQId = qid;
if (state.mode === 'switch') {
renderAnswerArea();
postRender();
} else {
const el = document.getElementById('q-' + qid);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
renderQuestionCard();
}
function toggleMode() {
state.mode = state.mode === 'switch' ? 'continuous' : 'switch';
document.getElementById('modeToggleText').textContent = state.mode === 'switch' ? '连贯模式' : '切换模式';
renderAnswerArea();
postRender();
renderQuestionCard();
if (state.mode === 'continuous' && state.currentQId) {
setupScrollObserver();
setTimeout(() => {
const el = document.getElementById('q-' + state.currentQId);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
}
}
function toggleTheme() {
state.theme = state.theme === 'dark' ? 'light' : 'dark';
applyTheme();
localStorage.setItem('exam-theme', state.theme);
}
function applyTheme() {
document.documentElement.setAttribute('data-theme', state.theme);
const btn = document.getElementById('themeBtn');
btn.innerHTML = state.theme === 'dark' ? '&#9788;' : '&#9790;';
}
function startTimer() {
if (state.timer <= 0) {
document.getElementById('timerDisplay').textContent = '不限时';
return;
}
updateTimerDisplay();
state.timerInterval = setInterval(() => {
state.timer--;
updateTimerDisplay();
if (state.timer <= 0) {
clearInterval(state.timerInterval);
submitExam();
}
}, 1000);
}
function updateTimerDisplay() {
const h = Math.floor(state.timer / 3600);
const m = Math.floor((state.timer % 3600) / 60);
const s = state.timer % 60;
document.getElementById('timerDisplay').textContent =
String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
function showSubmitModal() {
const total = state.allQuestions.length;
let answered = 0;
state.allQuestions.forEach(q => {
if (isQuestionAnswered(q.id, q.type)) answered++;
});
document.getElementById('submitModalBody').innerHTML =
`<p>您已完成 <strong>${answered}</strong>/${total} 题。</p>
<p style="margin-top:8px;color:var(--danger)">${total - answered > 0 ? '还有 ' + (total - answered) + ' 题未作答!' : '所有题目已作答。'}</p>
<p style="margin-top:8px">确定要提交试卷吗?</p>`;
document.getElementById('submitModal').classList.add('show');
}
function closeSubmitModal() {
document.getElementById('submitModal').classList.remove('show');
}
function submitExam() {
clearInterval(state.timerInterval);
closeSubmitModal();
const results = collectAnswers();
document.getElementById('answerArea').innerHTML = `
<div style="text-align:center;padding:60px 20px">
<div style="font-size:48px;margin-bottom:16px">&#10003;</div>
<h2 style="margin-bottom:8px">试卷已提交</h2>
<p style="color:var(--text-muted)">共 ${state.allQuestions.length} 题,已答 ${results.filter(r => r.answered).length} 题</p>
<pre style="text-align:left;margin-top:20px;padding:16px;background:var(--bg-tertiary);border-radius:4px;font-size:12px;max-height:400px;overflow:auto">${escHtml(JSON.stringify(results, null, 2))}</pre>
</div>`;
document.getElementById('questionCard').style.display = 'none';
}
function collectAnswers() {
return state.allQuestions.map(q => {
const ans = state.answers[q.id];
return {
id: q.id,
type: q.type,
answered: isQuestionAnswered(q.id, q.type),
answer: ans !== undefined ? ans : null
};
});
}
function postRender() {
initSorting();
state.allQuestions.forEach(q => {
if (q.type === 'drawing') initCanvas(q.id);
});
document.querySelectorAll('.code-editor').forEach(editor => {
editor.addEventListener('keydown', e => {
if (e.key === 'Tab') {
e.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 4;
const qid = editor.dataset.qid;
state.answers[qid] = editor.value;
}
});
});
}
function postRenderQuestion(el, qid) {
const q = state.questionMap[qid];
if (q && q.type === 'drawing') {
setTimeout(() => initCanvas(qid), 50);
}
if (q && q.type === 'sorting') {
el.querySelectorAll('.sort-item').forEach(item => {
item.addEventListener('dragstart', e => {
dragSrcIdx = parseInt(item.dataset.idx);
item.classList.add('sort-dragging');
e.dataTransfer.effectAllowed = 'move';
});
item.addEventListener('dragend', () => {
item.classList.remove('sort-dragging');
dragSrcIdx = null;
});
item.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
item.addEventListener('drop', e => {
e.preventDefault();
const dropIdx = parseInt(item.dataset.idx);
if (dragSrcIdx !== null && dragSrcIdx !== dropIdx) {
if (!state.sortState[qid]) state.sortState[qid] = { order: [], userChanged: false };
const order = state.sortState[qid].order;
const srcPos = order.indexOf(dragSrcIdx);
const dropPos = order.indexOf(dropIdx);
order.splice(srcPos, 1);
order.splice(dropPos, 0, dragSrcIdx);
state.sortState[qid].userChanged = true;
state.answers[qid] = [...order];
reRenderQuestion(qid);
renderQuestionCard();
}
});
});
}
}
function escHtml(s) {
if (typeof s !== 'string') return '';
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function escAttr(s) {
if (typeof s !== 'string') return '';
return s.replace(/'/g, "\\'").replace(/"/g, '\\"');
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>