CCGO 构建产物部署至 TempLate/CCGO(旧文件移至 backup/ccgo);GVSite 已迁移残留的 Invite/Errors 旧页移至 backup/www;.gitignore 排除新工程
1856 lines
61 KiB
HTML
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">⏱</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="切换主题">☾</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)">✔ 正确</div>
|
|
<div class="tf-btn${ans === false ? ' selected' : ''}" onclick="selectTF(${q.id},false)">✘ 错误</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">→</span>
|
|
<span>${r}</span>
|
|
<span class="match-pair-remove" onclick="matchRemove(${q.id},'${escAttr(l)}')">✕</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">☰</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' ? '☼' : '☾';
|
|
}
|
|
|
|
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">✓</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
function escAttr(s) {
|
|
if (typeof s !== 'string') return '';
|
|
return s.replace(/'/g, "\\'").replace(/"/g, '\\"');
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
</script>
|
|
</body>
|
|
</html>
|