好友
阅读权限10
听众
最后登录1970-1-1
|
Zzz0
发表于 2025-8-3 10:51
本帖最后由 Zzz0 于 2025-10-7 19:10 编辑
用DeepSeek改进了你的答题系统
增强版答题系统
源码
[HTML] 纯文本查看 复制代码 <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>增强版答题系统</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
color: #1f2937;
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin: 20px 0 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.85);
border-radius: 16px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
h1 {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(to right, #3B82F6, #8B5CF6);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.subtitle {
color: #64748b;
font-size: 1.2rem;
max-width: 700px;
margin: 0 auto;
}
.card {
background: #fff;
border-radius: 16px;
padding: 30px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.08);
margin-bottom: 25px;
border: 1px solid #eef2f6;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px -10px rgba(0, 0, 0, 0.15);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
padding: 14px 28px;
border-radius: 12px;
border: none;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.2);
font-size: 1.05rem;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 7px 14px rgba(59, 130, 246, 0.25);
}
.btn:active {
transform: translateY(1px);
}
.btn-primary {
background: #3B82F6;
color: #fff;
}
.btn-secondary {
background: #10B981;
color: #fff;
}
.btn-gray {
background: #e5e7eb;
color: #4b5563;
}
.export-btn {
background: linear-gradient(to right, #8e44ad, #9b59b6);
color: white;
border: none;
padding: 14px 28px;
border-radius: 12px;
cursor: pointer;
font-size: 1.05rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 10px;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(142, 68, 173, 0.3);
}
.export-btn:hover {
transform: translateY(-3px);
box-shadow: 0 7px 14px rgba(142, 68, 173, 0.4);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.file-import {
max-width: 800px;
margin: 0 auto;
text-align: center;
}
#file-upload {
display: none;
}
.file-info {
margin-top: 20px;
display: none;
}
.help-text {
color: #64748b;
font-size: 0.95rem;
margin-top: 15px;
}
.flex-container {
display: flex;
flex-direction: column;
gap: 25px;
max-width: 1200px;
margin: 0 auto;
}
@media (min-width: 1024px) {
.flex-container {
flex-direction: row;
}
.main-content {
width: 750px;
flex-shrink: 0;
}
.sidebar {
width: 350px;
flex-shrink: 0;
}
}
.quiz-controls {
display: none;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
background: #f8fafc;
padding: 20px;
border-radius: 16px;
}
.quiz-controls.show {
display: flex;
}
.question-count {
font-size: 1.25rem;
font-weight: 600;
}
.total-questions {
color: #64748b;
margin-left: 8px;
}
.score-display {
background: #fff;
padding: 10px 20px;
border-radius: 50px;
font-size: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.score-value {
font-weight: 700;
color: #3B82F6;
margin-left: 5px;
}
.question-section {
display: none;
}
.question-section.show {
display: block;
}
.question-type {
display: inline-block;
padding: 8px 16px;
background: rgba(59, 130, 246, 0.1);
color: #3B82F6;
font-size: 0.95rem;
font-weight: 600;
border-radius: 50px;
margin-bottom: 20px;
}
.question-text {
font-size: 1.4rem;
font-weight: 700;
margin-bottom: 25px;
color: #1f2937;
line-height: 1.5;
}
@media (min-width: 768px) {
.question-text {
font-size: 1.6rem;
}
}
.options-container {
margin-bottom: 30px;
display: flex;
flex-direction: column;
gap: 15px;
}
.option {
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 14px;
cursor: pointer;
transition: all 0.2s;
background: #f9fafb;
}
.option:hover {
background: rgba(59, 130, 246, 0.08);
border-color: #93c5fd;
}
.option.selected {
border-color: #3B82F6;
background: rgba(59, 130, 246, 0.06);
}
.option-label {
display: flex;
align-items: flex-start;
}
.option-letter {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
margin-right: 16px;
font-weight: 600;
font-size: 1.1rem;
}
.option.selected .option-letter {
background: #3B82F6;
color: #fff;
}
.option:not(.selected) .option-letter {
background: rgba(59, 130, 246, 0.1);
color: #3B82F6;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 25px;
}
.confirm-btn-container {
text-align: center;
margin-bottom: 25px;
}
.feedback {
padding: 20px;
border-radius: 12px;
margin-bottom: 25px;
display: none;
align-items: center;
font-size: 1.1rem;
}
.feedback.show {
display: flex;
}
.feedback-success {
background: #f0fdf4;
border: 1px solid #dcfce7;
color: #166534;
}
.feedback-error {
background: #fee2e2;
border: 1px solid #fecaca;
color: #b91c1c;
}
.results-section {
display: none;
text-align: center;
max-width: 700px;
margin: 0 auto;
}
.results-section.show {
display: block;
}
.results-title {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 10px;
}
.results-subtitle {
color: #64748b;
margin-bottom: 30px;
font-size: 1.1rem;
}
.score-circle {
display: flex;
align-items: center;
justify-content: center;
width: 220px;
height: 220px;
border-radius: 50%;
background: linear-gradient(to bottom right, #3B82F6, #8B5CF6);
margin: 0 auto 30px;
color: #fff;
text-align: center;
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
}
.final-score {
font-size: 3.5rem;
font-weight: 800;
}
.final-total {
font-size: 1.8rem;
font-weight: 700;
}
.results-buttons {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 30px;
flex-wrap: wrap;
}
.error-section {
display: none;
text-align: center;
max-width: 700px;
margin: 0 auto;
}
.error-section.show {
display: block;
}
.error-icon-container {
display: inline-flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border-radius: 50%;
background: #fee2e2;
margin-bottom: 20px;
}
.error-icon {
color: #dc2626;
font-size: 2.5rem;
}
.error-title {
font-size: 1.8rem;
font-weight: 700;
color: #dc2626;
margin-bottom: 10px;
}
.error-message {
color: #64748b;
margin-bottom: 25px;
font-size: 1.1rem;
}
.debug-info {
font-size: 0.95rem;
background: #f3f4f6;
padding: 20px;
border-radius: 12px;
margin-bottom: 25px;
max-height: 200px;
overflow-y: auto;
display: none;
}
.debug-info.show {
display: block;
}
.answer-sheet-section {
display: none;
}
.answer-sheet-section.show {
display: block;
}
.answer-sheet {
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
}
.answer-sheet-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 1.25rem;
font-weight: 700;
color: #3B82F6;
}
.answer-sheet-icon {
margin-right: 10px;
color: #3B82F6;
font-size: 1.4rem;
}
.answer-stats {
display: flex;
justify-content: space-between;
font-size: 0.95rem;
margin-bottom: 20px;
}
.stat-label {
color: #64748b;
}
.stat-value {
font-weight: 600;
}
.answer-sheet-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.answer-sheet-btn {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.answer-sheet-btn.unanswered {
background: #e5e7eb;
color: #4b5563;
}
.answer-sheet-btn.current {
background: #3B82F6;
color: #fff;
}
.answer-sheet-btn.correct {
background: #10B981;
color: #fff;
}
.answer-sheet-btn.incorrect {
background: #EF4444;
color: #fff;
}
.answer-sheet-btn.partial {
background: linear-gradient(135deg, #FBBF24 50%, #EF4444 50%);
color: #fff;
}
.legend {
display: flex;
align-items: center;
font-size: 0.9rem;
gap: 15px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 50%;
}
.legend-unanswered {
background-color: #e5e7eb;
}
.legend-current {
background-color: #3B82F6;
}
.legend-correct {
background-color: #10B981;
}
.legend-incorrect {
background-color: #EF4444;
}
.legend-partial {
background-color: #FBBF24;
}
.debug-toggle-btn {
margin-top: 20px;
background: transparent;
border: 1px solid #94a3b8;
color: #64748b;
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
font-size: 0.95rem;
transition: all 0.3s;
}
.debug-toggle-btn:hover {
background: rgba(59, 130, 246, 0.1);
border-color: #3B82F6;
color: #3B82F6;
}
.start-quiz-section {
text-align: center;
padding: 30px 0;
}
/* ========= 统一图片预览布局 ========= */
.image-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
margin: 20px 0;
position: relative;
border-radius: 12px;
overflow: hidden;
background: #f8f9ff;
padding: 15px;
border: 1px solid #e5e7eb;
}
.question-image, .option-image {
max-width: 100%;
max-height: 350px;
border-radius: 10px;
display: block;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.preview-btn {
position: absolute;
bottom: 15px;
right: 15px;
background: rgba(59, 130, 246, 0.8);
color: white;
border: none;
border-radius: 6px;
padding: 8px 15px;
font-size: 0.95rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.3s;
backdrop-filter: blur(4px);
z-index: 10;
}
.preview-btn:hover {
background: #3B82F6;
transform: scale(1.05);
}
.points-badge {
background: rgba(231, 111, 81, 0.15);
color: #e76f51;
padding: 6px 14px;
border-radius: 50px;
font-size: 0.95rem;
font-weight: 600;
display: inline-block;
margin-left: 15px;
}
.question-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.explanation-content {
margin-top: 20px;
padding: 20px;
background: #f8f9ff;
border-radius: 12px;
border-left: 4px solid #3B82F6;
line-height: 1.7;
}
.advanced-features {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 30px 0;
padding: 25px;
background: #f8f9ff;
border-radius: 16px;
border-left: 4px solid #3B82F6;
}
.feature-item {
flex: 1;
min-width: 250px;
}
.feature-title {
font-weight: 700;
margin-bottom: 12px;
color: #3B82F6;
display: flex;
align-items: center;
gap: 10px;
font-size: 1.1rem;
}
.feature-icon {
font-size: 1.3rem;
}
.file-info-content {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 20px;
border-radius: 12px;
background-color: #f0fdf4;
border: 1px solid #dcfce7;
}
.file-info-content span:first-child {
color: #10B981;
font-weight: bold;
font-size: 1.4rem;
}
.file-info-content span:last-child {
color: #166534;
font-weight: 600;
font-size: 1.1rem;
}
.import-status {
margin-top: 25px;
padding: 20px;
border-radius: 12px;
text-align: center;
display: none;
font-size: 1.1rem;
}
.import-status.show {
display: block;
}
.import-status.success {
background: #f0fdf4;
border: 1px solid #dcfce7;
color: #166534;
}
.import-status.error {
background: #fee2e2;
border: 1px solid #fecaca;
color: #b91c1c;
}
.import-status i {
margin-right: 12px;
}
.truefalse-option {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
padding: 20px;
border-radius: 14px;
background: #f8f9ff;
border: 2px solid #e5e7eb;
cursor: pointer;
transition: all 0.2s;
font-size: 1.1rem;
font-weight: 600;
}
.truefalse-option.selected {
border-color: #3B82F6;
background: rgba(59, 130, 246, 0.1);
}
.truefalse-container {
display: flex;
gap: 20px;
margin-top: 15px;
}
.question-type-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.type-group {
background: #f8f9ff;
border-radius: 14px;
padding: 20px;
border: 1px solid #e5e7eb;
}
.type-title {
font-weight: 700;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
color: #3B82F6;
font-size: 1.1rem;
}
.type-count {
margin-top: 15px;
font-size: 1rem;
color: #64748b;
}
.type-controls {
display: flex;
align-items: center;
gap: 15px;
margin-top: 15px;
}
.type-controls input {
width: 90px;
padding: 12px;
border-radius: 10px;
border: 1px solid #cbd5e1;
background: white;
}
.type-controls span {
font-size: 1rem;
color: #64748b;
}
.error-details {
background: #fff5f5;
border: 1px solid #ffd6d6;
border-radius: 12px;
padding: 20px;
margin-top: 25px;
}
.error-details-title {
font-weight: 700;
margin-bottom: 15px;
color: #dc2626;
display: flex;
align-items: center;
gap: 12px;
font-size: 1.1rem;
}
.error-details-content {
font-family: monospace;
white-space: pre-wrap;
word-break: break-all;
max-height: 250px;
overflow-y: auto;
padding: 15px;
background: #fff;
border-radius: 10px;
border: 1px solid #e5e7eb;
}
.file-format {
background: #f0fdf4;
border-radius: 12px;
padding: 25px;
margin: 20px 0;
border-left: 4px solid #10B981;
}
.file-format-title {
font-weight: 700;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 12px;
color: #10B981;
font-size: 1.2rem;
}
.file-format ul {
padding-left: 25px;
margin-top: 15px;
}
.file-format li {
margin-bottom: 12px;
line-height: 1.6;
}
.file-format strong {
color: #1e293b;
}
.encoding-selector {
margin-top: 20px;
padding: 12px;
border-radius: 10px;
border: 1px solid #cbd5e1;
width: 100%;
background: white;
font-size: 1rem;
}
.random-questions-group {
margin-top: 20px;
display: flex;
align-items: center;
gap: 15px;
}
.random-questions-group input {
width: 100px;
padding: 12px;
border-radius: 10px;
border: 1px solid #cbd5e1;
background: white;
}
.fillblank-container {
margin-top: 20px;
}
.blank-row {
display: flex;
align-items: center;
margin-bottom: 15px;
gap: 15px;
}
.blank-number {
width: 36px;
height: 36px;
border-radius: 50%;
background: #9b59b6;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.blank-input {
flex: 1;
padding: 15px;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 1.1rem;
transition: border-color 0.3s;
}
.blank-input:focus {
border-color: #3B82F6;
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.blank-hint {
color: #64748b;
font-size: 0.95rem;
margin-left: 10px;
}
.feedback-blank {
display: flex;
align-items: center;
gap: 10px;
margin-top: 8px;
padding: 10px 15px;
border-radius: 8px;
font-size: 1rem;
display: none;
}
.feedback-blank.correct {
background: #f0fdf4;
color: #166534;
}
.feedback-blank.incorrect {
background: #fee2e2;
color: #b91c1c;
}
.blank-scoring-mode {
display: inline-block;
padding: 6px 14px;
border-radius: 50px;
font-weight: 600;
font-size: 0.9rem;
margin-left: 15px;
}
.blank-scoring-per {
background: rgba(46, 204, 113, 0.15);
color: #2ecc71;
}
.blank-scoring-all {
background: rgba(231, 76, 60, 0.15);
color: #e74c3c;
}
.export-blank-answer {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.export-blank-input {
background: white;
border: 1px solid #e5e7eb;
padding: 8px 15px;
border-radius: 8px;
font-weight: 500;
}
.export-blank-correct {
background: #e6fffa;
color: #0d9488;
border-color: #0d9488;
}
.export-blank-incorrect {
background: #ffebee;
color: #e53935;
border-color: #e53935;
}
.export-blank-label {
font-weight: 600;
min-width: 80px;
}
.question-text .blank-placeholder {
position: relative;
display: inline-block;
min-width: 120px;
border-bottom: 2px solid #3B82F6;
margin: 0 5px;
padding: 0 0 3px;
color: #3B82F6;
font-weight: 600;
vertical-align: middle;
}
.question-text .blank-placeholder::after {
content: attr(data-index);
position: absolute;
top: -18px;
left: 2px;
font-size: 0.8rem;
color: #64748b;
}
.inline-blank {
display: inline-block;
min-width: 120px;
padding: 0 10px;
border: none;
border-bottom: 2px solid #3B82F6;
font-size: 1.1rem;
text-align: center;
background: transparent;
outline: none;
margin: 0 5px;
vertical-align: middle;
}
.inline-blank:focus {
border-bottom: 2px solid #8B5CF6;
background: rgba(139, 92, 246, 0.05);
}
.shuffle-container {
display: flex;
align-items: center;
gap: 12px;
margin-top: 20px;
padding: 15px;
background: #f8f9ff;
border-radius: 12px;
border-left: 4px solid #9b59b6;
}
.shuffle-label {
font-weight: 600;
color: #9b59b6;
}
.exam-mode-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
padding: 20px;
background: #f8f9ff;
border-radius: 16px;
border-left: 4px solid #3498db;
}
.exam-mode-toggle {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.exam-mode-title {
font-weight: 700;
color: #3498db;
}
.exam-settings {
display: flex;
align-items: center;
gap: 15px;
flex: 1;
}
.exam-settings label {
font-weight: 600;
color: #2c3e50;
}
.exam-settings input {
width: 100px;
padding: 12px;
border-radius: 10px;
border: 1px solid #cbd5e1;
background: white;
}
.timer-container {
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 50px;
padding: 12px 25px;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.15);
z-index: 1000;
display: flex;
align-items: center;
gap: 10px;
border: 2px solid #3498db;
display: none;
}
.timer-label {
font-weight: 700;
color: #3498db;
}
.timer-value {
font-size: 1.5rem;
font-weight: 800;
color: #e74c3c;
}
.timer-warning {
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { color: #e74c3c; }
50% { color: #ff6b6b; }
100% { color: #e74c3c; }
}
.wrong-questions-btn {
background: linear-gradient(to right, #e74c3c, #c0392b);
color: white;
border: none;
padding: 14px 28px;
border-radius: 12px;
cursor: pointer;
font-size: 1.05rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 10px;
transition: all 0.3s;
box-shadow: 0 4px 6px rgba(231, 76, 60, 0.3);
}
.wrong-questions-btn:hover {
transform: translateY(-3px);
box-shadow: 0 7px 14px rgba(231, 76, 60, 0.4);
}
/* 图片预览模态框 */
.image-preview-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 1000;
justify-content: center;
align-items: center;
flex-direction: column;
}
.preview-container {
position: relative;
max-width: 90%;
max-height: 80vh;
text-align: center;
}
#preview-image {
max-width: 100%;
max-height: 75vh;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.preview-nav {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 30px;
}
.preview-nav-btn {
background: #3B82F6;
color: white;
border: none;
border-radius: 50%;
width: 60px;
height: 60px;
font-size: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
}
.preview-nav-btn:hover {
background: #2563EB;
transform: scale(1.1);
}
#close-preview {
position: absolute;
top: 30px;
right: 30px;
background: #fff;
color: #333;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.image-counter {
color: white;
font-size: 1.2rem;
margin-top: 20px;
background: rgba(0, 0, 0, 0.5);
padding: 8px 20px;
border-radius: 30px;
font-weight: 500;
}
.download-sample {
text-align: center;
margin-top: 30px;
}
.download-btn {
display: inline-flex;
align-items: center;
gap: 10px;
color: #3B82F6;
font-weight: 600;
text-decoration: none;
padding: 12px 25px;
border-radius: 12px;
background: rgba(59, 130, 246, 0.1);
transition: all 0.3s;
}
.download-btn:hover {
background: rgba(59, 130, 246, 0.2);
transform: translateY(-3px);
}
.sample-image {
max-width: 100%;
border-radius: 12px;
margin-top: 25px;
border: 1px solid #e2e8f0;
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
}
/* ========= 分类答题卡样式 ========= */
.type-section {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e5e7eb;
}
.type-section:last-child {
border-bottom: none;
}
.type-section-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 1.1rem;
font-weight: 600;
color: #3B82F6;
}
.type-section-icon {
font-size: 1.1rem;
}
.type-section-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.type-section .answer-sheet-btn {
width: 36px;
height: 36px;
font-size: 0.9rem;
}
.type-section-stats {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #64748b;
margin-top: 8px;
}
.type-section-stats .stat-value {
font-weight: 600;
color: #3B82F6;
}
.type-section-stats .stat-correct {
color: #10B981;
}
.type-section-stats .stat-incorrect {
color: #EF4444;
}
/* 进度条容器 - 仅在答题时显示 */
.progress-container {
display: none;
margin-top: 30px;
padding: 25px;
background: #f8f9ff;
border-radius: 16px;
border-left: 4px solid #3B82F6;
}
.progress-container.show {
display: block;
}
.progress-title {
font-weight: 700;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
color: #3B82F6;
font-size: 1.2rem;
}
.progress-bar {
height: 16px;
background: #e5e7eb;
border-radius: 8px;
overflow: hidden;
margin-bottom: 15px;
}
.progress-fill {
height: 100%;
background: linear-gradient(to right, #3B82F6, #8B5CF6);
border-radius: 8px;
transition: width 0.5s ease;
}
.progress-labels {
display: flex;
justify-content: space-between;
font-size: 0.95rem;
color: #64748b;
}
.progress-label {
display: flex;
flex-direction: column;
align-items: center;
}
.progress-value {
font-weight: 700;
margin-top: 8px;
color: #3B82F6;
font-size: 1.1rem;
}
/* ========= 解答题样式 ========= */
.essay-container {
margin-top: 20px;
}
.essay-answer-box {
width: 100%;
min-height: 150px;
padding: 15px;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 1.1rem;
transition: border-color 0.3s;
resize: vertical;
}
.essay-answer-box:focus {
border-color: #3B82F6;
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.scoring-mode {
display: inline-block;
padding: 6px 14px;
border-radius: 50px;
font-weight: 600;
font-size: 0.9rem;
margin-left: 15px;
}
.scoring-per {
background: rgba(46, 204, 113, 0.15);
color: #2ecc71;
}
.scoring-all {
background: rgba(231, 76, 60, 0.15);
color: #e74c3c;
}
.scoring-partial {
background: rgba(241, 196, 15, 0.15);
color: #f39c12;
}
.keyword-preview-tip {
margin-top: 15px;
padding: 12px 15px;
background-color: #f0f7ff;
border-radius: 8px;
border-left: 3px solid #3B82F6;
font-size: 0.95rem;
color: #4b5563;
}
.keyword-feedback {
margin-top: 15px;
padding: 15px;
background: #f8f9ff;
border-radius: 12px;
border-left: 4px solid #f39c12;
}
.keyword-item {
display: flex;
align-items: center;
padding: 10px;
margin-bottom: 8px;
border-radius: 8px;
}
.keyword-item.correct {
background: #e6fffa;
border: 1px solid #0d9488;
}
.keyword-item.incorrect {
background: #ffebee;
border: 1px solid #e53935;
}
.keyword-text {
flex: 1;
}
.keyword-points {
font-weight: 600;
min-width: 60px;
text-align: right;
}
.keyword-status {
margin-left: 10px;
font-size: 1.2rem;
}
.essay-score-display {
display: flex;
align-items: center;
justify-content: center;
margin: 15px 0;
font-size: 1.2rem;
}
.essay-score-value {
font-weight: 800;
font-size: 1.8rem;
color: #3B82F6;
margin: 0 5px;
}
.essay-score-total {
font-weight: 600;
color: #64748b;
}
.keyword-match-detail {
font-size: 0.9rem;
color: #64748b;
margin-top: 5px;
padding: 8px;
background: rgba(0,0,0,0.03);
border-radius: 6px;
}
.keyword-match-detail strong {
color: #3B82F6;
}
/* 解答题答题卡按钮 */
.answer-sheet-btn.essay {
background: #9CA3AF;
}
.answer-sheet-btn.essay.correct {
background: #10B981;
}
.answer-sheet-btn.essay.incorrect {
background: #EF4444;
}
.answer-sheet-btn.essay.partial {
background: linear-gradient(135deg, #FBBF24 50%, #9CA3AF 50%);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>增强版答题系统</h1>
<p class="subtitle">解答题关键词评分优化 - 解决关键词匹配但得零分问题</p>
</header>
<div id="file-import-section" class="card file-import">
<label for="file-upload" class="btn btn-primary">
<i class="fas fa-file-import"></i> 选择题库文件
</label>
<input id="file-upload" type="file" accept=".csv, .xlsx, .xls">
<div class="advanced-features">
<div class="feature-item">
<div class="feature-title">
<i class="fas fa-random feature-icon"></i>
<span>随机抽题设置</span>
</div>
<p class="help-text">请为每种题型设置抽取数量(0表示抽取全部题目)</p>
</div>
<div class="feature-item">
<div class="feature-title">
<i class="fas fa-language feature-icon"></i>
<span>文件编码</span>
</div>
<select id="encoding-selector" class="encoding-selector">
<option value="utf-8">UTF-8</option>
<option value="gbk">GBK</option>
</select>
</div>
</div>
<!-- 打乱题目顺序选项 -->
<div class="shuffle-container">
<input type="checkbox" id="shuffle-questions" class="shuffle-checkbox">
<label for="shuffle-questions" class="shuffle-label">
<i class="fas fa-random"></i> 打乱题目顺序
</label>
</div>
<!-- 模拟考试模式设置 -->
<div class="exam-mode-container">
<div class="exam-mode-toggle">
<input type="checkbox" id="exam-mode-toggle" class="exam-toggle">
<label for="exam-mode-toggle" class="exam-mode-title">
<i class="fas fa-clock"></i> 模拟考试模式
</label>
</div>
<div class="exam-settings">
<label for="exam-duration">考试时长(分钟):</label>
<input type="number" id="exam-duration" min="1" max="180" value="30" disabled>
</div>
</div>
<!-- 题型分组设置 -->
<div class="question-type-group">
<div class="type-group">
<div class="type-title">
<i class="fas fa-dot-circle"></i>
<span>单选题</span>
</div>
<div class="type-count">题库数量: <span id="single-count">0</span></div>
<div class="type-controls">
<label>抽取数量:</label>
<input type="number" id="single-count-input" min="0" value="0">
</div>
</div>
<div class="type-group">
<div class="type-title">
<i class="fas fa-check-double"></i>
<span>多选题</span>
</div>
<div class="type-count">题库数量: <span id="multiple-count">0</span></div>
<div class="type-controls">
<label>抽取数量:</label>
<input type="number" id="multiple-count-input" min="0" value="0">
</div>
</div>
<div class="type-group">
<div class="type-title">
<i class="fas fa-check-circle"></i>
<span>判断题</span>
</div>
<div class="type-count">题库数量: <span id="truefalse-count">0</span></div>
<div class="type-controls">
<label>抽取数量:</label>
<input type="number" id="truefalse-count-input" min="0" value="0">
</div>
</div>
<div class="type-group">
<div class="type-title">
<i class="fas fa-pen"></i>
<span>填空题</span>
</div>
<div class="type-count">题库数量: <span id="fillblank-count">0</span></div>
<div class="type-controls">
<label>抽取数量:</label>
<input type="number" id="fillblank-count-input" min="0" value="0">
</div>
</div>
<div class="type-group">
<div class="type-title">
<i class="fas fa-comment"></i>
<span>解答题</span>
</div>
<div class="type-count">题库数量: <span id="essay-count">0</span></div>
<div class="type-controls">
<label>抽取数量:</label>
<input type="number" id="essay-count-input" min="0" value="0">
</div>
</div>
</div>
<div id="file-info" class="file-info">
<div class="file-info-content">
<span>✓</span>
<span id="selected-filename"></span>
</div>
</div>
<!-- 导入状态显示 -->
<div id="import-status" class="import-status">
<i class="fas fa-spinner fa-spin"></i>
<span id="import-message">正在处理文件...</span>
</div>
<button id="debug-toggle" class="debug-toggle-btn">显示调试信息</button>
<div id="debug-info" class="debug-info">
<h4 class="debug-title">调试信息:</h4>
<p id="debug-details"></p>
</div>
<div id="start-quiz-section" class="start-quiz-section" style="display: none;">
<button id="start-quiz-btn" class="btn btn-secondary">
<i class="fas fa-play-circle"></i> 开始答题
</button>
</div>
<!-- 文件格式说明放在底部 -->
<div class="file-format">
<div class="file-format-title">
<i class="fas fa-info-circle"></i>
<span>题库文件格式说明</span>
</div>
<p>支持CSV和Excel格式,第一行为标题行,支持以下字段(顺序不限):</p>
<ul>
<li><strong>问题</strong>: 题目内容(填空题使用 {1}、{2} 等作为占位符)</li>
<li><strong>问题图片</strong>: 题目图片URL(可选)</li>
<li><strong>选项A, 选项B, ...</strong>: 选项内容</li>
<li><strong>选项A图片, 选项B图片, ...</strong>: 选项图片URL(可选)</li>
<li><strong>答案</strong>: 正确答案(如A, AB, C等,填空题用 | 分隔不同空)</li>
<li><strong>题目类型</strong>: single(单选题)、multiple(多选题)、truefalse(判断题)、fillblank(填空题)、essay(解答题)</li>
<li><strong>题目分数</strong>: 该题分值</li>
<li><strong>答案解析</strong>: 题目解析(可选)</li>
<li><strong>评分规则</strong>: perBlank(按空给分)、allOrNothing(全对给分)、partialCredit(部分给分)</li>
<li><strong>关键词1, 关键词1分值, 关键词2, 关键词2分值, ...</strong>: 解答题评分关键词(最多5组)</li>
<li><strong>填空项1, 填空项2, ...</strong>: 填空题的提示文本(可选)</li>
</ul>
</div>
<div class="download-sample">
<a href="#" class="download-btn" id="download-sample">
<i class="fas fa-download"></i> 下载示例题库文件
</a>
</div>
</div>
<!-- 倒计时容器 -->
<div id="timer-container" class="timer-container">
<span class="timer-label">剩余时间:</span>
<span id="timer-value" class="timer-value">30:00</span>
</div>
<div class="flex-container">
<div class="main-content">
<!-- 进度条容器 - 初始隐藏 -->
<div id="progress-container" class="progress-container">
<div class="progress-title">
<i class="fas fa-tasks"></i>
<span>答题进度</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
</div>
<div class="progress-labels">
<div class="progress-label">
<span>单选题</span>
<span class="progress-value" id="single-progress">0/0</span>
</div>
<div class="progress-label">
<span>多选题</span>
<span class="progress-value" id="multiple-progress">0/0</span>
</div>
<div class="progress-label">
<span>判断题</span>
<span class="progress-value" id="truefalse-progress">0/0</span>
</div>
<div class="progress-label">
<span>填空题</span>
<span class="progress-value" id="fillblank-progress">0/0</span>
</div>
<div class="progress-label">
<span>解答题</span>
<span class="progress-value" id="essay-progress">0/0</span>
</div>
</div>
</div>
<div id="quiz-controls" class="quiz-controls">
<div class="question-count">
<span id="current-question">问题 1</span>
<span id="total-questions" class="total-questions">/ 0</span>
</div>
<div class="score-display">
<span>得分:</span>
<span id="score" class="score-value">0</span>
</div>
</div>
<div id="quiz-section" class="card question-section">
<div class="question-header">
<div class="question-type" id="question-type-tag">不定向选择题</div>
<span id="question-points" class="points-badge">1分</span>
<span id="scoring-mode" class="blank-scoring-mode" style="display: none;"></span>
</div>
<div id="question-content">
<h2 id="question-text" class="question-text"></h2>
<div id="question-image-container" class="image-container">
<!-- 题目图片将在这里动态插入 -->
</div>
</div>
<div id="options-container" class="options-container"></div>
<div id="fillblank-container" class="fillblank-container" style="display: none;"></div>
<div id="essay-container" class="essay-container" style="display: none;">
<textarea id="essay-answer-box" class="essay-answer-box" placeholder="请输入您的解答..."></textarea>
<div class="keyword-preview-tip">
<i class="fas fa-info-circle"></i>
<span>提交答案后可查看评分关键词</span>
</div>
</div>
<div class="confirm-btn-container">
<button id="confirm-btn" class="btn btn-primary">提交答案</button>
</div>
<div id="feedback" class="feedback"></div>
<div id="keyword-feedback" class="keyword-feedback" style="display: none;"></div>
<div id="explanation-content" class="explanation-content" style="display: none;"></div>
<div class="button-group">
<button id="prev-btn" class="btn btn-gray" disabled>
<i class="fas fa-arrow-left"></i> 上一题
</button>
<button id="next-btn" class="btn btn-primary" disabled>
下一题 <i class="fas fa-arrow-right"></i>
</button>
</div>
</div>
<div id="results-section" class="card results-section">
<h2 class="results-title">测验完成!</h2>
<p class="results-subtitle">恭喜你完成了所有问题</p>
<div class="score-circle">
<span id="final-score" class="final-score">0</span>
<span>/</span>
<span id="final-total" class="final-total">0</span>
</div>
<div class="results-buttons">
<button id="restart-btn" class="btn btn-secondary">
<i class="fas fa-redo"></i> 重新开始
</button>
<button id="export-results" class="export-btn">
<i class="fas fa-file-export"></i> 导出测试结果
</button>
<button id="export-wrong-questions" class="wrong-questions-btn">
<i class="fas fa-file-excel"></i> 导出错题集(Excel)
</button>
<button id="new-file-btn" class="btn btn-gray">
<i class="fas fa-file"></i> 选择新文件
</button>
</div>
</div>
<div id="error-section" class="card error-section">
<div class="error-icon-container"><span class="error-icon">!</span></div>
<h3 class="error-title">文件处理错误</h3>
<p id="error-message" class="error-message"></p>
<div class="error-details">
<div class="error-details-title">
<i class="fas fa-bug"></i>
<span>错误详情</span>
</div>
<div id="error-details-content" class="error-details-content"></div>
</div>
<div id="error-debug-info" class="debug-info">
<h4 class="debug-title">调试信息:</h4>
<p id="error-debug-details"></p>
</div>
<button id="retry-btn" class="btn btn-primary">重新选择文件</button>
</div>
</div>
<div id="answer-sheet-section" class="sidebar answer-sheet-section">
<div class="card answer-sheet">
<h3 class="answer-sheet-title"><i class="fas fa-clipboard-list answer-sheet-icon"></i> 答题卡</h3>
<div class="answer-stats">
<div><span class="stat-label">已答:</span><span id="answered-count" class="stat-value">0</span></div>
<div><span class="stat-label">正确:</span><span id="correct-count" class="stat-value stat-correct">0</span></div>
<div><span class="stat-label">总题数:</span><span id="total-count" class="stat-value">0</span></div>
</div>
<!-- 单选题答题卡 -->
<div id="single-section" class="type-section">
<div class="type-section-title">
<i class="fas fa-dot-circle type-section-icon"></i>
<span>单选题</span>
</div>
<div id="single-grid" class="type-section-grid"></div>
<div class="type-section-stats">
<span>正确: <span id="single-correct" class="stat-value">0</span></span>
<span>错误: <span id="single-incorrect" class="stat-value stat-incorrect">0</span></span>
<span>总计: <span id="single-total" class="stat-value">0</span></span>
</div>
</div>
<!-- 多选题答题卡 -->
<div id="multiple-section" class="type-section">
<div class="type-section-title">
<i class="fas fa-check-double type-section-icon"></i>
<span>多选题</span>
</div>
<div id="multiple-grid" class="type-section-grid"></div>
<div class="type-section-stats">
<span>正确: <span id="multiple-correct" class="stat-value">0</span></span>
<span>错误: <span id="multiple-incorrect" class="stat-value stat-incorrect">0</span></span>
<span>总计: <span id="multiple-total" class="stat-value">0</span></span>
</div>
</div>
<!-- 判断题答题卡 -->
<div id="truefalse-section" class="type-section">
<div class="type-section-title">
<i class="fas fa-check-circle type-section-icon"></i>
<span>判断题</span>
</div>
<div id="truefalse-grid" class="type-section-grid"></div>
<div class="type-section-stats">
<span>正确: <span id="truefalse-correct" class="stat-value">0</span></span>
<span>错误: <span id="truefalse-incorrect" class="stat-value stat-incorrect">0</span></span>
<span>总计: <span id="truefalse-total" class="stat-value">0</span></span>
</div>
</div>
<!-- 填空题答题卡 -->
<div id="fillblank-section" class="type-section">
<div class="type-section-title">
<i class="fas fa-pen type-section-icon"></i>
<span>填空题</span>
</div>
<div id="fillblank-grid" class="type-section-grid"></div>
<div class="type-section-stats">
<span>正确: <span id="fillblank-correct" class="stat-value">0</span></span>
<span>错误: <span id="fillblank-incorrect" class="stat-value stat-incorrect">0</span></span>
<span>总计: <span id="fillblank-total" class="stat-value">0</span></span>
</div>
</div>
<!-- 解答题答题卡 -->
<div id="essay-section" class="type-section">
<div class="type-section-title">
<i class="fas fa-comment type-section-icon"></i>
<span>解答题</span>
</div>
<div id="essay-grid" class="type-section-grid"></div>
<div class="type-section-stats">
<span>正确: <span id="essay-correct" class="stat-value">0</span></span>
<span>错误: <span id="essay-incorrect" class="stat-value stat-incorrect">0</span></span>
<span>总计: <span id="essay-total" class="stat-value">0</span></span>
</div>
</div>
<div class="legend">
<div class="legend-item">
<span class="legend-color legend-unanswered"></span>
<span class="text-unanswered">未答</span>
</div>
<div class="legend-item">
<span class="legend-color legend-current"></span>
<span class="text-current">当前</span>
</div>
<div class="legend-item">
<span class="legend-color legend-correct"></span>
<span class="text-correct">正确</span>
</div>
<div class="legend-item">
<span class="legend-color legend-incorrect"></span>
<span class="text-incorrect">错误</span>
</div>
<div class="legend-item">
<span class="legend-color legend-partial"></span>
<span class="text-partial">部分</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 图片预览模态框 -->
<div id="image-preview-modal" class="image-preview-modal">
<button id="close-preview"><i class="fas fa-times"></i></button>
<div class="preview-container">
<img id="preview-image" src="" alt="预览图片">
<div class="image-counter" id="image-counter">1/1</div>
</div>
<div class="preview-nav">
<button id="prev-image" class="preview-nav-btn"><i class="fas fa-chevron-left"></i></button>
<button id="next-image" class="preview-nav-btn"><i class="fas fa-chevron-right"></i></button>
</div>
</div>
<script>
/* ========= 全局变量 ========= */
let questions = [], current = 0, score = 0, answered = 0, correct = 0;
let status = [];
const valid = ['A','B','C','D','E','F'];
let parsingErrors = [];
let totalPoints = 0;
let questionGroups = { single: [], multiple: [], truefalse: [], fillblank: [], essay: [] };
let selectedQuestions = [];
let currentImages = []; // 当前题目所有图片
let currentImageIndex = 0; // 当前预览图片索引
let examMode = false;
let examDuration = 30; // 默认30分钟
let examTimer = null;
let remainingTime = 0; // 剩余秒数
let globalTitles = []; // 存储CSV标题行
/* ========= DOM 快捷 ========= */
const el = id => document.getElementById(id);
const fileUpload = el('file-upload');
const fileImport = el('file-import-section');
const quiz = el('quiz-section');
const results = el('results-section');
const errorSec = el('error-section');
const controls = el('quiz-controls');
const answerSheet = el('answer-sheet-section');
const answeredEl = el('answered-count');
const correctEl = el('correct-count');
const totalEl = el('total-count');
const filenameEl = el('selected-filename');
const qText = el('question-text');
const qImageContainer = el('question-image-container');
const qNum = el('current-question');
const qTotal = el('total-questions');
const scoreEl = el('score');
const confirmBtn = el('confirm-btn');
const prevBtn = el('prev-btn');
const nextBtn = el('next-btn');
const finalScore = el('final-score');
const finalTotal = el('final-total');
const optionsContainer = el('options-container');
const fillblankContainer = el('fillblank-container');
const essayContainer = el('essay-container');
const essayAnswerBox = el('essay-answer-box');
const keywordFeedback = el('keyword-feedback');
const startQuizBtn = el('start-quiz-btn');
const startQuizSection = el('start-quiz-section');
const debugToggleBtn = el('debug-toggle');
const debugInfo = el('debug-info');
const debugDetails = el('debug-details');
const encodingSelector = el('encoding-selector');
const explanationContent = el('explanation-content');
const questionTypeTag = el('question-type-tag');
const questionPoints = el('question-points');
const scoringModeEl = el('scoring-mode');
const importStatus = el('import-status');
const importMessage = el('import-message');
const singleCountEl = el('single-count');
const multipleCountEl = el('multiple-count');
const truefalseCountEl = el('truefalse-count');
const fillblankCountEl = el('fillblank-count');
const essayCountEl = el('essay-count');
const singleCountInput = el('single-count-input');
const multipleCountInput = el('multiple-count-input');
const truefalseCountInput = el('truefalse-count-input');
const fillblankCountInput = el('fillblank-count-input');
const essayCountInput = el('essay-count-input');
const progressFill = el('progress-fill');
const singleProgressEl = el('single-progress');
const multipleProgressEl = el('multiple-progress');
const truefalseProgressEl = el('truefalse-progress');
const fillblankProgressEl = el('fillblank-progress');
const essayProgressEl = el('essay-progress');
const errorDetailsContent = el('error-details-content');
const exportResultsBtn = el('export-results');
const exportWrongQuestionsBtn = el('export-wrong-questions');
const shuffleCheckbox = el('shuffle-questions');
const examModeToggle = el('exam-mode-toggle');
const examDurationInput = el('exam-duration');
const timerContainer = el('timer-container');
const timerValue = el('timer-value');
const progressContainer = el('progress-container');
// 图片预览相关元素
const previewModal = el('image-preview-modal');
const previewImage = el('preview-image');
const imageCounter = el('image-counter');
const prevImageBtn = el('prev-image');
const nextImageBtn = el('next-image');
const closePreviewBtn = el('close-preview');
// 分类答题卡元素
const singleGrid = el('single-grid');
const multipleGrid = el('multiple-grid');
const truefalseGrid = el('truefalse-grid');
const fillblankGrid = el('fillblank-grid');
const essayGrid = el('essay-grid');
const singleCorrectEl = el('single-correct');
const singleIncorrectEl = el('single-incorrect');
const singleTotalEl = el('single-total');
const multipleCorrectEl = el('multiple-correct');
const multipleIncorrectEl = el('multiple-incorrect');
const multipleTotalEl = el('multiple-total');
const truefalseCorrectEl = el('truefalse-correct');
const truefalseIncorrectEl = el('truefalse-incorrect');
const truefalseTotalEl = el('truefalse-total');
const fillblankCorrectEl = el('fillblank-correct');
const fillblankIncorrectEl = el('fillblank-incorrect');
const fillblankTotalEl = el('fillblank-total');
const essayCorrectEl = el('essay-correct');
const essayIncorrectEl = el('essay-incorrect');
const essayTotalEl = el('essay-total');
/* ========= 事件绑定 ========= */
fileUpload.addEventListener('change', handleFile);
confirmBtn.addEventListener('click', confirmAnswer);
prevBtn.addEventListener('click', ()=> go(-1));
nextBtn.addEventListener('click', ()=> go(1));
el('restart-btn').addEventListener('click', restart);
el('new-file-btn').addEventListener('click', selectNewFile);
el('retry-btn').addEventListener('click', retryFile);
debugToggleBtn.addEventListener('click', toggleDebugInfo);
el('download-sample').addEventListener('click', downloadSample);
exportResultsBtn.addEventListener('click', exportResults);
exportWrongQuestionsBtn.addEventListener('click', exportWrongQuestionsExcel);
examModeToggle.addEventListener('change', toggleExamMode);
shuffleCheckbox.addEventListener('change', () => {
// 当打乱顺序选项变化时不需要特殊处理
});
// 图片预览事件
prevImageBtn.addEventListener('click', showPrevImage);
nextImageBtn.addEventListener('click', showNextImage);
closePreviewBtn.addEventListener('click', closePreview);
// 使用事件委托确保按钮点击事件有效
document.addEventListener('click', function(e) {
if (e.target && e.target.id === 'start-quiz-btn') {
startQuiz();
}
});
/* ========= 显示/隐藏调试信息 ========= */
function toggleDebugInfo() {
debugInfo.classList.toggle('show');
}
/* ========= 下载示例题库 ========= */
function downloadSample() {
const csvContent = `问题,问题图片,选项A,选项A图片,选项B,选项B图片,选项C,选项C图片,选项D,选项D图片,答案,题目类型,题目分数,答案解析,填空项1,填空项1分值,关键词1,关键词1分值,关键词2,关键词2分值,评分规则
"过完鸡年需要多久","https://img0.baidu.com/it/u=342342545,1451632770&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","半年","","一年","","两年","","两年半","https://img0.baidu.com/it/u=342342545,1451632770&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500","D","single",1,"","","","","","","",""
"下列哪些是哺乳动物?","","鲸鱼","","海豚","","鲨鱼","","企鹅","","AB","multiple",2,"鲨鱼是鱼类,企鹅是鸟类","","","","","","",""
"地球是太阳系中最大的行星","","正确","","错误","",,,,,"B","truefalse",1,"木星是太阳系中最大的行星","","","","","","",""
"中国首都是{1},最大城市是{2}","",,,,,,,,,,"北京|上海","fillblank",2,"","首都","","","","","",""
"请写出两句蔡徐坤名言","",,,,,,,,,,"","essay",3,"","","哎呦,你干嘛",1,"一个真正的man,一个真正的男人他清楚自己要做什么",1,"partialCredit"`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', '示例题库.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
/* ========= 图片预览功能 ========= */
function showPreview(imgSrc, images) {
currentImages = images;
currentImageIndex = images.indexOf(imgSrc);
if (currentImageIndex === -1) {
currentImageIndex = 0;
}
updatePreview();
previewModal.style.display = 'flex';
}
function updatePreview() {
previewImage.src = currentImages[currentImageIndex];
imageCounter.textContent = `${currentImageIndex + 1}/${currentImages.length}`;
}
function showPrevImage() {
if (currentImages.length > 1) {
currentImageIndex = (currentImageIndex - 1 + currentImages.length) % currentImages.length;
updatePreview();
}
}
function showNextImage() {
if (currentImages.length > 1) {
currentImageIndex = (currentImageIndex + 1) % currentImages.length;
updatePreview();
}
}
function closePreview() {
previewModal.style.display = 'none';
}
// 点击模态框背景关闭预览
previewModal.addEventListener('click', function(e) {
if (e.target === previewModal) {
closePreview();
}
});
/* ========= 文件读取 ========= */
function handleFile(e){
const file = e.target.files[0];
if(!file) return;
// 更新导入状态
importStatus.classList.add('show');
importMessage.innerHTML = `<i class="fas fa-spinner fa-spin"></i> 正在处理文件:${file.name}`;
filenameEl.textContent = file.name;
el('file-info').classList.add('show');
// 重置状态
startQuizSection.style.display = 'none';
const reader = new FileReader();
const encoding = encodingSelector.value;
const fileExt = file.name.split('.').pop().toLowerCase();
// 处理Excel文件
if (fileExt === 'xlsx' || fileExt === 'xls') {
reader.onload = function(e) {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// 将工作表转换为JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length < 2) {
showError('Excel文件中没有足够的数据');
importStatus.classList.remove('show');
return;
}
// 提取标题行(第一行)
const titles = jsonData[0];
// 提取数据行(从第二行开始)
const rows = jsonData.slice(1).filter(row => row.length > 0);
// 解析数据
const res = parseExcelRows(rows, titles);
questions = res.questions;
parsingErrors = res.errors;
globalTitles = titles;
// 更新调试信息
debugDetails.textContent = parsingErrors.length
? parsingErrors.join('\n')
: '未发现解析错误';
if(questions.length === 0){
showError('未找到有效题目,请检查文件格式');
importStatus.classList.remove('show');
}else{
// 分组题目
groupQuestions();
// 显示开始答题按钮
startQuizSection.style.display = 'block';
// 隐藏调试信息(默认)
debugInfo.classList.remove('show');
debugToggleBtn.style.display = 'inline-block';
// 更新导入状态
importStatus.className = 'import-status success show';
importMessage.innerHTML = `<i class="fas fa-check-circle"></i> 成功导入${questions.length}道题目`;
}
} catch (err) {
showError('处理Excel文件时出错: ' + err.message);
importStatus.classList.remove('show');
}
};
reader.readAsArrayBuffer(file);
}
// 处理CSV文件
else {
reader.onload = ev => {
try{
let text = '';
if (encoding === 'auto' || encoding === 'gbk') {
// 尝试自动检测编码或处理GBK
try {
// 使用TextDecoder处理GBK编码
const decoder = new TextDecoder('gbk');
text = decoder.decode(ev.target.result);
} catch (err) {
// 如果失败,尝试UTF-8
text = new TextDecoder('utf-8').decode(ev.target.result);
}
} else {
// UTF-8处理
text = cleanText(ev.target.result);
}
const res = parseCSV(text);
questions = res.questions;
parsingErrors = res.errors;
globalTitles = res.titles; // 保存标题行
// 更新调试信息
debugDetails.textContent = parsingErrors.length
? parsingErrors.join('\n')
: '未发现解析错误';
if(questions.length === 0){
showError('未找到有效题目,请检查文件格式');
importStatus.classList.remove('show');
}else{
// 分组题目
groupQuestions();
// 显示开始答题按钮
startQuizSection.style.display = 'block';
// 隐藏调试信息(默认)
debugInfo.classList.remove('show');
debugToggleBtn.style.display = 'inline-block';
// 更新导入状态
importStatus.className = 'import-status success show';
importMessage.innerHTML = `<i class="fas fa-check-circle"></i> 成功导入${questions.length}道题目`;
}
// 无论是否有错误,都让调试按钮可用
debugToggleBtn.style.display = 'inline-block';
}catch(err){
showError(err.message);
importStatus.classList.remove('show');
}
};
// 使用readAsArrayBuffer以确保正确处理所有编码
reader.readAsArrayBuffer(file);
}
}
/* ---------- 文本清洗 ---------- */
function cleanText(buf){
let str = typeof buf === 'string' ? buf : new TextDecoder('utf-8').decode(buf);
if(str.charCodeAt(0) === 0xFEFF) str = str.slice(1);
return str;
}
/* ---------- 分组题目 ---------- */
function groupQuestions() {
questionGroups = { single: [], multiple: [], truefalse: [], fillblank: [], essay: [] };
questions.forEach(q => {
if (q.type === 'single') {
questionGroups.single.push(q);
} else if (q.type === 'multiple') {
questionGroups.multiple.push(q);
} else if (q.type === 'truefalse') {
questionGroups.truefalse.push(q);
} else if (q.type === 'fillblank') {
questionGroups.fillblank.push(q);
} else if (q.type === 'essay') {
questionGroups.essay.push(q);
}
});
// 更新UI显示题目数量
singleCountEl.textContent = questionGroups.single.length;
multipleCountEl.textContent = questionGroups.multiple.length;
truefalseCountEl.textContent = questionGroups.truefalse.length;
fillblankCountEl.textContent = questionGroups.fillblank.length;
essayCountEl.textContent = questionGroups.essay.length;
// 设置默认抽取数量为全部题目
singleCountInput.value = questionGroups.single.length;
multipleCountInput.value = questionGroups.multiple.length;
truefalseCountInput.value = questionGroups.truefalse.length;
fillblankCountInput.value = questionGroups.fillblank.length;
essayCountInput.value = questionGroups.essay.length;
}
/* ========= 新增函数:处理填空题占位符 ========= */
function processFillblankQuestion(q) {
// 复制问题对象以避免修改原始数据
const processed = {...q};
// 处理问题文本中的占位符 {1}, {2} 等
const placeholderRegex = /\{(\d+)\}/g;
let match;
let blankCount = 0;
// 计算空白数量
while ((match = placeholderRegex.exec(q.question)) !== null) {
blankCount = Math.max(blankCount, parseInt(match[1]));
}
// 如果问题文本中有占位符
if (blankCount > 0) {
// 创建空白答案数组(每个空对应一个空数组)
for (let i = 0; i < blankCount; i++) {
if (!processed.blankAnswers) {
processed.blankAnswers = [];
}
if (!processed.blankAnswers[i]) {
processed.blankAnswers.push([]);
}
}
// 移除问题文本中的占位符
processed.question = q.question.replace(placeholderRegex, (match, p1) => {
const index = parseInt(p1) - 1;
return `<span class="blank-placeholder" data-index="${p1}">${p1}.________</span>`;
});
}
return processed;
}
/* ---------- 增强容错 CSV 解析器 ---------- */
function parseCSV(text) {
const errors = [];
const questions = [];
const lines = text.split(/\r\n|\n|\r/);
let rowIndex = 0;
let currentLine = '';
let inQuotes = false;
let lineNumber = 0;
let titles = [];
// 处理带引号的CSV字段的解析器
function parseCSVLine(line) {
const fields = [];
let currentField = '';
let inQuotes = false;
let i = 0;
while (i < line.length) {
const char = line[i];
// 处理引号
if (char === '"') {
// 检查是否是双引号(转义的引号)
if (i + 1 < line.length && line[i + 1] === '"') {
currentField += '"';
i += 2; // 跳过第二个引号
continue;
}
inQuotes = !inQuotes;
i++;
}
// 处理逗号分隔符(仅当不在引号内时)
else if (char === ',' && !inQuotes) {
fields.push(currentField.trim());
currentField = '';
i++;
}
// 普通字符
else {
currentField += char;
i++;
}
}
// 添加最后一个字段
if (currentField !== '' || inQuotes) {
fields.push(currentField.trim());
}
return { fields, inQuotes };
}
// 处理未闭合引号时尝试合并下一行
function processLine(line) {
lineNumber++;
const trimmedLine = line.trim();
if (!trimmedLine && !inQuotes) return null; // 跳过空行(除非在引号内)
// 累加当前行内容
currentLine += (currentLine ? '\n' : '') + line;
// 解析当前累积的内容
const result = parseCSVLine(currentLine);
inQuotes = result.inQuotes;
// 如果引号已闭合,处理这一行
if (!inQuotes) {
const processedLine = currentLine;
currentLine = '';
return { line: processedLine, lineNum: lineNumber, fields: result.fields };
}
// 引号未闭合,继续累积
return null;
}
// 主解析循环
for (let line of lines) {
const processed = processLine(line);
if (processed) {
rowIndex++;
// 第一行是标题行
if (rowIndex === 1) {
titles = processed.fields;
continue;
}
try {
const fields = processed.fields;
// 至少需要有问题、至少一个选项和答案
if (fields.length < 3) {
throw new Error(`字段数量不足(至少需要3个字段,实际${fields.length}个)`);
}
// 动态查找字段位置
const getFieldIndex = (fieldName) => {
return titles.findIndex(t => t.toLowerCase().includes(fieldName.toLowerCase()));
};
const questionIndex = getFieldIndex('问题');
const answerIndex = getFieldIndex('答案');
const typeIndex = getFieldIndex('题目类型');
const pointsIndex = getFieldIndex('题目分数');
const explanationIndex = getFieldIndex('答案解析');
const questionImageIndex = getFieldIndex('问题图片');
const scoringModeIndex = getFieldIndex('评分规则');
const blankHintPrefix = '填空项';
const keywordPrefix = '关键词';
// 验证必要字段是否存在
if (questionIndex === -1) throw new Error('未找到"问题"字段');
if (answerIndex === -1) throw new Error('未找到"答案"字段');
// 提取问题
const question = fields[questionIndex] || '';
if (!question) throw new Error('问题内容为空');
// 提取题目图片
const questionImage = questionImageIndex !== -1 ? fields[questionImageIndex] : '';
// 提取题目类型
let type = 'multiple'; // 默认多选题
if (typeIndex !== -1) {
const typeStr = fields[typeIndex].toLowerCase();
if (typeStr === 'single' || typeStr === '单选题') {
type = 'single';
} else if (typeStr === 'truefalse' || typeStr === '判断题') {
type = 'truefalse';
} else if (typeStr === 'fillblank' || typeStr === '填空题') {
type = 'fillblank';
} else if (typeStr === 'essay' || typeStr === '解答题') {
type = 'essay';
}
}
// 提取评分规则
let scoringMode = 'allOrNothing'; // 默认全对给分
if (scoringModeIndex !== -1) {
const mode = fields[scoringModeIndex].toLowerCase();
if (mode === 'perblank' || mode === '按空给分') {
scoringMode = 'perBlank';
} else if (mode === 'partialcredit' || mode === '部分给分') {
scoringMode = 'partialCredit';
}
}
// 提取选项
let options = [];
const optionPrefixes = ['A','B','C','D','E','F'];
// 提取填空项提示
let blankHints = [];
for (let i = 0; i < 6; i++) {
const hintIndex = getFieldIndex(`${blankHintPrefix}${i+1}`);
if (hintIndex !== -1 && fields[hintIndex]) {
blankHints.push(fields[hintIndex]);
}
}
// 填空题特殊处理
if (type === 'fillblank') {
// 提取填空题答案(用|分隔不同空,用;分隔同一空的多个正确答案)
let blankAnswers = [];
let answers = fields[answerIndex] || '';
// 修复:如果答案列为空,尝试使用填空项1列作为答案
if (!answers.trim() && blankHints.length > 0) {
answers = blankHints[0];
errors.push(`警告:第${lineNumber}行填空题的答案字段为空,已使用填空项1列的内容作为答案。`);
}
if (answers) {
// 按|分割不同填空项
const blankParts = answers.split('|');
blankParts.forEach(part => {
// 按;分割同一空的不同正确答案
const answersForBlank = part.split(';').map(a => a.trim()).filter(a => a);
if (answersForBlank.length > 0) {
blankAnswers.push(answersForBlank);
}
});
}
if (blankAnswers.length === 0) {
throw new Error('填空题答案不能为空');
}
// 提取题目分数
let points = 1; // 默认1分
if (pointsIndex !== -1 && fields[pointsIndex]) {
const pointsValue = parseInt(fields[pointsIndex]);
if (!isNaN(pointsValue)) {
points = pointsValue;
}
}
// 提取答案解析
let explanation = '';
if (explanationIndex !== -1) {
explanation = fields[explanationIndex] || '';
}
const fillblankQuestion = {
question: question,
questionImage: questionImage,
type: type,
points: points,
explanation: explanation,
blankAnswers: blankAnswers,
blankHints: blankHints,
scoringMode: scoringMode,
rawFields: fields // 保存原始字段
};
// 处理填空题占位符
questions.push(processFillblankQuestion(fillblankQuestion));
}
// 解答题特殊处理
else if (type === 'essay') {
// 提取关键词和分值
let keywords = [];
for (let i = 0; i < 5; i++) {
const keywordIndex = getFieldIndex(`${keywordPrefix}${i+1}`);
const pointsIndex = getFieldIndex(`${keywordPrefix}${i+1}分值`);
if (keywordIndex !== -1 && pointsIndex !== -1 &&
fields[keywordIndex] && fields[pointsIndex]) {
const keyword = fields[keywordIndex].trim();
const points = parseInt(fields[pointsIndex]) || 0;
if (keyword && points > 0) {
keywords.push({ keyword, points });
}
}
}
// 提取题目分数
let points = 1; // 默认1分
if (pointsIndex !== -1 && fields[pointsIndex]) {
const pointsValue = parseInt(fields[pointsIndex]);
if (!isNaN(pointsValue)) {
points = pointsValue;
}
}
// 提取答案解析
let explanation = '';
if (explanationIndex !== -1) {
explanation = fields[explanationIndex] || '';
}
questions.push({
question: question,
questionImage: questionImage,
type: type,
points: points,
explanation: explanation,
keywords: keywords,
scoringMode: scoringMode,
rawFields: fields // 保存原始字段
});
}
// 其他题型
else {
// 其他题型处理
optionPrefixes.forEach(prefix => {
const optionIndex = getFieldIndex(`选项${prefix}`);
const optionImageIndex = getFieldIndex(`选项${prefix}图片`);
const optionText = optionIndex !== -1 ? fields[optionIndex] : '';
const optionImage = optionImageIndex !== -1 ? fields[optionImageIndex] : '';
// 如果选项文本或图片存在,则添加选项
if (optionText || optionImage) {
options.push({
text: optionText,
image: optionImage
});
}
});
if (options.length === 0) throw new Error('未找到任何有效选项');
// 判断题特殊处理
if (type === 'truefalse') {
// 判断题强制设置为两个选项
if (options.length < 2) {
options = [
{ text: '正确', image: '' },
{ text: '错误', image: '' }
];
} else {
options = options.slice(0, 2);
}
}
// 提取答案
let answer = (fields[answerIndex] || '').toUpperCase().replace(/[^A-F]/g, '');
if (!answer) throw new Error('答案字段为空');
// 生成选项标签(A, B, C...)
const optionLabels = valid.slice(0, options.length);
// 验证答案是否在有效选项范围内
answer.split('').forEach(letter => {
if (!optionLabels.includes(letter)) {
throw new Error(`答案包含无效选项: ${letter},有效选项为${optionLabels.join(',')}`);
}
});
// 提取题目分数
let points = 1; // 默认1分
if (pointsIndex !== -1 && fields[pointsIndex]) {
const pointsValue = parseInt(fields[pointsIndex]);
if (!isNaN(pointsValue)) {
points = pointsValue;
}
}
// 提取答案解析
let explanation = '';
if (explanationIndex !== -1) {
explanation = fields[explanationIndex] || '';
}
questions.push({
question: question,
questionImage: questionImage,
options: options,
optionLabels: optionLabels,
correctAnswers: answer.split(''),
type: type,
points: points,
explanation: explanation,
rawFields: fields // 保存原始字段
});
}
} catch (err) {
errors.push(`第 ${processed.lineNum} 行解析失败:${err.message}\n行内容:${processed.line.substring(0, 100)}${processed.line.length > 100 ? '...' : ''}`);
}
}
}
// 处理文件结束时仍未闭合的引号
if (inQuotes && currentLine) {
errors.push(`文件结束时存在未闭合的引号,未处理内容:${currentLine.substring(0, 100)}${currentLine.length > 100 ? '...' : ''}`);
}
return { questions, titles, errors };
}
/* ========= Excel 解析器 ========= */
function parseExcelRows(rows, titles) {
const errors = [];
const questions = [];
rows.forEach((row, rowIndex) => {
try {
// 确保行长度与标题行一致
const fields = [];
for (let i = 0; i < titles.length; i++) {
fields.push(row[i] || '');
}
// 至少需要有问题、至少一个选项和答案
if (fields.length < 3) {
throw new Error(`字段数量不足(至少需要3个字段,实际${fields.length}个)`);
}
// 动态查找字段位置
const getFieldIndex = (fieldName) => {
return titles.findIndex(t => t.toLowerCase().includes(fieldName.toLowerCase()));
};
const questionIndex = getFieldIndex('问题');
const answerIndex = getFieldIndex('答案');
const typeIndex = getFieldIndex('题目类型');
const pointsIndex = getFieldIndex('题目分数');
const explanationIndex = getFieldIndex('答案解析');
const questionImageIndex = getFieldIndex('问题图片');
const scoringModeIndex = getFieldIndex('评分规则');
const blankHintPrefix = '填空项';
const keywordPrefix = '关键词';
// 验证必要字段是否存在
if (questionIndex === -1) throw new Error('未找到"问题"字段');
if (answerIndex === -1) throw new Error('未找到"答案"字段');
// 提取问题
const question = fields[questionIndex] || '';
if (!question) throw new Error('问题内容为空');
// 提取题目图片
const questionImage = questionImageIndex !== -1 ? fields[questionImageIndex] : '';
// 提取题目类型
let type = 'multiple'; // 默认多选题
if (typeIndex !== -1) {
const typeStr = fields[typeIndex].toString().toLowerCase();
if (typeStr === 'single' || typeStr === '单选题') {
type = 'single';
} else if (typeStr === 'truefalse' || typeStr === '判断题') {
type = 'truefalse';
} else if (typeStr === 'fillblank' || typeStr === '填空题') {
type = 'fillblank';
} else if (typeStr === 'essay' || typeStr === '解答题') {
type = 'essay';
}
}
// 提取评分规则
let scoringMode = 'allOrNothing'; // 默认全对给分
if (scoringModeIndex !== -1) {
const mode = fields[scoringModeIndex].toString().toLowerCase();
if (mode === 'perblank' || mode === '按空给分') {
scoringMode = 'perBlank';
} else if (mode === 'partialcredit' || mode === '部分给分') {
scoringMode = 'partialCredit';
}
}
// 提取选项
let options = [];
const optionPrefixes = ['A','B','C','D','E','F'];
// 提取填空项提示
let blankHints = [];
for (let i = 0; i < 6; i++) {
const hintIndex = getFieldIndex(`${blankHintPrefix}${i+1}`);
if (hintIndex !== -1 && fields[hintIndex]) {
blankHints.push(fields[hintIndex].toString());
}
}
// 填空题特殊处理
if (type === 'fillblank') {
// 提取填空题答案(用|分隔不同空,用;分隔同一空的多个正确答案)
let blankAnswers = [];
let answers = fields[answerIndex] || '';
// 修复:如果答案列为空,尝试使用填空项1列作为答案
if (!answers.toString().trim() && blankHints.length > 0) {
answers = blankHints[0];
errors.push(`警告:第${rowIndex+2}行填空题的答案字段为空,已使用填空项1列的内容作为答案。`);
}
if (answers) {
// 按|分割不同填空项
const blankParts = answers.toString().split('|');
blankParts.forEach(part => {
// 按;分割同一空的不同正确答案
const answersForBlank = part.split(';').map(a => a.trim()).filter(a => a);
if (answersForBlank.length > 0) {
blankAnswers.push(answersForBlank);
}
});
}
if (blankAnswers.length === 0) {
throw new Error('填空题答案不能为空');
}
// 提取题目分数
let points = 1; // 默认1分
if (pointsIndex !== -1 && fields[pointsIndex]) {
const pointsValue = parseInt(fields[pointsIndex]);
if (!isNaN(pointsValue)) {
points = pointsValue;
}
}
// 提取答案解析
let explanation = '';
if (explanationIndex !== -1) {
explanation = fields[explanationIndex].toString() || '';
}
const fillblankQuestion = {
question: question,
questionImage: questionImage,
type: type,
points: points,
explanation: explanation,
blankAnswers: blankAnswers,
blankHints: blankHints,
scoringMode: scoringMode,
rawFields: fields // 保存原始字段
};
// 处理填空题占位符
questions.push(processFillblankQuestion(fillblankQuestion));
}
// 解答题特殊处理
else if (type === 'essay') {
// 提取关键词和分值
let keywords = [];
for (let i = 0; i < 5; i++) {
const keywordIndex = getFieldIndex(`${keywordPrefix}${i+1}`);
const pointsIndex = getFieldIndex(`${keywordPrefix}${i+1}分值`);
if (keywordIndex !== -1 && pointsIndex !== -1 &&
fields[keywordIndex] && fields[pointsIndex]) {
const keyword = fields[keywordIndex].toString().trim();
const points = parseInt(fields[pointsIndex]) || 0;
if (keyword && points > 0) {
keywords.push({ keyword, points });
}
}
}
// 提取题目分数
let points = 1; // 默认1分
if (pointsIndex !== -1 && fields[pointsIndex]) {
const pointsValue = parseInt(fields[pointsIndex]);
if (!isNaN(pointsValue)) {
points = pointsValue;
}
}
// 提取答案解析
let explanation = '';
if (explanationIndex !== -1) {
explanation = fields[explanationIndex].toString() || '';
}
questions.push({
question: question,
questionImage: questionImage,
type: type,
points: points,
explanation: explanation,
keywords: keywords,
scoringMode: scoringMode,
rawFields: fields // 保存原始字段
});
}
// 其他题型
else {
// 其他题型处理
optionPrefixes.forEach(prefix => {
const optionIndex = getFieldIndex(`选项${prefix}`);
const optionImageIndex = getFieldIndex(`选项${prefix}图片`);
const optionText = optionIndex !== -1 ? fields[optionIndex].toString() : '';
const optionImage = optionImageIndex !== -1 ? fields[optionImageIndex].toString() : '';
// 如果选项文本或图片存在,则添加选项
if (optionText || optionImage) {
options.push({
text: optionText,
image: optionImage
});
}
});
if (options.length === 0) throw new Error('未找到任何有效选项');
// 判断题特殊处理
if (type === 'truefalse') {
// 判断题强制设置为两个选项
if (options.length < 2) {
options = [
{ text: '正确', image: '' },
{ text: '错误', image: '' }
];
} else {
options = options.slice(0, 2);
}
}
// 提取答案
let answer = (fields[answerIndex] || '').toString().toUpperCase().replace(/[^A-F]/g, '');
if (!answer) throw new Error('答案字段为空');
// 生成选项标签(A, B, C...)
const optionLabels = valid.slice(0, options.length);
// 验证答案是否在有效选项范围内
answer.split('').forEach(letter => {
if (!optionLabels.includes(letter)) {
throw new Error(`答案包含无效选项: ${letter},有效选项为${optionLabels.join(',')}`);
}
});
// 提取题目分数
let points = 1; // 默认1分
if (pointsIndex !== -1 && fields[pointsIndex]) {
const pointsValue = parseInt(fields[pointsIndex]);
if (!isNaN(pointsValue)) {
points = pointsValue;
}
}
// 提取答案解析
let explanation = '';
if (explanationIndex !== -1) {
explanation = fields[explanationIndex].toString() || '';
}
questions.push({
question: question,
questionImage: questionImage,
options: options,
optionLabels: optionLabels,
correctAnswers: answer.split(''),
type: type,
points: points,
explanation: explanation,
rawFields: fields // 保存原始字段
});
}
} catch (err) {
errors.push(`第 ${rowIndex + 2} 行解析失败:${err.message}`);
}
});
return { questions, errors };
}
/* ========= 开始答题 ========= */
function startQuiz() {
// 确保有题目
if (questions.length === 0) {
showError('题库中没有题目,请重新导入文件');
return;
}
// 隐藏文件导入部分
fileImport.style.display = 'none';
// 获取用户设置的题目数量
const singleCount = parseInt(singleCountInput.value) || 0;
const multipleCount = parseInt(multipleCountInput.value) || 0;
const truefalseCount = parseInt(truefalseCountInput.value) || 0;
const fillblankCount = parseInt(fillblankCountInput.value) || 0;
const essayCount = parseInt(essayCountInput.value) || 0;
// 抽取题目
selectedQuestions = [];
// 抽取单选题
const selectedSingle = getRandomQuestions(questionGroups.single, singleCount);
selectedQuestions = selectedQuestions.concat(selectedSingle);
// 抽取多选题
const selectedMultiple = getRandomQuestions(questionGroups.multiple, multipleCount);
selectedQuestions = selectedQuestions.concat(selectedMultiple);
// 抽取判断题
const selectedTruefalse = getRandomQuestions(questionGroups.truefalse, truefalseCount);
selectedQuestions = selectedQuestions.concat(selectedTruefalse);
// 抽取填空题
const selectedFillblank = getRandomQuestions(questionGroups.fillblank, fillblankCount);
selectedQuestions = selectedQuestions.concat(selectedFillblank);
// 抽取解答题
const selectedEssay = getRandomQuestions(questionGroups.essay, essayCount);
selectedQuestions = selectedQuestions.concat(selectedEssay);
if (selectedQuestions.length === 0) {
showError('未抽取到任何题目,请检查设置');
return;
}
// 打乱题目顺序(如果用户选择了)
if (shuffleCheckbox.checked) {
selectedQuestions = shuffleArray(selectedQuestions);
}
// 初始化并显示答题界面
current = score = answered = correct = 0;
totalPoints = selectedQuestions.reduce((sum, q) => sum + q.points, 0);
status = selectedQuestions.map((q) => {
if (q.type === 'essay') {
return {
answered: false,
score: 0,
answerText: ''
};
} else if (q.type === 'fillblank') {
return {
answered: false,
correct: false,
selected: [], // 选择题用
blankAnswers: Array(q.blankAnswers?.length || 0).fill('') // 填空题用
};
} else {
return {
answered: false,
correct: false,
selected: [] // 选择题用
};
}
});
controls.classList.add('show');
quiz.classList.add('show');
answerSheet.classList.add('show');
progressContainer.classList.add('show'); // 显示进度条
// 构建答题卡(每种题型独立编号)
buildAnswerSheet();
renderQuestion(current);
// 更新进度条
updateProgress();
// 启动考试计时器(如果启用了模拟考试模式)
if (examMode) {
startExamTimer();
}
}
// 随机抽取题目
function getRandomQuestions(allQuestions, count) {
if (count <= 0) return [];
if (count >= allQuestions.length) return [...allQuestions];
const shuffled = [...allQuestions].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
}
// 随机打乱数组
function shuffleArray(array) {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}
/* ---------- 分类答题卡 ---------- */
function buildAnswerSheet(){
// 清空所有网格
singleGrid.innerHTML = '';
multipleGrid.innerHTML = '';
truefalseGrid.innerHTML = '';
fillblankGrid.innerHTML = '';
essayGrid.innerHTML = '';
// 重置统计数据
singleTotalEl.textContent = '0';
singleCorrectEl.textContent = '0';
singleIncorrectEl.textContent = '0';
multipleTotalEl.textContent = '0';
multipleCorrectEl.textContent = '0';
multipleIncorrectEl.textContent = '0';
truefalseTotalEl.textContent = '0';
truefalseCorrectEl.textContent = '0';
truefalseIncorrectEl.textContent = '0';
fillblankTotalEl.textContent = '0';
fillblankCorrectEl.textContent = '0';
fillblankIncorrectEl.textContent = '0';
essayTotalEl.textContent = '0';
essayCorrectEl.textContent = '0';
essayIncorrectEl.textContent = '0';
// 统计每种题型的题目数量
const typeCounts = {
single: 0,
multiple: 0,
truefalse: 0,
fillblank: 0,
essay: 0
};
// 创建题目按钮(每种题型独立编号)
let questionIndex = 0;
// 单选题答题卡
const singleQuestions = selectedQuestions.filter(q => q.type === 'single');
typeCounts.single = singleQuestions.length;
singleQuestions.forEach((q, i) => {
const btn = document.createElement('button');
btn.className='answer-sheet-btn unanswered';
btn.textContent = i+1;
btn.onclick = ()=>{
if(status[questionIndex].answered || current===questionIndex){
renderQuestion(questionIndex);
}else alert('请先完成当前题');
};
singleGrid.appendChild(btn);
questionIndex++;
});
// 多选题答题卡
const multipleQuestions = selectedQuestions.filter(q => q.type === 'multiple');
typeCounts.multiple = multipleQuestions.length;
multipleQuestions.forEach((q, i) => {
const btn = document.createElement('button');
btn.className='answer-sheet-btn unanswered';
btn.textContent = i+1;
btn.onclick = ()=>{
if(status[questionIndex].answered || current===questionIndex){
renderQuestion(questionIndex);
}else alert('请先完成当前题');
};
multipleGrid.appendChild(btn);
questionIndex++;
});
// 判断题答题卡
const truefalseQuestions = selectedQuestions.filter(q => q.type === 'truefalse');
typeCounts.truefalse = truefalseQuestions.length;
truefalseQuestions.forEach((q, i) => {
const btn = document.createElement('button');
btn.className='answer-sheet-btn unanswered';
btn.textContent = i+1;
btn.onclick = ()=>{
if(status[questionIndex].answered || current===questionIndex){
renderQuestion(questionIndex);
}else alert('请先完成当前题');
};
truefalseGrid.appendChild(btn);
questionIndex++;
});
// 填空题答题卡
const fillblankQuestions = selectedQuestions.filter(q => q.type === 'fillblank');
typeCounts.fillblank = fillblankQuestions.length;
fillblankQuestions.forEach((q, i) => {
const btn = document.createElement('button');
btn.className='answer-sheet-btn unanswered';
btn.textContent = i+1;
btn.onclick = ()=>{
if(status[questionIndex].answered || current===questionIndex){
renderQuestion(questionIndex);
}else alert('请先完成当前题');
};
fillblankGrid.appendChild(btn);
questionIndex++;
});
// 解答题答题卡
const essayQuestions = selectedQuestions.filter(q => q.type === 'essay');
typeCounts.essay = essayQuestions.length;
essayQuestions.forEach((q, i) => {
const btn = document.createElement('button');
btn.className='answer-sheet-btn essay unanswered';
btn.textContent = i+1;
btn.onclick = ()=>{
if(status[questionIndex].answered || current===questionIndex){
renderQuestion(questionIndex);
}else alert('请先完成当前题');
};
essayGrid.appendChild(btn);
questionIndex++;
});
// 更新统计
singleTotalEl.textContent = typeCounts.single;
multipleTotalEl.textContent = typeCounts.multiple;
truefalseTotalEl.textContent = typeCounts.truefalse;
fillblankTotalEl.textContent = typeCounts.fillblank;
essayTotalEl.textContent = typeCounts.essay;
// 如果没有某种题型,隐藏对应的区域
el('single-section').style.display = typeCounts.single > 0 ? 'block' : 'none';
el('multiple-section').style.display = typeCounts.multiple > 0 ? 'block' : 'none';
el('truefalse-section').style.display = typeCounts.truefalse > 0 ? 'block' : 'none';
el('fillblank-section').style.display = typeCounts.fillblank > 0 ? 'block' : 'none';
el('essay-section').style.display = typeCounts.essay > 0 ? 'block' : 'none';
// 更新总题数
totalEl.textContent = selectedQuestions.length;
qTotal.textContent = `/ ${selectedQuestions.length}`;
}
/* ---------- 更新答题卡状态 ---------- */
function updateAnswerSheet() {
// 重置统计数据
let singleCorrect = 0, singleIncorrect = 0;
let multipleCorrect = 0, multipleIncorrect = 0;
let truefalseCorrect = 0, truefalseIncorrect = 0;
let fillblankCorrect = 0, fillblankIncorrect = 0;
let essayCorrect = 0, essayIncorrect = 0;
// 更新所有按钮状态
let questionIndex = 0;
// 单选题
const singleButtons = singleGrid.querySelectorAll('button');
singleButtons.forEach((btn, i) => {
const s = status[questionIndex];
btn.className = 'answer-sheet-btn';
if (questionIndex === current) {
btn.classList.add('current');
} else if (s.answered) {
if (s.correct) {
btn.classList.add('correct');
singleCorrect++;
} else {
btn.classList.add('incorrect');
singleIncorrect++;
}
} else {
btn.classList.add('unanswered');
}
questionIndex++;
});
// 多选题
const multipleButtons = multipleGrid.querySelectorAll('button');
multipleButtons.forEach((btn, i) => {
const s = status[questionIndex];
btn.className = 'answer-sheet-btn';
if (questionIndex === current) {
btn.classList.add('current');
} else if (s.answered) {
if (s.correct) {
btn.classList.add('correct');
multipleCorrect++;
} else {
btn.classList.add('incorrect');
multipleIncorrect++;
}
} else {
btn.classList.add('unanswered');
}
questionIndex++;
});
// 判断题
const truefalseButtons = truefalseGrid.querySelectorAll('button');
truefalseButtons.forEach((btn, i) => {
const s = status[questionIndex];
btn.className = 'answer-sheet-btn';
if (questionIndex === current) {
btn.classList.add('current');
} else if (s.answered) {
if (s.correct) {
btn.classList.add('correct');
truefalseCorrect++;
} else {
btn.classList.add('incorrect');
truefalseIncorrect++;
}
} else {
btn.classList.add('unanswered');
}
questionIndex++;
});
// 填空题
const fillblankButtons = fillblankGrid.querySelectorAll('button');
fillblankButtons.forEach((btn, i) => {
const s = status[questionIndex];
btn.className = 'answer-sheet-btn';
if (questionIndex === current) {
btn.classList.add('current');
} else if (s.answered) {
if (s.correct) {
btn.classList.add('correct');
fillblankCorrect++;
} else {
btn.classList.add('incorrect');
fillblankIncorrect++;
}
} else {
btn.classList.add('unanswered');
}
questionIndex++;
});
// 解答题
const essayButtons = essayGrid.querySelectorAll('button');
essayButtons.forEach((btn, i) => {
const s = status[questionIndex];
btn.className = 'answer-sheet-btn essay';
if (questionIndex === current) {
btn.classList.add('current');
} else if (s.answered) {
// 解答题根据得分比例显示状态
const q = selectedQuestions[questionIndex];
const maxPoints = q.points;
const earnedPoints = s.score;
if (earnedPoints === maxPoints) {
btn.classList.add('correct');
essayCorrect++;
} else if (earnedPoints > 0) {
btn.classList.add('partial');
essayIncorrect++;
} else {
btn.classList.add('incorrect');
essayIncorrect++;
}
} else {
btn.classList.add('unanswered');
}
questionIndex++;
});
// 更新统计显示
singleCorrectEl.textContent = singleCorrect;
singleIncorrectEl.textContent = singleIncorrect;
multipleCorrectEl.textContent = multipleCorrect;
multipleIncorrectEl.textContent = multipleIncorrect;
truefalseCorrectEl.textContent = truefalseCorrect;
truefalseIncorrectEl.textContent = truefalseIncorrect;
fillblankCorrectEl.textContent = fillblankCorrect;
fillblankIncorrectEl.textContent = fillblankIncorrect;
essayCorrectEl.textContent = essayCorrect;
essayIncorrectEl.textContent = essayIncorrect;
// 更新总答题统计
answered = status.filter(s => s.answered).length;
correct = status.filter(s => {
if (s.correct !== undefined) return s.correct; // 选择题
if (s.score !== undefined) return s.score === selectedQuestions.find(q => q).points; // 解答题满分
return false;
}).length;
answeredEl.textContent = answered;
correctEl.textContent = correct;
}
/* ---------- 更新进度条 ---------- */
function updateProgress() {
// 计算每种题型的进度
const singleAnswered = status.filter((s, i) =>
s.answered && selectedQuestions[i].type === 'single'
).length;
const multipleAnswered = status.filter((s, i) =>
s.answered && selectedQuestions[i].type === 'multiple'
).length;
const truefalseAnswered = status.filter((s, i) =>
s.answered && selectedQuestions[i].type === 'truefalse'
).length;
const fillblankAnswered = status.filter((s, i) =>
s.answered && selectedQuestions[i].type === 'fillblank'
).length;
const essayAnswered = status.filter((s, i) =>
s.answered && selectedQuestions[i].type === 'essay'
).length;
// 更新进度值
singleProgressEl.textContent = `${singleAnswered}/${questionGroups.single.length}`;
multipleProgressEl.textContent = `${multipleAnswered}/${questionGroups.multiple.length}`;
truefalseProgressEl.textContent = `${truefalseAnswered}/${questionGroups.truefalse.length}`;
fillblankProgressEl.textContent = `${fillblankAnswered}/${questionGroups.fillblank.length}`;
essayProgressEl.textContent = `${essayAnswered}/${questionGroups.essay.length}`;
// 计算总进度
const answeredCount = status.filter(s => s.answered).length;
const progress = Math.round((answeredCount / selectedQuestions.length) * 100);
progressFill.style.width = `${progress}%`;
}
/* ---------- 渲染题目 ---------- */
function renderQuestion(idx){
if(idx<0||idx>=selectedQuestions.length) return;
current = idx;
const q = selectedQuestions[idx];
// 设置题目类型
let typeText = '';
if (q.type === 'single') {
typeText = '单选题';
} else if (q.type === 'multiple') {
typeText = '多选题';
} else if (q.type === 'truefalse') {
typeText = '判断题';
} else if (q.type === 'fillblank') {
typeText = '填空题';
} else if (q.type === 'essay') {
typeText = '解答题';
}
questionTypeTag.textContent = typeText;
// 设置题目分数
questionPoints.textContent = `${q.points}分`;
qNum.textContent = `问题 ${idx+1}`;
// 收集当前题目的所有图片(用于预览)
const allImages = [];
// 渲染题目图片 - 只在有图片URL时渲染
qImageContainer.innerHTML = '';
if (q.questionImage && q.questionImage.trim() !== '') {
const imgContainer = document.createElement('div');
imgContainer.className = 'image-container';
const img = document.createElement('img');
img.className = 'question-image';
img.alt = '题目图片';
img.src = q.questionImage;
// 添加预览按钮
const previewBtn = document.createElement('button');
previewBtn.className = 'preview-btn';
previewBtn.innerHTML = '<i class="fas fa-search-plus"></i> 预览';
previewBtn.onclick = () => {
const validImages = [q.questionImage, ...(q.options || []).map(o => o.image).filter(img => img && img.trim() !== '')];
showPreview(q.questionImage, validImages);
};
imgContainer.appendChild(img);
imgContainer.appendChild(previewBtn);
qImageContainer.appendChild(imgContainer);
allImages.push(q.questionImage);
}
// 显示或隐藏选项、填空题和解答题容器
optionsContainer.style.display = ['single', 'multiple', 'truefalse'].includes(q.type) ? 'flex' : 'none';
fillblankContainer.style.display = q.type === 'fillblank' ? 'block' : 'none';
essayContainer.style.display = q.type === 'essay' ? 'block' : 'none';
// 显示或隐藏评分规则
if (q.type === 'fillblank' || q.type === 'essay') {
scoringModeEl.style.display = 'inline-block';
if (q.type === 'fillblank') {
scoringModeEl.textContent = q.scoringMode === 'perBlank' ? '按空给分' : '全对给分';
scoringModeEl.className = q.scoringMode === 'perBlank' ?
'blank-scoring-mode blank-scoring-per' : 'blank-scoring-mode blank-scoring-all';
} else if (q.type === 'essay') {
scoringModeEl.textContent = q.scoringMode === 'partialCredit' ? '部分给分' : '全对给分';
scoringModeEl.className = q.scoringMode === 'partialCredit' ?
'scoring-mode scoring-partial' : 'scoring-mode scoring-all';
}
} else {
scoringModeEl.style.display = 'none';
}
optionsContainer.innerHTML = '';
fillblankContainer.innerHTML = '';
essayAnswerBox.value = '';
el('feedback').classList.remove('show');
keywordFeedback.style.display = 'none';
explanationContent.style.display = 'none';
confirmBtn.disabled = false;
nextBtn.disabled = !status[idx].answered;
// 填空题特殊处理 - 内联输入框
if (q.type === 'fillblank') {
let questionHTML = q.question;
const blankRegex = /<span class="blank-placeholder" data-index="(\d+)">(\d+).________<\/span>/g;
let match;
while ((match = blankRegex.exec(q.question)) !== null) {
const blankIndex = parseInt(match[1]) - 1;
const userAnswer = status[idx].blankAnswers[blankIndex] || '';
const inputHtml = `<input type="text" class="inline-blank" data-index="${blankIndex}"
value="${userAnswer}"
placeholder="填写答案">`;
questionHTML = questionHTML.replace(match[0], inputHtml);
}
qText.innerHTML = questionHTML;
// 为内联输入框绑定事件
setTimeout(() => {
const blanks = qText.querySelectorAll('.inline-blank');
blanks.forEach(input => {
input.addEventListener('input', function() {
const index = parseInt(this.dataset.index);
status[idx].blankAnswers[index] = this.value.trim();
});
});
}, 0);
}
// 判断题特殊处理
else if (q.type === 'truefalse') {
qText.textContent = q.question;
optionsContainer.innerHTML = `
<div class="truefalse-container">
<div class="truefalse-option" id="true-option">
<span>✓</span>
<span>正确</span>
</div>
<div class="truefalse-option" id="false-option">
<span>✗</span>
<span>错误</span>
</div>
</div>
`;
// 添加判断题事件监听
const trueOption = el('true-option');
const falseOption = el('false-option');
trueOption.onclick = () => toggleTrueFalse(idx, 'A');
falseOption.onclick = () => toggleTrueFalse(idx, 'B');
// 设置选中状态
if (status[idx].selected.includes('A')) {
trueOption.classList.add('selected');
}
if (status[idx].selected.includes('B')) {
falseOption.classList.add('selected');
}
} else if (q.type === 'essay') {
// 解答题渲染
qText.textContent = q.question;
essayAnswerBox.value = status[idx].answerText || '';
// 如果已答,显示反馈
if (status[idx].answered) {
showEssayFeedback(idx);
}
} else {
// 其他题型渲染选项
qText.textContent = q.question;
q.optionLabels.forEach((lab, i) => {
const optDiv = document.createElement('div');
const sel = status[idx].selected.includes(lab);
optDiv.className = `option ${sel ? 'selected' : ''}`;
const option = q.options[i];
let contentHtml = '';
// 选项文本
if (option.text) {
contentHtml += `<div class="option-text">${option.text}</div>`;
}
// 选项图片
if (option.image && option.image.trim() !== '') {
contentHtml += `
<div class="image-container" style="position: relative; margin-top: 15px;">
<img src="${option.image}" class="option-image" alt="选项图片">
<button class="preview-btn">
<i class="fas fa-search-plus"></i> 预览
</button>
</div>
`;
}
optDiv.innerHTML = `
<div class="option-label">
<span class="option-letter">${lab}</span>
<div class="option-content">
${contentHtml}
</div>
</div>
`;
// 添加点击事件
optDiv.onclick = ()=> toggleOption(idx,lab);
optionsContainer.appendChild(optDiv);
// 处理选项图片
const imgContainer = optDiv.querySelector('.image-container');
if (imgContainer) {
const img = imgContainer.querySelector('img');
const previewBtn = imgContainer.querySelector('.preview-btn');
// 添加预览按钮事件
previewBtn.onclick = (e) => {
e.stopPropagation();
const validImages = [q.questionImage, ...q.options.map(o => o.image).filter(img => img && img.trim() !== '')];
showPreview(option.image, validImages);
};
}
});
}
prevBtn.disabled = idx === 0;
nextBtn.innerHTML = idx === selectedQuestions.length - 1 ? '<span>完成测验</span>' : '<span>下一题</span>';
updateAnswerSheet();
}
// 判断题选项切换
function toggleTrueFalse(idx, option) {
const s = status[idx].selected;
// 清空所有选择(判断题只能选一个)
s.length = 0;
s.push(option);
renderQuestion(idx);
}
// 其他题型选项切换
function toggleOption(idx,lab){
const q = selectedQuestions[idx];
const s = status[idx].selected;
// 如果是单选题,先清除其他选择
if (q.type === 'single') {
s.length = 0;
}
const i = s.indexOf(lab);
i>-1 ? s.splice(i,1) : s.push(lab);
renderQuestion(idx);
}
function confirmAnswer(){
const idx = current;
const q = selectedQuestions[idx];
const s = status[idx];
// 填空题特殊处理
if (q.type === 'fillblank') {
// 收集内联输入框的值
const blankInputs = qText.querySelectorAll('.inline-blank');
blankInputs.forEach((input, i) => {
s.blankAnswers[i] = input.value.trim();
});
// 检查是否所有空都已填写
if (s.blankAnswers.some(answer => !answer)) {
alert('请填写所有空白项');
return;
}
}
// 解答题特殊处理
else if (q.type === 'essay') {
s.answerText = essayAnswerBox.value.trim();
if (!s.answerText) {
alert('请填写解答内容');
return;
}
}
// 其他题型检查是否选择答案
else {
if(s.selected.length===0){
alert('请至少选择一个答案');
return;
}
}
// 检查答案是否正确
const result = checkAnswer(q, s);
const ok = result.ok;
const earnedPoints = result.points;
if(!s.answered){
answered++;
if(ok){
correct++;
}
score += earnedPoints;
}else{
const prevScore = q.type === 'essay' ? s.score : (s.correct ? q.points : 0);
score -= prevScore;
score += earnedPoints;
if(ok && !s.correct){
correct++;
}
else if(!ok && s.correct){
correct--;
}
}
s.answered = true;
s.correct = ok;
if (q.type === 'essay') {
s.score = earnedPoints;
}
showFeedback(q, s, earnedPoints);
updateStats();
confirmBtn.disabled=true;
nextBtn.disabled=false;
updateAnswerSheet();
updateProgress();
}
// 关键词文本清理函数
function cleanKeywordText(str) {
if (!str) return '';
// 移除标点、空格和特殊字符,只保留中文、英文和数字
return str.replace(/[^\w\u4e00-\u9fa5]/g, '').toLowerCase();
}
// 关键词匹配函数(支持部分匹配)
function matchKeyword(answer, keyword) {
if (!answer || !keyword) return false;
const cleanAnswer = cleanKeywordText(answer);
const cleanKeyword = cleanKeywordText(keyword);
// 如果清理后的关键词长度大于5,允许部分匹配
if (cleanKeyword.length > 5) {
// 计算关键词在答案中出现的比例
let matchedChars = 0;
for (const char of cleanKeyword) {
if (cleanAnswer.includes(char)) {
matchedChars++;
}
}
// 如果匹配字符数达到关键词长度的80%,则认为匹配
const matchThreshold = 0.8;
return (matchedChars / cleanKeyword.length) >= matchThreshold;
}
// 短关键词要求完全包含
return cleanAnswer.includes(cleanKeyword);
}
// 检查答案是否正确
function checkAnswer(q, s) {
if (q.type === 'fillblank') {
// 填空题检查
if (q.scoringMode === 'allOrNothing') {
// 全对给分模式:所有空都必须正确
const allCorrect = q.blankAnswers.every((answers, i) => {
return answers.includes(s.blankAnswers[i]);
});
return {
ok: allCorrect,
points: allCorrect ? q.points : 0
};
} else {
// 按空给分模式:计算正确空的数量
const correctBlanks = q.blankAnswers.filter((answers, i) => {
return answers.includes(s.blankAnswers[i]);
}).length;
const pointsPerBlank = q.points / q.blankAnswers.length;
const earnedPoints = Math.round(correctBlanks * pointsPerBlank * 10) / 10;
return {
ok: earnedPoints === q.points,
points: earnedPoints
};
}
} else if (q.type === 'essay') {
// 解答题检查
// 如果没有关键词,使用全文匹配
if (!q.keywords || q.keywords.length === 0) {
const isCorrect = s.answerText === q.fullAnswer;
return {
ok: isCorrect,
points: isCorrect ? q.points : 0
};
}
// 使用关键词匹配
let totalPoints = 0;
if (q.scoringMode === 'allOrNothing') {
// 全对给分模式:所有关键词都必须匹配
const allMatched = q.keywords.every(kw => {
return matchKeyword(s.answerText, kw.keyword);
});
totalPoints = allMatched ? q.points : 0;
} else {
// 部分给分模式:计算匹配关键词的总分
q.keywords.forEach(kw => {
if (matchKeyword(s.answerText, kw.keyword)) {
totalPoints += kw.points;
}
});
// 不超过题目总分
totalPoints = Math.min(totalPoints, q.points);
}
return {
ok: totalPoints === q.points,
points: totalPoints
};
} else {
// 其他题型检查
const isCorrect = JSON.stringify(s.selected.sort()) === JSON.stringify(q.correctAnswers.sort());
return {
ok: isCorrect,
points: isCorrect ? q.points : 0
};
}
}
function showFeedback(q, s, earnedPoints){
const fb = el('feedback');
if (q.type === 'fillblank') {
// 填空题反馈
let feedbackHTML = `<div>${earnedPoints > 0 ? '<span>✓</span> 部分正确' : '<span>✗</span> 不正确'}</div>`;
feedbackHTML += `<div>得分: <strong>${earnedPoints}</strong> / ${q.points}</div>`;
q.blankAnswers.forEach((answers, i) => {
const userAnswer = s.blankAnswers[i] || '未填写';
const isCorrect = answers.includes(userAnswer);
feedbackHTML += `
<div class="feedback-blank ${isCorrect ? 'correct' : 'incorrect'}">
<div><strong>空 ${i+1}:</strong> ${userAnswer}</div>
<div>${isCorrect ? '✓ 正确' : `✗ 正确答案: ${answers.join(' 或 ')}`}</div>
</div>
`;
});
fb.innerHTML = feedbackHTML;
} else if (q.type === 'essay') {
// 解答题反馈
fb.innerHTML = earnedPoints > 0
? `<div><span>✓</span> 部分正确,得分: ${earnedPoints}/${q.points}</div>`
: `<div><span>✗</span> 不正确,得分: 0/${q.points}</div>`;
// 显示关键词反馈
if (q.keywords && q.keywords.length > 0) {
keywordFeedback.style.display = 'block';
keywordFeedback.innerHTML = '<div style="font-weight:600; margin-bottom:10px;">关键词匹配情况:</div>';
q.keywords.forEach(kw => {
const isMatched = matchKeyword(s.answerText, kw.keyword);
const cleanedKeyword = cleanKeywordText(kw.keyword);
const cleanedAnswer = cleanKeywordText(s.answerText);
keywordFeedback.innerHTML += `
<div class="keyword-item ${isMatched ? 'correct' : 'incorrect'}">
<div class="keyword-text">${kw.keyword}</div>
<div class="keyword-points">${isMatched ? '+' : ''}${kw.points}分</div>
<div class="keyword-status">${isMatched ? '✓' : '✗'}</div>
<div class="keyword-match-detail">
匹配详情: 关键词 <strong>${cleanedKeyword}</strong>
${isMatched ? '在答案中被找到' : '未在答案中找到'}
</div>
</div>
`;
});
}
} else {
// 其他题型反馈
fb.innerHTML = earnedPoints > 0
? `<span>✓</span> 正确! +${q.points}分`
: `<span>✗</span> 不正确,正确答案是 ${q.correctAnswers.join('、')}`;
}
fb.className = `feedback ${earnedPoints > 0 ? 'feedback-success' : 'feedback-error'} show`;
// 显示答案解析
if (q.explanation) {
explanationContent.textContent = q.explanation;
explanationContent.style.display = 'block';
}
}
// 解答题反馈显示
function showEssayFeedback(idx) {
const q = selectedQuestions[idx];
const s = status[idx];
if (s.answered) {
showFeedback(q, s, s.score);
}
}
function go(dir){
if(dir===1 && current===selectedQuestions.length-1) return showResults();
const next = current + dir;
if(next>=0 && next<selectedQuestions.length) renderQuestion(next);
}
function updateStats(){
answeredEl.textContent = answered;
correctEl.textContent = correct;
scoreEl.textContent = score;
}
function showResults(){
quiz.classList.remove('show');
controls.classList.remove('show');
results.classList.add('show');
finalScore.textContent = score;
finalTotal.textContent = totalPoints;
// 清除考试计时器
if (examTimer) {
clearInterval(examTimer);
examTimer = null;
}
timerContainer.style.display = 'none';
}
function restart(){
results.classList.remove('show');
startQuiz();
}
function selectNewFile(){
results.classList.remove('show');
answerSheet.classList.remove('show');
progressContainer.classList.remove('show'); // 隐藏进度条
fileImport.style.display='block';
fileUpload.value='';
el('file-info').classList.remove('show');
startQuizSection.style.display = 'none';
debugInfo.classList.remove('show');
importStatus.classList.remove('show');
}
function retryFile(){
errorSec.classList.remove('show');
fileImport.style.display='block';
fileUpload.value='';
el('file-info').classList.remove('show');
startQuizSection.style.display = 'none';
debugInfo.classList.remove('show');
importStatus.classList.remove('show');
}
function showError(msg){
el('error-message').textContent = msg;
el('error-debug-details').textContent = parsingErrors.length
? parsingErrors.join('\n')
: msg;
el('error-debug-info').classList.toggle('show', parsingErrors.length > 0);
// 显示错误详情
errorDetailsContent.textContent = parsingErrors.length
? parsingErrors.join('\n\n')
: msg;
fileImport.classList.remove('show');
fileImport.style.display='none';
errorSec.classList.add('show');
importStatus.classList.remove('show');
}
/* ========= 导出测试结果功能 ========= */
function exportResults() {
// 收集错题
const wrongQuestions = [];
status.forEach((s, index) => {
const q = selectedQuestions[index];
const isCorrect = q.type === 'essay' ? s.score === q.points : s.correct;
if (!isCorrect) {
wrongQuestions.push({
question: q,
status: s,
index: index + 1
});
}
});
// 生成导出HTML内容
const exportHTML = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试结果报告</title>
<style>
body {
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f7fa;
color: #2c3e50;
line-height: 1.6;
margin: 0;
padding: 20px;
}
.export-container {
max-width: 800px;
margin: 40px auto;
padding: 30px;
background: white;
border-radius: 16px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.export-header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid #eee;
}
.export-title {
font-size: 2.2rem;
background: linear-gradient(to right, #8e44ad, #3498db);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 10px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: #f8f9ff;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
.stat-value {
font-size: 2.2rem;
font-weight: 700;
margin: 10px 0;
}
.stat-label {
color: #64748b;
font-size: 1rem;
}
.wrong-questions-section {
margin-top: 40px;
}
.section-title {
font-size: 1.5rem;
font-weight: 700;
color: #8e44ad;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f2f5;
}
.wrong-question {
background: #fef6ff;
border-radius: 12px;
padding: 25px;
margin-bottom: 25px;
border-left: 4px solid #e74c3c;
position: relative;
}
.question-header-export {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
}
.question-type-export {
display: inline-block;
padding: 6px 14px;
background: rgba(231, 76, 60, 0.1);
color: #e74c3c;
font-size: 0.9rem;
font-weight: 600;
border-radius: 50px;
}
.question-text-export {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 20px;
color: #2c3e50;
line-height: 1.5;
}
.export-options-container {
margin-bottom: 20px;
}
.export-option {
padding: 15px;
border: 1px solid #e5e7eb;
border-radius: 10px;
margin-bottom: 12px;
background: white;
}
.export-option.correct {
border-color: #10B981;
background: rgba(16, 185, 129, 0.05);
}
.export-option.incorrect {
border-color: #e74c3c;
background: rgba(231, 76, 60, 0.05);
}
.option-label-export {
display: flex;
align-items: center;
}
.option-letter-export {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
margin-right: 12px;
font-weight: 600;
font-size: 1rem;
}
.export-option.correct .option-letter-export {
background: #10B981;
color: #fff;
}
.export-option.incorrect .option-letter-export {
background: #e74c3c;
color: #fff;
}
.explanation-content-export {
padding: 20px;
background: #f8f9ff;
border-radius: 12px;
line-height: 1.7;
margin-top: 20px;
border-left: 4px solid #3498db;
}
.points-badge-export {
background: rgba(231, 111, 81, 0.15);
color: #e76f51;
padding: 6px 14px;
border-radius: 50px;
font-size: 0.95rem;
font-weight: 600;
display: inline-block;
}
.footer-note {
text-align: center;
margin-top: 40px;
color: #7f8c8d;
font-size: 0.95rem;
padding-top: 20px;
border-top: 1px solid #eee;
}
.blank-answer-container {
margin-top: 15px;
}
.blank-answer-row {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
}
.blank-number-export {
width: 28px;
height: 28px;
border-radius: 50%;
background: #9b59b6;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.export-blank-answer {
display: flex;
align-items: center;
gap: 10px;
}
.export-blank-input {
background: white;
border: 1px solid #e5e7eb;
padding: 8px 15px;
border-radius: 8px;
font-weight: 500;
}
.export-blank-correct {
background: #e6fffa;
color: #0d9488;
border-color: #0d9488;
}
.export-blank-incorrect {
background: #ffebee;
color: #e53935;
border-color: #e53935;
}
.export-blank-label {
font-weight: 600;
min-width: 80px;
}
.scoring-mode-export {
display: inline-block;
padding: 6px 14px;
border-radius: 50px;
font-weight: 600;
font-size: 0.9rem;
margin-left: 15px;
}
.scoring-per {
background: rgba(46, 204, 113, 0.15);
color: #2ecc71;
}
.scoring-all {
background: rgba(231, 76, 60, 0.15);
color: #e74c3c;
}
.scoring-partial {
background: rgba(241, 196, 15, 0.15);
color: #f39c12;
}
.essay-answer-export {
background: #f8f9ff;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
border: 1px solid #e5e7eb;
}
.keyword-item-export {
display: flex;
align-items: center;
padding: 8px;
margin: 5px 0;
border-radius: 8px;
font-size: 0.95rem;
}
.keyword-item-export.correct {
background: #e6fffa;
}
.keyword-item-export.incorrect {
background: #ffebee;
}
</style>
</head>
<body>
<div class="export-container">
<div class="export-header">
<h1 class="export-title">测试结果分析报告</h1>
<p>${new Date().toLocaleDateString()} | ${filenameEl.textContent || '未命名测试'}</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">${selectedQuestions.length}</div>
<div class="stat-label">总题数</div>
</div>
<div class="stat-card">
<div class="stat-value">${wrongQuestions.length}</div>
<div class="stat-label">错题数</div>
</div>
<div class="stat-card">
<div class="stat-value">${score}/${totalPoints}</div>
<div class="stat-label">得分/总分</div>
</div>
</div>
<div class="wrong-questions-section">
<h2 class="section-title">
<i class="fas fa-exclamation-circle"></i>
错题分析 (${wrongQuestions.length}题)
</h2>
${wrongQuestions.length > 0 ? wrongQuestions.map(q => renderWrongQuestion(q)).join('') :
`<div style="text-align: center; padding: 30px; background: #f8f9ff; border-radius: 12px;">
<i class="fas fa-check-circle" style="font-size: 3rem; color: #2ecc71; margin-bottom: 20px;"></i>
<h3>太棒了!本次测试没有错题</h3>
<p>继续保持,你已掌握所有知识点</p>
</div>`
}
</div>
<div class="footer-note">
<p>本报告由增强版答题系统生成 | ${new Date().toLocaleString()}</p>
</div>
</div>
</body>
</html>
`;
// 创建并下载文件
const blob = new Blob([exportHTML], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `测试报告_${new Date().toISOString().slice(0, 10)}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 渲染单个错题
function renderWrongQuestion(q) {
const question = q.question;
const selected = q.status.selected;
const blankAnswers = q.status.blankAnswers || [];
const answerText = q.status.answerText || '';
return `
<div class="wrong-question">
<div class="question-header-export">
<div class="question-type-export">${getQuestionTypeText(question.type)}</div>
<div class="points-badge-export">${question.points}分</div>
${question.type === 'fillblank' ?
`<span class="scoring-mode-export ${question.scoringMode === 'perBlank' ? 'scoring-per' : 'scoring-all'}">
${question.scoringMode === 'perBlank' ? '按空给分' : '全对给分'}
</span>` :
question.type === 'essay' ?
`<span class="scoring-mode-export ${question.scoringMode === 'partialCredit' ? 'scoring-partial' : 'scoring-all'}">
${question.scoringMode === 'partialCredit' ? '部分给分' : '全对给分'}
</span>` : ''
}
</div>
<div class="question-text-export">
<strong>${q.index}.</strong> ${question.question}
</div>
${question.type === 'fillblank' ? renderFillblankAnswers(question, blankAnswers) :
question.type === 'essay' ? renderEssayAnswer(question, answerText) :
renderOptions(question, selected)}
${question.explanation ? `
<div class="explanation-content-export">
<strong>解析:</strong> ${question.explanation}
</div>
` : ''}
</div>
`;
}
// 渲染填空题答案
function renderFillblankAnswers(q, userAnswers) {
let html = '<div class="blank-answer-container">';
q.blankAnswers.forEach((answers, i) => {
const userAnswer = userAnswers[i] || '未填写';
const isCorrect = answers.includes(userAnswer);
html += `
<div class="blank-answer-row">
<div class="blank-number-export">${i + 1}</div>
<div class="export-blank-answer">
<span class="export-blank-label">您的答案:</span>
<span class="export-blank-input ${isCorrect ? 'export-blank-correct' : 'export-blank-incorrect'}">${userAnswer}</span>
</div>
<div class="export-blank-answer">
<span class="export-blank-label">正确答案:</span>
<span class="export-blank-input">${answers.join(' 或 ')}</span>
</div>
</div>
`;
});
html += '</div>';
return html;
}
// 渲染解答题答案
function renderEssayAnswer(q, answerText) {
let html = `
<div class="essay-answer-export">
<div><strong>您的解答:</strong></div>
<div style="margin-top: 10px; padding: 10px; background: #f1f5f9; border-radius: 8px;">${answerText}</div>
</div>
`;
if (q.keywords && q.keywords.length > 0) {
html += '<div style="margin-top: 20px;"><strong>关键词匹配情况:</strong></div>';
q.keywords.forEach(kw => {
const isMatched = matchKeyword(answerText, kw.keyword);
html += `
<div class="keyword-item-export ${isMatched ? 'correct' : 'incorrect'}">
<div style="flex: 1;">${kw.keyword}</div>
<div style="min-width: 60px; text-align: right;">${isMatched ? '+' : ''}${kw.points}分</div>
<div style="margin-left: 10px; font-size: 1.2rem;">${isMatched ? '✓' : '✗'}</div>
</div>
`;
});
}
return html;
}
// 渲染选择题选项
function renderOptions(q, selected) {
let optionsHtml = '<div class="export-options-container">';
q.optionLabels.forEach((lab, i) => {
const isCorrect = q.correctAnswers.includes(lab);
const isSelected = selected.includes(lab);
const option = q.options[i];
let optionClass = '';
if (isCorrect) optionClass = 'correct';
else if (isSelected) optionClass = 'incorrect';
optionsHtml += `
<div class="export-option ${optionClass}">
<div class="option-label-export">
<span class="option-letter-export">${lab}</span>
<div class="option-text">${option.text || ''}</div>
</div>
${option.image && option.image.trim() !== '' ?
`<div style="margin-top: 12px;">
<img src="${option.image}" style="max-width: 100%; max-height: 200px; border-radius: 8px; border: 1px solid #e5e7eb;">
</div>` : ''
}
</div>
`;
});
optionsHtml += '</div>';
return optionsHtml + `
<div style="display: flex; gap: 20px; margin-bottom: 15px;">
<div style="flex: 1; background: rgba(231, 76, 60, 0.08); padding: 12px; border-radius: 8px;">
<strong>您的答案:</strong> ${selected.join(', ') || '未作答'}
</div>
<div style="flex: 1; background: rgba(16, 185, 129, 0.08); padding: 12px; border-radius: 8px;">
<strong>正确答案:</strong> ${q.correctAnswers.join(', ')}
</div>
</div>
`;
}
// 获取题目类型文本
function getQuestionTypeText(type) {
const types = {
'single': '单选题',
'multiple': '多选题',
'truefalse': '判断题',
'fillblank': '填空题',
'essay': '解答题'
};
return types[type] || '未知题型';
}
/* ========= 导出错题集为Excel ========= */
function exportWrongQuestionsExcel() {
// 收集错题
const wrongQuestions = [];
status.forEach((s, index) => {
const q = selectedQuestions[index];
const isCorrect = q.type === 'essay' ? s.score === q.points : s.correct;
if (!isCorrect) {
wrongQuestions.push({
question: q,
status: s,
index: index + 1
});
}
});
if (wrongQuestions.length === 0) {
alert('恭喜!本次测试没有错题,无需导出。');
return;
}
// 准备Excel数据
const wb = XLSX.utils.book_new();
// 创建工作表数据
const wsData = [globalTitles]; // 使用原始标题行作为表头
// 添加错题数据
wrongQuestions.forEach(wq => {
const q = wq.question;
// 使用原始字段数据(如果存在)
if (q.rawFields) {
wsData.push(q.rawFields);
}
// 否则动态构建字段数据
else {
const row = [];
// 动态映射字段
const fieldMap = {
'问题': q.question,
'问题图片': q.questionImage || '',
'题目类型': q.type,
'题目分数': q.points,
'答案解析': q.explanation || '',
'答案': q.type === 'fillblank' ?
q.blankAnswers.map(arr => arr.join(';')).join('|') :
q.type === 'essay' ? '' : q.correctAnswers.join(''),
'评分规则': q.type === 'fillblank' || q.type === 'essay' ? q.scoringMode : ''
};
// 添加选项字段
for (let i = 0; i < 6; i++) {
const prefix = valid[i];
if (prefix) {
fieldMap[`选项${prefix}`] = q.options && q.options[i] ? q.options[i].text : '';
fieldMap[`选项${prefix}图片`] = q.options && q.options[i] ? q.options[i].image : '';
}
}
// 添加填空提示字段
if (q.type === 'fillblank' && q.blankHints) {
q.blankHints.forEach((hint, idx) => {
fieldMap[`填空项${idx + 1}`] = hint;
});
}
// 添加关键词字段
if (q.type === 'essay' && q.keywords) {
q.keywords.forEach((kw, idx) => {
fieldMap[`关键词${idx + 1}`] = kw.keyword;
fieldMap[`关键词${idx + 1}分值`] = kw.points;
});
}
// 按照原始标题顺序构建行
globalTitles.forEach(title => {
row.push(fieldMap[title] || '');
});
wsData.push(row);
}
});
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(wsData);
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, '错题集');
// 生成Excel文件并下载
XLSX.writeFile(wb, `错题集_${new Date().toISOString().slice(0, 10)}.xlsx`);
}
/* ========= 模拟考试模式功能 ========= */
function toggleExamMode() {
examMode = examModeToggle.checked;
examDurationInput.disabled = !examMode;
// 更新考试时长
if (examMode) {
examDuration = parseInt(examDurationInput.value) || 30;
}
}
function startExamTimer() {
// 清除现有计时器
if (examTimer) {
clearInterval(examTimer);
}
// 显示计时器
timerContainer.style.display = 'flex';
// 设置初始剩余时间(秒)
remainingTime = examDuration * 60;
// 更新计时器显示
updateTimerDisplay();
// 启动计时器
examTimer = setInterval(() => {
remainingTime--;
updateTimerDisplay();
// 最后5分钟警告
if (remainingTime === 5 * 60) {
timerValue.classList.add('timer-warning');
alert('考试剩余5分钟!');
}
// 考试结束
if (remainingTime <= 0) {
clearInterval(examTimer);
timerValue.textContent = "00:00";
alert('考试时间到!系统将自动提交试卷。');
forceSubmitExam();
}
}, 1000);
}
function updateTimerDisplay() {
const minutes = Math.floor(remainingTime / 60).toString().padStart(2, '0');
const seconds = (remainingTime % 60).toString().padStart(2, '0');
timerValue.textContent = `${minutes}:${seconds}`;
}
function forceSubmitExam() {
// 自动提交所有未答题目
for (let i = 0; i < selectedQuestions.length; i++) {
if (!status[i].answered) {
// 如果是填空题,尝试获取答案
if (selectedQuestions[i].type === 'fillblank') {
const blankInputs = document.querySelectorAll('.inline-blank');
blankInputs.forEach((input, index) => {
status[i].blankAnswers[index] = input.value.trim();
});
}
// 如果是解答题,获取答案文本
else if (selectedQuestions[i].type === 'essay') {
status[i].answerText = essayAnswerBox.value.trim();
}
// 标记为已答
status[i].answered = true;
// 检查答案
const result = checkAnswer(selectedQuestions[i], status[i]);
status[i].correct = result.ok;
if (selectedQuestions[i].type === 'essay') {
status[i].score = result.points;
}
// 更新统计
if (result.ok) {
correct++;
}
score += result.points;
}
}
// 更新统计
answered = selectedQuestions.length;
updateStats();
updateAnswerSheet();
// 显示结果
showResults();
}
// 初始化页面状态
function initPage() {
fileImport.style.display = 'block';
quiz.classList.remove('show');
results.classList.remove('show');
errorSec.classList.remove('show');
answerSheet.classList.remove('show');
progressContainer.classList.remove('show');
controls.classList.remove('show');
debugInfo.classList.remove('show');
startQuizSection.style.display = 'none';
importStatus.classList.remove('show');
timerContainer.style.display = 'none';
// 为考试时长输入框添加验证
examDurationInput.addEventListener('change', function() {
let value = parseInt(this.value);
if (isNaN(value)) value = 30;
if (value < 1) value = 1;
if (value > 180) value = 180;
this.value = value;
examDuration = value;
});
}
window.addEventListener('load', initPage);
</script>
</body>
</html>
题库制作工具
源码
[HTML] 纯文本查看 复制代码 <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>题库制作工具</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.sheetjs.com/xlsx-0.20.0/package/dist/xlsx.full.min.js"></script>
<style>
/* 原有样式保持不变 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
color: #333;
line-height: 1.6;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
padding: 30px 0;
margin-bottom: 30px;
}
h1 {
font-size: 2.8rem;
font-weight: 700;
background: linear-gradient(90deg, #4361ee, #3a0ca3);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 10px;
}
.subtitle {
color: #6c757d;
font-size: 1.2rem;
max-width: 700px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
padding: 30px;
margin-bottom: 30px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.12);
}
.card-title {
font-size: 1.5rem;
color: #3a0ca3;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f2f5;
display: flex;
align-items: center;
}
.card-title i {
margin-right: 10px;
color: #4361ee;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
}
input, textarea, select {
width: 100%;
padding: 14px;
border: 2px solid #e1e5eb;
border-radius: 10px;
font-size: 1rem;
transition: border-color 0.3s;
}
textarea {
min-height: 120px;
resize: vertical;
}
input:focus, textarea:focus, select:focus {
border-color: #4361ee;
outline: none;
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15);
}
.option-row {
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 10px;
}
.option-input {
flex: 1;
}
.option-actions {
display: flex;
gap: 8px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
border: none;
gap: 8px;
}
.btn i {
font-size: 1.1rem;
}
.btn-primary {
background: #4361ee;
color: white;
}
.btn-primary:hover {
background: #3a0ca3;
transform: translateY(-2px);
}
.btn-success {
background: #2a9d8f;
color: white;
}
.btn-success:hover {
background: #21867a;
transform: translateY(-2px);
}
.btn-danger {
background: #e76f51;
color: white;
}
.btn-danger:hover {
background: #d45d3f;
transform: translateY(-2px);
}
.btn-outline {
background: transparent;
border: 2px solid #4361ee;
color: #4361ee;
}
.btn-outline:hover {
background: #f0f4ff;
}
.btn-sm {
padding: 8px 15px;
font-size: 0.9rem;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.actions {
display: flex;
gap: 15px;
flex-wrap: wrap;
margin-top: 20px;
}
.question-list {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.question-list th {
background: #f8f9fa;
padding: 15px;
text-align: left;
color: #495057;
font-weight: 600;
border-bottom: 2px solid #e9ecef;
}
.question-list td {
padding: 15px;
border-bottom: 1px solid #e9ecef;
vertical-align: top;
}
.question-list tr:hover td {
background: #f8f9ff;
}
.question-text {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.options-preview {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.option-tag {
background: #e7f4ff;
color: #1a73e8;
padding: 5px 12px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
}
.option-tag.correct {
background: #e6f7ee;
color: #0a9d58;
}
.list-actions {
display: flex;
gap: 10px;
}
.action-btn {
padding: 8px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: #f8f9fa;
border: 1px solid #e1e5eb;
color: #495057;
}
.action-btn:hover {
background: #edf2ff;
color: #4361ee;
border-color: #d0d9ff;
}
.preview-card {
background: #f8f9ff;
border: 2px dashed #d0d9ff;
padding: 25px;
border-radius: 12px;
margin-top: 20px;
}
.preview-title {
font-size: 1.2rem;
color: #4361ee;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.preview-question {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 20px;
color: #212529;
}
.preview-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
}
.preview-option {
padding: 15px;
border: 2px solid #e1e5eb;
border-radius: 10px;
background: white;
display: flex;
align-items: center;
gap: 15px;
transition: all 0.2s;
cursor: default;
}
.preview-option.correct {
border-color: #2a9d8f;
background: #f0faf7;
}
.option-letter {
width: 36px;
height: 36px;
border-radius: 50%;
background: #f0f4ff;
color: #4361ee;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
flex-shrink: 0;
}
.preview-option.correct .option-letter {
background: #2a9d8f;
color: white;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #6c757d;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 20px;
color: #ced4da;
}
.counter {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding-top: 20px;
border-top: 2px solid #f0f2f5;
font-weight: 600;
color: #495057;
flex-wrap: wrap;
gap: 15px;
}
.counter div {
background: #f8f9ff;
padding: 8px 15px;
border-radius: 8px;
border: 1px solid #e1e5eb;
}
.counter span {
color: #4361ee;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 10px;
background: white;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 15px;
transform: translateX(150%);
transition: transform 0.4s ease;
z-index: 1000;
}
.notification.show {
transform: translateX(0);
}
.notification.success {
border-left: 4px solid #2a9d8f;
}
.notification.error {
border-left: 4px solid #e76f51;
}
.notification i {
font-size: 1.5rem;
}
.notification.success i {
color: #2a9d8f;
}
.notification.error i {
color: #e76f51;
}
.notification-content h3 {
margin-bottom: 5px;
}
.highlight {
animation: highlight 1.5s ease;
}
@keyframes highlight {
0% { background-color: rgba(67, 97, 238, 0.2); }
100% { background-color: transparent; }
}
.question-type {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.type-option {
flex: 1;
text-align: center;
padding: 15px;
border-radius: 10px;
background: #f8f9fa;
cursor: pointer;
border: 2px solid #e1e5eb;
transition: all 0.3s;
}
.type-option.selected {
border-color: #4361ee;
background: #eef2ff;
box-shadow: 0 5px 15px rgba(67, 97, 238, 0.1);
}
.type-option i {
font-size: 1.5rem;
margin-bottom: 10px;
color: #4361ee;
}
.type-option.selected i {
color: #3a0ca3;
}
.points-input {
display: flex;
align-items: center;
gap: 10px;
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
}
.points-input input {
max-width: 80px;
}
.points-label {
font-weight: 600;
}
.points-hint {
color: #6c757d;
font-size: 0.9rem;
}
.badge {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
.badge-single {
background: rgba(67, 97, 238, 0.15);
color: #4361ee;
}
.badge-multiple {
background: rgba(42, 157, 143, 0.15);
color: #2a9d8f;
}
.badge-truefalse {
background: rgba(231, 111, 81, 0.15);
color: #e76f51;
}
.badge-fillblank {
background: rgba(155, 89, 182, 0.15);
color: #9b59b6;
}
.badge-essay {
background: rgba(241, 196, 15, 0.15);
color: #f39c12;
}
.badge-points {
background: rgba(231, 111, 81, 0.15);
color: #e76f51;
}
/* 新布局样式 - 优化部分 */
.import-export-actions {
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
margin-top: 20px;
}
.actions-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
width: 100%;
}
.action-group {
display: flex;
flex: 1;
min-width: 120px;
height: 48px;
}
.filename-group {
flex: 1;
min-width: 180px;
max-width: 300px;
height: 48px;
}
.filename-group input {
padding: 12px 15px;
height: 100%;
min-width: 250px;
}
.encoding-group {
min-width: 130px;
max-width: 150px;
height: 48px;
}
.encoding-group select {
padding: 12px 15px;
padding-right: 35px;
border-radius: 10px;
background: #f8f9fa;
border: 2px solid #4361ee;
color: #4361ee;
font-weight: 600;
cursor: pointer;
appearance: none;
position: relative;
height: 100%;
width: 100%;
}
.btn-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
flex: 1;
}
.btn-group .btn {
flex: 1;
min-width: 120px;
width: 100%;
height: 48px;
padding: 0 15px;
}
/* 图片相关样式 */
.image-container {
margin-top: 10px;
position: relative;
display: none;
background: #f8f9ff;
padding: 15px;
border-radius: 10px;
border: 1px solid #e1e5eb;
}
.image-preview {
max-width: 100%;
max-height: 200px;
border-radius: 8px;
margin-top: 10px;
display: none;
border: 1px solid #e1e5eb;
overflow: auto;
}
.image-preview img {
max-width: 100%;
max-height: 200px;
display: block;
}
.remove-image-btn {
position: absolute;
top: 5px;
right: 5px;
background: rgba(231, 111, 81, 0.8);
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 0.8rem;
}
.add-image-btn {
margin-top: 8px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #f0f4ff;
border: 1px dashed #4361ee;
border-radius: 8px;
color: #4361ee;
cursor: pointer;
transition: all 0.2s;
}
.add-image-btn:hover {
background: #e1e9ff;
}
.option-image-container {
margin-top: 8px;
position: relative;
display: none;
background: #f8f9ff;
padding: 10px;
border-radius: 8px;
border: 1px solid #e1e5eb;
}
.option-image-preview {
max-width: 100%;
max-height: 100px;
border-radius: 6px;
margin-top: 8px;
display: none;
border: 1px solid #e1e5eb;
overflow: auto;
}
.option-image-preview img {
max-width: 100%;
max-height: 100px;
display: block;
}
.image-input {
margin-top: 8px;
}
.image-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #4361ee;
}
.option-image-label {
font-size: 0.9rem;
margin-top: 8px;
}
.preview-image {
max-width: 100%;
max-height: 300px;
border-radius: 8px;
margin: 15px 0;
display: block;
border: 1px solid #e1e5eb;
}
.option-preview-image {
max-width: 100%;
max-height: 100px;
border-radius: 6px;
margin-top: 8px;
}
.question-text-with-image {
display: flex;
flex-direction: column;
gap: 10px;
}
.option-with-image {
display: flex;
flex-direction: column;
gap: 5px;
}
/* 图片选项样式 */
.image-option {
border: 2px solid #e1e5eb;
border-radius: 10px;
padding: 10px;
text-align: center;
transition: all 0.2s;
cursor: pointer;
position: relative;
overflow: hidden;
height: 150px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.image-option:hover {
border-color: #4361ee;
transform: translateY(-3px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.image-option.selected {
border-color: #4361ee;
background-color: #f0f4ff;
}
.image-option.correct {
border-color: #2a9d8f;
background-color: #e6f7ee;
}
.option-image {
max-height: 100px;
max-width: 100%;
object-fit: contain;
margin: 0 auto;
flex-grow: 1;
}
.option-image-label {
font-size: 0.9rem;
margin-top: 5px;
font-weight: 600;
color: #4361ee;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.image-option .option-letter {
position: absolute;
top: 5px;
left: 5px;
width: 28px;
height: 28px;
font-size: 0.9rem;
}
.image-option-preview {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.image-option-preview-item {
border: 2px solid #e1e5eb;
border-radius: 10px;
padding: 10px;
text-align: center;
height: 120px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.image-option-preview-item img {
max-height: 80px;
max-width: 100%;
object-fit: contain;
margin: 0 auto;
}
.image-option-preview-label {
font-size: 0.8rem;
margin-top: 5px;
font-weight: 600;
color: #4361ee;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.option-type-toggle {
display: flex;
gap: 10px;
margin-top: 10px;
}
.option-type-btn {
flex: 1;
padding: 8px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: #f8f9fa;
border: 2px solid #e1e5eb;
text-align: center;
font-size: 0.9rem;
}
.option-type-btn.selected {
background: #eef2ff;
border-color: #4361ee;
color: #4361ee;
font-weight: 600;
}
/* 填空题样式 */
#blanks-section {
display: none;
background: #f8f9ff;
padding: 15px;
border-radius: 12px;
border: 1px solid #e1e5eb;
margin-bottom: 15px;
}
.blank-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
padding: 12px;
background: white;
border-radius: 8px;
border: 1px solid #e1e5eb;
}
.blank-row .blank-number {
width: 30px;
height: 30px;
border-radius: 50%;
background: #9b59b6;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.blank-input {
flex: 1;
}
.blank-points {
width: 100px;
}
.scoring-mode {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.mode-option {
flex: 1;
padding: 12px;
border-radius: 8px;
background: #f8f9fa;
border: 2px solid #e1e5eb;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.mode-option.selected {
background: #f0e6ff;
border-color: #9b59b6;
color: #9b59b6;
font-weight: 600;
}
.mode-option i {
margin-right: 5px;
color: #9b59b6;
}
.preview-blank {
display: flex;
flex-direction: column;
gap: 10px;
background: white;
border-radius: 8px;
padding: 15px;
border: 1px solid #e1e5eb;
margin-bottom: 10px;
}
.preview-blank-header {
display: flex;
align-items: center;
gap: 10px;
}
.preview-blank-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: #9b59b6;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.preview-blank-content {
padding-left: 38px;
}
.blank-answer {
background: #f0e6ff;
padding: 5px 10px;
border-radius: 4px;
margin-top: 5px;
display: inline-block;
}
/* 填空题增强样式 */
.question-text-editor {
position: relative;
}
.insert-blank-toolbar {
display: flex;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
display: none; /* 默认隐藏 */
}
.insert-blank-btn {
padding: 8px 15px;
background: #eef2ff;
border: 1px solid #d0d9ff;
border-radius: 6px;
color: #4361ee;
cursor: pointer;
font-size: 0.95rem;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.2s;
}
.insert-blank-btn:hover {
background: #dce5ff;
transform: translateY(-2px);
}
.blank-preview {
background: #f8f9ff;
padding: 15px;
border-radius: 10px;
margin-top: 15px;
border: 1px solid #e1e5eb;
}
.blank-preview-title {
font-weight: 600;
color: #4361ee;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.blank-preview-content {
line-height: 1.6;
font-size: 1.1rem;
padding: 10px;
}
/* 优化填空项占位符样式 */
.blank-placeholder {
display: inline-block;
position: relative;
color: #9b59b6;
font-weight: 600;
background: rgba(155, 89, 182, 0.08);
border-bottom: 2px solid #9b59b6;
padding: 0 8px;
border-radius: 4px;
margin: 0 2px;
}
.blank-answer-reveal {
display: inline-block;
position: relative;
color: #2a9d8f;
font-weight: 600;
background: rgba(42, 157, 143, 0.08);
border-bottom: 2px solid #2a9d8f;
padding: 0 8px;
border-radius: 4px;
margin: 0 2px;
}
.show-answers-btn {
margin-top: 15px;
}
/* 功能说明卡片 */
.feature-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-top: 20px;
}
.feature-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
transition: transform 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-icon {
width: 60px;
height: 60px;
background: #f0f4ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
font-size: 1.5rem;
color: #4361ee;
}
.feature-title {
font-size: 1.2rem;
margin-bottom: 10px;
color: #3a0ca3;
}
/* 拖拽排序样式 */
.question-list tr.dragging {
background-color: #f0f7ff;
opacity: 0.8;
}
.question-list tr.drag-over {
border-top: 2px solid #4361ee;
}
.drag-handle {
cursor: move;
padding: 8px;
color: #6c757d;
opacity: 0.6;
transition: all 0.2s;
}
.drag-handle:hover {
color: #4361ee;
opacity: 1;
}
/* 解答题样式 */
#essay-section {
display: none;
background: #f8f9ff;
padding: 15px;
border-radius: 12px;
border: 1px solid #e1e5eb;
margin-bottom: 15px;
}
.keyword-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
padding: 12px;
background: white;
border-radius: 8px;
border: 1px solid #e1e5eb;
}
.keyword-row .keyword-number {
width: 30px;
height: 30px;
border-radius: 50%;
background: #f39c12;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.keyword-input {
flex: 1;
}
.keyword-points {
width: 100px;
}
.preview-keyword {
display: flex;
flex-direction: column;
gap: 10px;
background: white;
border-radius: 8px;
padding: 15px;
border: 1px solid #e1e5eb;
margin-bottom: 10px;
}
.preview-keyword-header {
display: flex;
align-items: center;
gap: 10px;
}
.preview-keyword-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: #f39c12;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
flex-shrink: 0;
}
.preview-keyword-content {
padding-left: 38px;
}
.keyword-answer {
background: #fef9e7;
padding: 5px 10px;
border-radius: 4px;
margin-top: 5px;
display: inline-block;
}
/* 解答题评分规则 */
.essay-scoring-mode {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
/* 解答题分数高亮 */
.essay-points {
background: rgba(241, 196, 15, 0.2);
border: 1px solid #f39c12;
}
/* 响应式设计 */
@media (max-width: 768px) {
.actions {
flex-direction: column;
}
.btn {
width: 100%;
}
.question-list {
display: block;
overflow-x: auto;
}
.preview-options {
grid-template-columns: 1fr;
}
.option-row {
flex-direction: column;
align-items: flex-start;
}
.option-actions {
width: 100%;
justify-content: space-between;
}
.import-export-actions {
flex-direction: column;
}
.action-group, .filename-group, .encoding-group {
width: 100%;
max-width: 100%;
}
.question-type {
flex-direction: column;
}
.truefalse-options {
flex-direction: column;
}
.preview-image {
max-height: 200px;
}
.image-preview {
max-height: 150px;
}
.image-option-preview {
grid-template-columns: repeat(2, 1fr);
}
.blank-row, .keyword-row {
flex-direction: column;
align-items: stretch;
}
.blank-points, .keyword-points {
width: 100%;
}
.scoring-mode, .essay-scoring-mode {
flex-direction: column;
}
.insert-blank-toolbar {
flex-direction: column;
}
.insert-blank-btn {
width: 100%;
justify-content: center;
}
/* 移动端按钮优化 */
.actions-row {
flex-direction: column;
}
.action-group {
min-width: 100%;
max-width: 100%;
}
.file-encoding-group {
flex-direction: column;
gap: 10px;
}
.btn-group {
margin-left: 0;
width: 100%;
}
}
/* 新增布局优化 */
.file-encoding-group {
display: flex;
gap: 15px;
width: 100%;
}
/* 导入导出区域优化 */
.import-export-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
width: 100%;
margin-bottom: 15px;
}
.import-section {
display: flex;
gap: 15px;
align-items: center;
}
.filename-section {
flex-grow: 1;
max-width: 350px;
}
.export-section {
display: flex;
gap: 15px;
align-items: center;
margin-left: auto;
}
/* 优化后的操作区域 */
.import-export-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.filename-container {
flex: 1;
min-width: 200px;
max-width: 300px;
}
.encoding-container {
min-width: 120px;
max-width: 140px;
}
.buttons-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
flex: 1;
}
.action-button {
flex: 1;
min-width: 150px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 15px;
}
/* 题目统计卡片 */
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
margin-top: 20px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 15px;
text-align: center;
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
position: relative;
border-left: 4px solid #4361ee;
}
.stat-value {
font-size: 1.8rem;
font-weight: 700;
color: #3a0ca3;
margin: 10px 0;
}
.stat-label {
color: #6c757d;
font-size: 0.9rem;
}
/* 解答题提示 */
.essay-hint {
background: #fff8e6;
border-left: 4px solid #f39c12;
padding: 10px 15px;
border-radius: 0 8px 8px 0;
margin-bottom: 15px;
font-size: 0.9rem;
}
/* 判断题选项样式修复 */
.truefalse-options {
display: flex;
gap: 15px;
margin-top: 10px;
}
.truefalse-option {
flex: 1;
text-align: center;
padding: 15px;
border-radius: 10px;
background: #f8f9fa;
cursor: pointer;
border: 2px solid #e1e5eb;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.truefalse-option.selected {
border-color: #4361ee;
background: #eef2ff;
box-shadow: 0 5px 15px rgba(67, 97, 238, 0.1);
}
.truefalse-option:hover {
border-color: #4361ee;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>题库制作工具</h1>
<p class="subtitle">支持Excel/CSV导入导出,题目和选项添加图片</p>
</header>
<div class="card">
<h2 class="card-title"><i class="fas fa-plus-circle"></i> 添加新题目</h2>
<div class="form-group question-text-editor">
<label for="question">问题内容 <span style="font-weight:normal;color:#6c757d">(使用 {1}、{2} 等标记填空位置)</span></label>
<textarea id="question" placeholder="输入问题描述..."></textarea>
<!-- 插入填空项按钮 -->
<div class="insert-blank-toolbar" id="insert-blank-toolbar">
<div class="insert-blank-btn" id="insert-blank-1">
<i class="fas fa-underline"></i> 插入填空项 1
</div>
<div class="insert-blank-btn" id="insert-blank-2">
<i class="fas fa-underline"></i> 插入填空项 2
</div>
<div class="insert-blank-btn" id="insert-blank-3">
<i class="fas fa-underline"></i> 插入填空项 3
</div>
</div>
<div class="add-image-btn" id="add-question-image-btn">
<i class="fas fa-image"></i> 添加题目图片
</div>
<div class="image-container" id="question-image-container">
<label class="image-label">
<i class="fas fa-link"></i> 图片URL:
</label>
<input type="text" id="question-image-url" class="image-input" placeholder="输入图片URL">
<button class="remove-image-btn" id="remove-question-image-btn">
<i class="fas fa-times"></i>
</button>
<div class="image-preview" id="question-image-preview"></div>
</div>
<!-- 题目预览区域 -->
<div class="blank-preview">
<div class="blank-preview-title">
<i class="fas fa-eye"></i> 题目预览
</div>
<div class="blank-preview-content" id="question-preview"></div>
</div>
</div>
<div class="form-group">
<label>题目类型</label>
<div class="question-type">
<div class="type-option selected" id="single-type">
<i class="fas fa-dot-circle"></i>
<h3>单选题</h3>
<p>只能选择一个正确答案</p>
</div>
<div class="type-option" id="multiple-type">
<i class="fas fa-check-double"></i>
<h3>多选题</h3>
<p>可选择一个或多个正确答案</p>
</div>
<div class="type-option" id="truefalse-type">
<i class="fas fa-check-circle"></i>
<h3>判断题</h3>
<p>判断正确或错误</p>
</div>
<div class="type-option" id="fillblank-type">
<i class="fas fa-pen"></i>
<h3>填空题</h3>
<p>填写正确答案</p>
</div>
<div class="type-option" id="essay-type">
<i class="fas fa-file-alt"></i>
<h3>解答题</h3>
<p>详细解答问题</p>
</div>
</div>
</div>
<div class="form-group">
<label>题目分数</label>
<div class="points-input">
<span class="points-label">分值:</span>
<input type="number" id="question-points" min="1" max="10" value="3">
<span class="points-hint" id="points-hint">(1-10分)</span>
</div>
</div>
<div class="form-group" id="options-section">
<label>选项设置</label>
<div id="options-container">
<!-- 选项会动态添加到这里 -->
</div>
<button id="add-option-btn" class="btn btn-outline" style="margin-top: 10px;">
<i class="fas fa-plus"></i> 添加选项
</button>
</div>
<div class="form-group" id="truefalse-section" style="display: none;">
<label>判断题选项</label>
<div class="truefalse-options">
<div class="truefalse-option" id="true-option" data-value="true">
<i class="fas fa-check-circle"></i>
<span>正确</span>
</div>
<div class="truefalse-option" id="false-option" data-value="false">
<i class="fas fa-times-circle"></i>
<span>错误</span>
</div>
</div>
</div>
<!-- 填空题设置区域 -->
<div id="blanks-section" style="display: none;">
<div class="form-group">
<label>评分规则</label>
<div class="scoring-mode">
<div class="mode-option" id="per-blank-mode">
<i class="fas fa-check-square"></i> 按空给分
</div>
<div class="mode-option selected" id="all-or-nothing-mode">
<i class="fas fa-ban"></i> 全对给分
</div>
</div>
</div>
<div class="form-group">
<label>填空项设置</label>
<div id="blanks-container">
<!-- 填空项会动态添加到这里 -->
</div>
<button id="add-blank-btn" class="btn btn-outline" style="margin-top: 10px;">
<i class="fas fa-plus"></i> 添加填空项
</button>
</div>
</div>
<!-- 解答题设置区域 -->
<div id="essay-section" style="display: none;">
<div class="essay-hint">
<i class="fas fa-info-circle"></i> 解答题可以设置评分关键词(可选),也可以只提供完整答案
</div>
<!-- 新增:解答题评分规则 -->
<div class="form-group">
<label>评分规则</label>
<div class="essay-scoring-mode">
<div class="mode-option selected" id="partial-credit-mode">
<i class="fas fa-percentage"></i> 按关键词给分
</div>
<div class="mode-option" id="all-or-nothing-essay-mode">
<i class="fas fa-ban"></i> 全对给分
</div>
</div>
</div>
<div class="form-group">
<label for="essay-answer">完整答案 <span style="font-weight:normal;color:#6c757d">(必填)</span></label>
<textarea id="essay-answer" placeholder="输入问题的完整答案..."></textarea>
</div>
<div class="form-group">
<label>评分关键词设置 <span style="font-weight:normal;color:#6c757d">(可选)</span></label>
<div id="keywords-container">
<!-- 评分关键词会动态添加到这里 -->
</div>
<button id="add-keyword-btn" class="btn btn-outline" style="margin-top: 10px;">
<i class="fas fa-plus"></i> 添加关键词
</button>
</div>
</div>
<div class="form-group">
<label for="explanation">答案解析 <span class="hint">(可选)</span></label>
<textarea id="explanation" placeholder="输入题目解析、解题思路或知识点..."></textarea>
</div>
<div class="actions">
<button id="add-question-btn" class="btn btn-primary">
<i class="fas fa-save"></i> 保存题目
</button>
<button id="preview-btn" class="btn btn-outline">
<i class="fas fa-eye"></i> 预览题目
</button>
<button id="reset-btn" class="btn btn-outline">
<i class="fas fa-redo"></i> 重置表单
</button>
</div>
<div id="preview-section" class="preview-card" style="display: none;">
<div class="preview-title">
<i class="fas fa-search"></i> 题目预览
</div>
<div id="preview-question-container">
<!-- 预览内容会动态生成 -->
</div>
<div id="preview-blanks">
<!-- 预览内容会动态生成 -->
</div>
<div id="preview-keywords">
<!-- 预览内容会动态生成 -->
</div>
<button id="show-answers-btn" class="btn btn-outline show-answers-btn" style="display: none;">
<i class="fas fa-eye"></i> 显示答案
</button>
<div id="preview-explanation" style="display: none; margin-top: 20px; padding: 15px; background: #f8f9ff; border-radius: 10px; border-left: 4px solid #4361ee;">
<div style="font-weight: 600; color: #4361ee; margin-bottom: 10px; display: flex; align-items: center; gap: 10px;">
<i class="fas fa-lightbulb"></i> 答案解析
</div>
<div id="explanation-content"></div>
</div>
</div>
</div>
<div class="card">
<h2 class="card-title"><i class="fas fa-list"></i> 题目列表</h2>
<div class="stats-container">
<div class="stat-card">
<div class="stat-value" id="total-count">0</div>
<div class="stat-label">题目总数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="total-points">0</div>
<div class="stat-label">总分数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="image-count">0</div>
<div class="stat-label">含图片题目</div>
</div>
<div class="stat-card">
<div class="stat-value" id="essay-count">0</div>
<div class="stat-label">解答题数量</div>
</div>
</div>
<div id="questions-table">
<table class="question-list">
<thead>
<tr>
<th width="5%"></th>
<th width="5%">#</th>
<th width="20%">问题</th>
<th width="20%">选项/关键词</th>
<th width="15%">类型</th>
<th width="10%">分数</th>
<th width="15%">操作</th>
</tr>
</thead>
<tbody id="questions-list">
<tr id="empty-row">
<td colspan="7" class="empty-state">
<i class="fas fa-inbox"></i>
<h3>题库为空</h3>
<p>请添加题目或导入文件</p>
</td>
</tr>
</tbody>
</table>
</div>
<div class="counter">
<div>单选题: <span id="single-count">0</span></div>
<div>多选题: <span id="multiple-count">0</span></div>
<div>判断题: <span id="truefalse-count">0</span></div>
<div>填空题: <span id="fillblank-count">0</span></div>
<div>解答题: <span id="essay-count2">0</span></div>
</div>
<!-- 优化后的操作按钮区域 -->
<div class="import-export-actions">
<div class="import-export-container">
<div class="filename-container">
<input type="text" id="export-filename" class="filename-input" placeholder="输入文件名" value="我的题库">
</div>
<div class="encoding-container">
<select id="common-encoding">
<option value="utf-8">UTF-8</option>
<option value="gbk">GBK</option>
</select>
</div>
<div class="buttons-container">
<button id="import-btn" class="btn btn-outline action-button">
<i class="fas fa-file-import"></i> 导入题库
</button>
<button id="export-excel-btn" class="btn btn-success action-button" disabled>
<i class="fas fa-file-excel"></i> 导出Excel
</button>
<button id="export-csv-btn" class="btn btn-success action-button" disabled>
<i class="fas fa-file-csv"></i> 导出CSV
</button>
<button id="clear-all-btn" class="btn btn-danger action-button" disabled>
<i class="fas fa-trash"></i> 清空题库
</button>
</div>
</div>
<input type="file" id="import-file" accept=".xlsx, .xls, .csv" style="display:none;">
</div>
<div class="feature-cards">
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-file-export"></i>
</div>
<h3 class="feature-title">多种格式支持</h3>
<p>支持Excel和CSV格式导入导出,方便与其他系统集成和交换数据。</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-image"></i>
</div>
<h3 class="feature-title">图片题目支持</h3>
<p>题目和选项均可添加图片,特别适合数学、化学等需要公式和图像的科目。</p>
</div>
<div class="feature-card">
<div class="feature-icon">
<i class="fas fa-edit"></i>
</div>
<h3 class="feature-title">解答题增强</h3>
<p>解答题支持完整答案和评分关键词(可选),适用于各种主观题评分场景。</p>
</div>
</div>
</div>
</div>
<div class="notification" id="notification">
<i class="fas fa-check-circle"></i>
<div class="notification-content">
<h3>操作成功</h3>
<p id="notification-message">题目已添加到题库</p>
</div>
</div>
<script>
// 全局变量
let questions = [];
let optionCounter = 0;
let blankCounter = 0;
let keywordCounter = 0;
let currentType = 'single'; // 默认改为单选题
let currentPoints = 3;
let truefalseAnswer = 'true';
let blankScoringMode = 'allOrNothing';
let essayScoringMode = 'partialCredit'; // 解答题评分规则(新增)
let dragSrcEl = null; // 当前拖动的元素
let dragStartIndex = -1; // 拖动起始索引
// DOM 元素
const questionInput = document.getElementById('question');
const essayAnswerInput = document.getElementById('essay-answer');
const explanationInput = document.getElementById('explanation');
const pointsInput = document.getElementById('question-points');
const optionsContainer = document.getElementById('options-container');
const optionsSection = document.getElementById('options-section');
const truefalseSection = document.getElementById('truefalse-section');
const blanksSection = document.getElementById('blanks-section');
const essaySection = document.getElementById('essay-section');
const blanksContainer = document.getElementById('blanks-container');
const keywordsContainer = document.getElementById('keywords-container');
const addOptionBtn = document.getElementById('add-option-btn');
const addBlankBtn = document.getElementById('add-blank-btn');
const addKeywordBtn = document.getElementById('add-keyword-btn');
const addQuestionBtn = document.getElementById('add-question-btn');
const previewBtn = document.getElementById('preview-btn');
const resetBtn = document.getElementById('reset-btn');
const exportExcelBtn = document.getElementById('export-excel-btn');
const exportCsvBtn = document.getElementById('export-csv-btn');
const clearAllBtn = document.getElementById('clear-all-btn');
const importBtn = document.getElementById('import-btn');
const importFile = document.getElementById('import-file');
const commonEncoding = document.getElementById('common-encoding');
const exportFilename = document.getElementById('export-filename');
const questionsList = document.getElementById('questions-list');
const emptyRow = document.getElementById('empty-row');
const previewSection = document.getElementById('preview-section');
const previewQuestionContainer = document.getElementById('preview-question-container');
const previewOptions = document.getElementById('preview-options');
const previewBlanks = document.getElementById('preview-blanks');
const previewKeywords = document.getElementById('preview-keywords');
const previewExplanation = document.getElementById('preview-explanation');
const explanationContent = document.getElementById('explanation-content');
const notification = document.getElementById('notification');
const totalCountEl = document.getElementById('total-count');
const singleCountEl = document.getElementById('single-count');
const multipleCountEl = document.getElementById('multiple-count');
const truefalseCountEl = document.getElementById('truefalse-count');
const fillblankCountEl = document.getElementById('fillblank-count');
const essayCountEl = document.getElementById('essay-count');
const essayCountEl2 = document.getElementById('essay-count2');
const totalPointsEl = document.getElementById('total-points');
const imageCountEl = document.getElementById('image-count');
const singleTypeBtn = document.getElementById('single-type');
const multipleTypeBtn = document.getElementById('multiple-type');
const truefalseTypeBtn = document.getElementById('truefalse-type');
const fillblankTypeBtn = document.getElementById('fillblank-type');
const essayTypeBtn = document.getElementById('essay-type');
const trueOption = document.getElementById('true-option');
const falseOption = document.getElementById('false-option');
const perBlankModeBtn = document.getElementById('per-blank-mode');
const allOrNothingModeBtn = document.getElementById('all-or-nothing-mode');
const partialCreditModeBtn = document.getElementById('partial-credit-mode'); // 新增
const allOrNothingEssayModeBtn = document.getElementById('all-or-nothing-essay-mode'); // 新增
const questionImageContainer = document.getElementById('question-image-container');
const questionImageUrl = document.getElementById('question-image-url');
const questionImagePreview = document.getElementById('question-image-preview');
const addQuestionImageBtn = document.getElementById('add-question-image-btn');
const removeQuestionImageBtn = document.getElementById('remove-question-image-btn');
const questionPreviewEl = document.getElementById('question-preview');
const showAnswersBtn = document.getElementById('show-answers-btn');
const insertBlankToolbar = document.getElementById('insert-blank-toolbar');
const pointsHint = document.getElementById('points-hint');
// 初始化
function init() {
// 添加初始选项(单选题需要两个选项)
addOption();
addOption();
// 更新预览
updateQuestionPreview();
// 事件监听
addOptionBtn.addEventListener('click', addOption);
addBlankBtn.addEventListener('click', addBlank);
addKeywordBtn.addEventListener('click', addKeyword);
addQuestionBtn.addEventListener('click', addQuestion);
previewBtn.addEventListener('click', togglePreview);
resetBtn.addEventListener('click', resetForm);
exportExcelBtn.addEventListener('click', exportExcel);
exportCsvBtn.addEventListener('click', exportCSV);
clearAllBtn.addEventListener('click', clearAllQuestions);
importBtn.addEventListener('click', () => importFile.click());
importFile.addEventListener('change', handleFileImport);
// 题目类型切换
singleTypeBtn.addEventListener('click', () => setQuestionType('single'));
multipleTypeBtn.addEventListener('click', () => setQuestionType('multiple'));
truefalseTypeBtn.addEventListener('click', () => setQuestionType('truefalse'));
fillblankTypeBtn.addEventListener('click', () => setQuestionType('fillblank'));
essayTypeBtn.addEventListener('click', () => setQuestionType('essay'));
// 判断题选项选择
trueOption.addEventListener('click', () => selectTrueFalseOption('true'));
falseOption.addEventListener('click', () => selectTrueFalseOption('false'));
// 填空题评分规则选择
perBlankModeBtn.addEventListener('click', () => setBlankScoringMode('perBlank'));
allOrNothingModeBtn.addEventListener('click', () => setBlankScoringMode('allOrNothing'));
// 解答题评分规则选择(新增)
partialCreditModeBtn.addEventListener('click', () => setEssayScoringMode('partialCredit'));
allOrNothingEssayModeBtn.addEventListener('click', () => setEssayScoringMode('allOrNothing'));
// 分数输入监听
pointsInput.addEventListener('change', function() {
let value = parseInt(this.value);
if (isNaN(value)) value = 1;
if (value < 1) value = 1;
// 解答题没有上限,其他题型上限为10分
if (currentType !== 'essay' && value > 10) {
value = 10;
}
this.value = value;
currentPoints = value;
});
// 题目图片URL输入监听
questionImageUrl.addEventListener('input', function() {
const url = this.value.trim();
if (url) {
questionImagePreview.innerHTML = `<img src="${url}" alt="题目图片预览">`;
questionImagePreview.style.display = 'block';
} else {
questionImagePreview.style.display = 'none';
}
});
// 添加题目图片按钮
addQuestionImageBtn.addEventListener('click', function() {
questionImageContainer.style.display = 'block';
this.style.display = 'none';
});
// 移除题目图片按钮
removeQuestionImageBtn.addEventListener('click', function() {
questionImageContainer.style.display = 'none';
questionImageUrl.value = '';
questionImagePreview.style.display = 'none';
addQuestionImageBtn.style.display = 'inline-flex';
});
// 题目文本输入监听
questionInput.addEventListener('input', updateQuestionPreview);
// 插入填空项按钮
document.querySelectorAll('.insert-blank-btn').forEach(btn => {
btn.addEventListener('click', function() {
const blankNum = this.id.split('-')[2];
insertBlank(blankNum);
});
});
// 显示答案按钮
showAnswersBtn.addEventListener('click', toggleAnswers);
// 初始渲染题目列表
renderQuestionList();
// 初始化判断题选项
selectTrueFalseOption(truefalseAnswer);
}
// 在题目中插入填空项标记
function insertBlank(blankNum) {
const textarea = questionInput;
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
const text = textarea.value;
// 创建填空标记
const blankMark = `{${blankNum}}`;
// 插入填空标记
textarea.value = text.substring(0, startPos) + blankMark + text.substring(endPos);
// 设置光标位置
textarea.selectionStart = startPos + blankMark.length;
textarea.selectionEnd = startPos + blankMark.length;
// 更新预览
updateQuestionPreview();
// 聚焦到文本框
textarea.focus();
}
// 更新题目预览
function updateQuestionPreview() {
const text = questionInput.value;
// 替换填空标记为带序号的占位符(显示为"1、________")
const previewText = text.replace(/\{(\d+)\}/g, (match, p1) => {
return `<span class="blank-placeholder">${p1}、${'________'.repeat(2)}</span>`;
});
questionPreviewEl.innerHTML = previewText;
}
// 设置题目类型
function setQuestionType(type) {
currentType = type;
// 更新UI
singleTypeBtn.classList.toggle('selected', type === 'single');
multipleTypeBtn.classList.toggle('selected', type === 'multiple');
truefalseTypeBtn.classList.toggle('selected', type === 'truefalse');
fillblankTypeBtn.classList.toggle('selected', type === 'fillblank');
essayTypeBtn.classList.toggle('selected', type === 'essay');
// 根据题目类型显示不同的选项区域
if (type === 'truefalse') {
optionsSection.style.display = 'none';
truefalseSection.style.display = 'block';
blanksSection.style.display = 'none';
essaySection.style.display = 'none';
insertBlankToolbar.style.display = 'none';
// 初始化判断题选项
selectTrueFalseOption(truefalseAnswer);
} else if (type === 'fillblank') {
optionsSection.style.display = 'none';
truefalseSection.style.display = 'none';
blanksSection.style.display = 'block';
essaySection.style.display = 'none';
insertBlankToolbar.style.display = 'flex';
} else if (type === 'essay') {
optionsSection.style.display = 'none';
truefalseSection.style.display = 'none';
blanksSection.style.display = 'none';
essaySection.style.display = 'block';
insertBlankToolbar.style.display = 'none';
} else {
optionsSection.style.display = 'block';
truefalseSection.style.display = 'none';
blanksSection.style.display = 'none';
essaySection.style.display = 'none';
insertBlankToolbar.style.display = 'none';
}
// 如果是单选题,确保只有一个正确答案
if (type === 'single') {
let foundCorrect = false;
document.querySelectorAll('.option-row input[type="checkbox"]').forEach(checkbox => {
if (foundCorrect) {
checkbox.checked = false;
} else if (checkbox.checked) {
foundCorrect = true;
}
});
}
// 更新分数输入框限制
if (type === 'essay') {
pointsInput.removeAttribute('max');
pointsHint.textContent = '(1分以上)';
pointsInput.parentElement.classList.add('essay-points');
} else {
pointsInput.setAttribute('max', '10');
pointsHint.textContent = '(1-10分)';
pointsInput.parentElement.classList.remove('essay-points');
// 如果当前分数大于10,修正为10
if (parseInt(pointsInput.value) > 10) {
pointsInput.value = 10;
currentPoints = 10;
}
}
}
// 设置填空题评分规则
function setBlankScoringMode(mode) {
blankScoringMode = mode;
// 更新UI
perBlankModeBtn.classList.toggle('selected', mode === 'perBlank');
allOrNothingModeBtn.classList.toggle('selected', mode === 'allOrNothing');
// 显示/隐藏每个填空项的分值输入框
document.querySelectorAll('.blank-points').forEach(input => {
input.style.display = mode === 'perBlank' ? 'block' : 'none';
});
}
// 设置解答题评分规则(新增)
function setEssayScoringMode(mode) {
essayScoringMode = mode;
// 更新UI
partialCreditModeBtn.classList.toggle('selected', mode === 'partialCredit');
allOrNothingEssayModeBtn.classList.toggle('selected', mode === 'allOrNothing');
}
// 选择判断题答案
function selectTrueFalseOption(value) {
truefalseAnswer = value;
// 更新UI
trueOption.classList.toggle('selected', value === 'true');
falseOption.classList.toggle('selected', value === 'false');
}
// 添加新选项
function addOption() {
if (optionCounter >= 6) {
showNotification('最多只能添加6个选项', 'error');
return;
}
const optionId = optionCounter;
const optionRow = document.createElement('div');
optionRow.className = 'option-row';
optionRow.innerHTML = `
<div class="option-input">
<input type="text" id="option-${optionId}" placeholder="选项内容(可选)">
<div class="add-image-btn add-option-image-btn" data-id="${optionId}">
<i class="fas fa-image"></i> 添加选项图片
</div>
<div class="option-image-container" id="option-image-container-${optionId}" style="display: none;">
<label class="option-image-label">
<i class="fas fa-link"></i> 选项图片URL:
</label>
<input type="text" id="option-image-url-${optionId}" class="image-input" placeholder="输入图片URL">
<div class="option-image-preview" id="option-image-preview-${optionId}"></div>
</div>
</div>
<div class="option-actions">
<label class="option-checkbox">
<input type="checkbox" id="correct-${optionId}"> 正确答案
</label>
<button class="action-btn delete-option" data-id="${optionId}">
<i class="fas fa-trash"></i>
</button>
</div>
`;
optionsContainer.appendChild(optionRow);
// 添加事件监听
const deleteBtn = optionRow.querySelector('.delete-option');
deleteBtn.addEventListener('click', function() {
if (optionCounter <= 2) {
showNotification('至少需要两个选项', 'error');
return;
}
optionRow.remove();
optionCounter--;
});
// 添加选项图片按钮事件
const addOptionImageBtn = optionRow.querySelector('.add-option-image-btn');
addOptionImageBtn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
const container = document.getElementById(`option-image-container-${id}`);
container.style.display = 'block';
this.style.display = 'none';
});
// 选项图片URL输入监听
const optionImageUrl = document.getElementById(`option-image-url-${optionId}`);
const optionImagePreview = document.getElementById(`option-image-preview-${optionId}`);
optionImageUrl.addEventListener('input', function() {
const url = this.value.trim();
if (url) {
optionImagePreview.innerHTML = `<img src="${url}" alt="选项图片预览">`;
optionImagePreview.style.display = 'block';
} else {
optionImagePreview.style.display = 'none';
}
});
optionCounter++;
}
// 添加新填空项
function addBlank() {
if (blankCounter >= 6) {
showNotification('最多只能添加6个填空项', 'error');
return;
}
const blankId = blankCounter;
const blankRow = document.createElement('div');
blankRow.className = 'blank-row';
blankRow.innerHTML = `
<div class="blank-number">${blankCounter + 1}</div>
<div class="blank-input">
<input type="text" id="blank-${blankId}" placeholder="填空项正确答案">
</div>
<input type="number" id="blank-points-${blankId}" class="blank-points" min="1" value="1" ${blankScoringMode === 'allOrNothing' ? 'style="display: none;"' : ''}>
<button class="action-btn delete-blank" data-id="${blankId}">
<i class="fas fa-trash"></i>
</button>
`;
blanksContainer.appendChild(blankRow);
// 添加事件监听
const deleteBtn = blankRow.querySelector('.delete-blank');
deleteBtn.addEventListener('click', function() {
if (blankCounter <= 1) {
showNotification('至少需要一个填空项', 'error');
return;
}
blankRow.remove();
blankCounter--;
// 更新编号
document.querySelectorAll('.blank-row').forEach((row, index) => {
row.querySelector('.blank-number').textContent = index + 1;
});
});
// 分数输入监听 - 移除上限限制
const pointsInput = document.getElementById(`blank-points-${blankId}`);
pointsInput.addEventListener('change', function() {
let value = parseInt(this.value);
if (isNaN(value)) value = 1;
if (value < 1) value = 1;
this.value = value;
});
blankCounter++;
}
// 添加评分关键词
function addKeyword() {
if (keywordCounter >= 10) {
showNotification('最多只能添加10个关键词', 'error');
return;
}
const keywordId = keywordCounter;
const keywordRow = document.createElement('div');
keywordRow.className = 'keyword-row';
keywordRow.innerHTML = `
<div class="keyword-number">${keywordCounter + 1}</div>
<div class="keyword-input">
<input type="text" id="keyword-${keywordId}" placeholder="评分关键词">
</div>
<input type="number" id="keyword-points-${keywordId}" class="keyword-points" min="1" max="10" value="1">
<button class="action-btn delete-keyword" data-id="${keywordId}">
<i class="fas fa-trash"></i>
</button>
`;
keywordsContainer.appendChild(keywordRow);
// 添加事件监听
const deleteBtn = keywordRow.querySelector('.delete-keyword');
deleteBtn.addEventListener('click', function() {
keywordRow.remove();
keywordCounter--;
// 更新编号
document.querySelectorAll('.keyword-row').forEach((row, index) => {
row.querySelector('.keyword-number').textContent = index + 1;
});
});
// 分数输入监听
const pointsInput = document.getElementById(`keyword-points-${keywordId}`);
pointsInput.addEventListener('change', function() {
let value = parseInt(this.value);
if (isNaN(value)) value = 1;
if (value < 1) value = 1;
if (value > 10) value = 10;
this.value = value;
});
keywordCounter++;
}
// 添加新题目
function addQuestion() {
const questionText = questionInput.value.trim();
const essayAnswer = essayAnswerInput.value.trim();
const explanation = explanationInput.value.trim();
const questionImage = questionImageUrl.value.trim();
// 验证问题
if (!questionText) {
showNotification('请输入问题内容', 'error');
return;
}
// 收集选项
const options = [];
let correctAnswers = [];
let hasOptions = false;
let blanks = [];
let keywords = [];
if (currentType === 'truefalse') {
// 判断题特殊处理
options.push({
text: '正确',
});
options.push({
text: '错误',
});
correctAnswers = [truefalseAnswer === 'true' ? 'A' : 'B'];
} else if (currentType === 'fillblank') {
// 填空题处理
document.querySelectorAll('.blank-row').forEach((row, index) => {
const blankInput = row.querySelector('input[type="text"]');
const blankValue = blankInput ? blankInput.value.trim() : '';
const pointsInput = row.querySelector('input[type="number"]');
const points = pointsInput ? parseInt(pointsInput.value) : 0;
if (blankValue) {
hasOptions = true;
blanks.push({
answer: blankValue,
points: points
});
}
});
if (blanks.length === 0) {
showNotification('请至少输入一个填空项', 'error');
return;
}
} else if (currentType === 'essay') {
// 解答题处理
document.querySelectorAll('.keyword-row').forEach((row, index) => {
const keywordInput = row.querySelector('input[type="text"]');
const keywordValue = keywordInput ? keywordInput.value.trim() : '';
const pointsInput = row.querySelector('input[type="number"]');
const points = pointsInput ? parseInt(pointsInput.value) : 0;
if (keywordValue) {
hasOptions = true;
keywords.push({
keyword: keywordValue,
points: points
});
}
});
// 解答题完整答案是必填项
if (!essayAnswer) {
showNotification('请输入完整答案', 'error');
return;
}
} else {
// 遍历所有选项行
document.querySelectorAll('.option-row').forEach((row, index) => {
const optionInput = row.querySelector('input[type="text"]');
const optionValue = optionInput ? optionInput.value.trim() : '';
const correctCheckbox = row.querySelector('input[type="checkbox"]');
const optionImageUrl = row.querySelector('.option-image-container input[type="text"]');
const optionImage = optionImageUrl ? optionImageUrl.value.trim() : '';
if (optionValue || optionImage) {
hasOptions = true;
options.push({
text: optionValue,
image: optionImage
});
if (correctCheckbox && correctCheckbox.checked) {
// 正确答案使用字母表示 (A, B, C...)
correctAnswers.push(String.fromCharCode(65 + index));
}
}
});
if (!hasOptions) {
showNotification('请至少输入一个选项或添加选项图片', 'error');
return;
}
if (correctAnswers.length === 0) {
showNotification('请至少选择一个正确答案', 'error');
return;
}
// 如果是单选题,但选择了多个答案
if (currentType === 'single' && correctAnswers.length > 1) {
showNotification('单选题只能选择一个正确答案', 'error');
return;
}
}
// 创建题目对象
const question = {
id: Date.now(), // 唯一ID
text: questionText,
image: questionImage,
explanation: explanation,
type: currentType,
points: currentPoints,
options: options,
correct: correctAnswers,
blanks: blanks,
essayAnswer: essayAnswer,
keywords: keywords,
blankScoringMode: blankScoringMode,
essayScoringMode: essayScoringMode // 新增解答题评分规则
};
// 添加到题库
questions.push(question);
// 更新UI
renderQuestionList();
resetForm();
showNotification('题目已添加到题库');
// 启用按钮
exportExcelBtn.disabled = false;
exportCsvBtn.disabled = false;
clearAllBtn.disabled = false;
}
// 渲染题目列表
function renderQuestionList() {
// 隐藏空状态
if (emptyRow) emptyRow.style.display = questions.length ? 'none' : '';
// 清空列表
questionsList.innerHTML = '';
if (questions.length === 0) {
questionsList.appendChild(emptyRow);
emptyRow.style.display = '';
return;
}
// 添加题目
let singleCount = 0;
let multipleCount = 0;
let truefalseCount = 0;
let fillblankCount = 0;
let essayCount = 0;
let imageCount = 0;
let totalPoints = 0;
questions.forEach((q, index) => {
// 统计题目类型
if (q.type === 'single') singleCount++;
else if (q.type === 'multiple') multipleCount++;
else if (q.type === 'truefalse') truefalseCount++;
else if (q.type === 'fillblank') fillblankCount++;
else if (q.type === 'essay') essayCount++;
// 统计题目图片
if (q.image) imageCount++;
// 统计总分
totalPoints += q.points;
const row = document.createElement('tr');
row.setAttribute('data-id', q.id);
row.setAttribute('data-index', index);
row.draggable = true; // 启用拖拽
if (index === questions.length - 1) {
row.classList.add('highlight');
}
// 生成选项预览
let optionsHtml = '';
if (q.type === 'fillblank') {
// 填空题预览
optionsHtml = `<div class="options-preview">`;
q.blanks.forEach((blank, idx) => {
optionsHtml += `
<div class="option-tag">
${idx+1}. ${blank.answer}
</div>
`;
});
optionsHtml += `</div>`;
} else if (q.type === 'truefalse') {
// 判断题特殊显示
q.options.forEach((opt, optIndex) => {
const isCorrect = q.correct.includes(String.fromCharCode(65 + optIndex));
optionsHtml += `
<div class="option-tag ${isCorrect ? 'correct' : ''}">
${opt.text} ${isCorrect ? '<i class="fas fa-check"></i>' : ''}
</div>
`;
});
} else if (q.type === 'essay') {
// 解答题关键词预览
if (q.keywords && q.keywords.length > 0) {
optionsHtml = `<div class="options-preview">`;
q.keywords.forEach((kw, idx) => {
optionsHtml += `
<div class="option-tag">
${kw.keyword} (${kw.points}分)
</div>
`;
});
optionsHtml += `</div>`;
} else {
optionsHtml = `<div class="option-tag">无关键词</div>`;
}
} else {
// 选择题预览
q.options.forEach((opt, optIndex) => {
const isCorrect = q.correct.includes(String.fromCharCode(65 + optIndex));
let optionContent = `
<div class="option-tag ${isCorrect ? 'correct' : ''}">
`;
if (opt.image) {
optionContent += `
<div class="image-option-preview-item">
<img src="${opt.image}" alt="选项图片">
<div class="image-option-preview-label">${opt.text || '图片选项'}</div>
</div>
`;
} else {
optionContent += `
${String.fromCharCode(65 + optIndex)}: ${opt.text || ''}
${isCorrect ? '<i class="fas fa-check"></i>' : ''}
`;
}
optionContent += `</div>`;
optionsHtml += optionContent;
});
}
// 题目类型显示
let typeBadge = '';
if (q.type === 'single') {
typeBadge = '<span class="badge badge-single">单选题</span>';
} else if (q.type === 'multiple') {
typeBadge = '<span class="badge badge-multiple">多选题</span>';
} else if (q.type === 'truefalse') {
typeBadge = '<span class="badge badge-truefalse">判断题</span>';
} else if (q.type === 'fillblank') {
typeBadge = '<span class="badge badge-fillblank">填空题</span>';
} else if (q.type === 'essay') {
typeBadge = '<span class="badge badge-essay">解答题</span>';
}
// 问题内容显示(含图片)
let questionContent = '';
if (q.type === 'fillblank') {
// 对于填空题,替换填空标记为占位符(显示为"1、________")
const previewText = q.text.replace(/\{(\d+)\}/g, (match, p1) => {
return `<span class="blank-placeholder">${p1}、${'________'.repeat(2)}</span>`;
});
questionContent = `<div>${previewText}</div>`;
} else {
questionContent = `<div class="question-text">${q.text}</div>`;
}
if (q.image) {
questionContent = `
<div class="question-text-with-image">
${questionContent}
<img src="${q.image}" alt="题目图片" style="max-width: 100px; max-height: 60px;">
</div>
`;
}
row.innerHTML = `
<td class="drag-handle" title="拖动排序"><i class="fas fa-grip-vertical"></i></td>
<td>${index + 1}</td>
<td class="question-text">${questionContent}</td>
<td>
${optionsHtml}
</td>
<td>
${typeBadge}
</td>
<td>
<span class="badge badge-points ${q.type === 'essay' ? 'essay-points' : ''}">${q.points}分</span>
</td>
<td>
<div class="list-actions">
<button class="action-btn edit-btn" data-id="${q.id}">
<i class="fas fa-edit"></i>
</button>
<button class="action-btn delete-btn" data-id="${q.id}">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
questionsList.appendChild(row);
});
// 添加事件监听
document.querySelectorAll('.edit-btn').forEach(btn => {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
editQuestion(id);
});
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', function() {
const id = this.getAttribute('data-id');
deleteQuestion(id);
});
});
// 添加拖拽事件监听
setupDragAndDrop();
// 更新计数
totalCountEl.textContent = questions.length;
singleCountEl.textContent = singleCount;
multipleCountEl.textContent = multipleCount;
truefalseCountEl.textContent = truefalseCount;
fillblankCountEl.textContent = fillblankCount;
essayCountEl.textContent = essayCount;
essayCountEl2.textContent = essayCount;
totalPointsEl.textContent = totalPoints;
imageCountEl.textContent = imageCount;
}
// 设置拖拽排序功能
function setupDragAndDrop() {
const rows = document.querySelectorAll('#questions-list tr:not(.empty-state)');
rows.forEach(row => {
row.addEventListener('dragstart', handleDragStart);
row.addEventListener('dragover', handleDragOver);
row.addEventListener('dragenter', handleDragEnter);
row.addEventListener('dragleave', handleDragLeave);
row.addEventListener('drop', handleDrop);
row.addEventListener('dragend', handleDragEnd);
});
}
// 拖拽开始
function handleDragStart(e) {
dragSrcEl = this;
dragStartIndex = parseInt(this.getAttribute('data-index'));
this.classList.add('dragging');
// 设置拖拽效果
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.outerHTML);
}
// 拖拽经过
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault(); // 允许放置
}
e.dataTransfer.dropEffect = 'move';
return false;
}
// 拖拽进入
function handleDragEnter(e) {
this.classList.add('drag-over');
}
// 拖拽离开
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
// 放置
function handleDrop(e) {
e.stopPropagation(); // 阻止冒泡
e.preventDefault();
if (dragSrcEl !== this) {
const dragEndIndex = parseInt(this.getAttribute('data-index'));
// 交换数组中的元素
[questions[dragStartIndex], questions[dragEndIndex]] =
[questions[dragEndIndex], questions[dragStartIndex]];
// 重新渲染题目列表
renderQuestionList();
}
return false;
}
// 拖拽结束
function handleDragEnd(e) {
// 移除所有行的拖拽样式
document.querySelectorAll('#questions-list tr').forEach(row => {
row.classList.remove('dragging');
row.classList.remove('drag-over');
});
}
// 编辑题目
function editQuestion(id) {
const question = questions.find(q => q.id == id);
if (!question) return;
// 填充表单
questionInput.value = question.text;
essayAnswerInput.value = question.essayAnswer || '';
explanationInput.value = question.explanation || '';
pointsInput.value = question.points;
currentPoints = question.points;
// 设置题目图片
if (question.image) {
questionImageUrl.value = question.image;
questionImagePreview.innerHTML = `<img src="${question.image}" alt="题目图片预览">`;
questionImagePreview.style.display = 'block';
questionImageContainer.style.display = 'block';
addQuestionImageBtn.style.display = 'none';
}
// 设置题目类型
setQuestionType(question.type);
// 更新预览
updateQuestionPreview();
// 判断题特殊处理
if (question.type === 'truefalse') {
// 设置判断题答案
const answer = question.correct[0];
truefalseAnswer = answer === 'A' ? 'true' : 'false';
selectTrueFalseOption(truefalseAnswer);
} else if (question.type === 'fillblank') {
// 设置填空题
setBlankScoringMode(question.blankScoringMode || 'allOrNothing');
// 清空填空项
blanksContainer.innerHTML = '';
blankCounter = 0;
// 添加填空项
question.blanks.forEach(blank => {
addBlank();
const blankInput = document.getElementById(`blank-${blankCounter - 1}`);
const pointsInput = document.getElementById(`blank-points-${blankCounter - 1}`);
if (blankInput) {
blankInput.value = blank.answer || '';
if (pointsInput) {
pointsInput.value = blank.points || 1;
}
}
});
} else if (question.type === 'essay') {
// 设置解答题
setEssayScoringMode(question.essayScoringMode || 'partialCredit');
// 清空关键词
keywordsContainer.innerHTML = '';
keywordCounter = 0;
// 添加关键词
question.keywords.forEach(kw => {
addKeyword();
const keywordInput = document.getElementById(`keyword-${keywordCounter - 1}`);
const pointsInput = document.getElementById(`keyword-points-${keywordCounter - 1}`);
if (keywordInput) {
keywordInput.value = kw.keyword || '';
if (pointsInput) {
pointsInput.value = kw.points || 1;
}
}
});
} else {
// 清空选项
optionsContainer.innerHTML = '';
optionCounter = 0;
// 添加选项
question.options.forEach((opt, index) => {
addOption();
const optionInput = document.getElementById(`option-${optionCounter - 1}`);
const correctCheckbox = document.getElementById(`correct-${optionCounter - 1}`);
const optionImageInput = document.getElementById(`option-image-url-${optionCounter - 1}`);
const optionImagePreview = document.getElementById(`option-image-preview-${optionCounter - 1}`);
if (optionInput) {
optionInput.value = opt.text || '';
// 设置选项图片
if (opt.image) {
optionImageInput.value = opt.image;
optionImagePreview.innerHTML = `<img src="${opt.image}" alt="选项图片预览">`;
optionImagePreview.style.display = 'block';
document.getElementById(`option-image-container-${optionCounter - 1}`).style.display = 'block';
document.querySelector(`[data-id="${optionCounter - 1}"].add-option-image-btn`).style.display = 'none';
}
// 设置正确答案
const isCorrect = question.correct.includes(String.fromCharCode(65 + index));
if (correctCheckbox) {
correctCheckbox.checked = isCorrect;
}
}
});
}
// 删除原始题目
deleteQuestion(id);
// 滚动到顶部
window.scrollTo(0, 0);
}
// 删除题目
function deleteQuestion(id) {
questions = questions.filter(q => q.id != id);
renderQuestionList();
if (questions.length === 0) {
if (emptyRow) emptyRow.style.display = '';
exportExcelBtn.disabled = true;
exportCsvBtn.disabled = true;
clearAllBtn.disabled = true;
}
}
// 重置表单
function resetForm() {
questionInput.value = '';
essayAnswerInput.value = '';
explanationInput.value = '';
optionsContainer.innerHTML = '';
blanksContainer.innerHTML = '';
keywordsContainer.innerHTML = '';
optionCounter = 0;
blankCounter = 0;
keywordCounter = 0;
previewSection.style.display = 'none';
currentPoints = 1;
pointsInput.value = 1;
setQuestionType('single');
truefalseAnswer = 'true';
selectTrueFalseOption('true');
setBlankScoringMode('allOrNothing');
setEssayScoringMode('partialCredit'); // 重置为按关键词给分
// 重置题目图片
questionImageUrl.value = '';
questionImagePreview.style.display = 'none';
questionImageContainer.style.display = 'none';
addQuestionImageBtn.style.display = 'inline-flex';
// 更新预览
updateQuestionPreview();
// 重新添加两个选项(单选题)
addOption();
addOption();
}
// 切换预览
function togglePreview() {
const questionText = questionInput.value.trim();
const essayAnswer = essayAnswerInput.value.trim();
const explanation = explanationInput.value.trim();
const questionImage = questionImageUrl.value.trim();
if (!questionText) {
showNotification('请输入问题内容', 'error');
return;
}
// 收集选项
const options = [];
let correctAnswers = [];
let blanks = [];
let keywords = [];
if (currentType === 'truefalse') {
// 判断题特殊处理
options.push({
text: '正确',
});
options.push({
text: '错误',
});
correctAnswers = [truefalseAnswer === 'true' ? 'A' : 'B'];
} else if (currentType === 'fillblank') {
// 填空题处理
document.querySelectorAll('.blank-row').forEach((row, index) => {
const blankInput = row.querySelector('input[type="text"]');
const blankValue = blankInput ? blankInput.value.trim() : '';
const pointsInput = row.querySelector('input[type="number"]');
const points = pointsInput ? parseInt(pointsInput.value) : 0;
if (blankValue) {
blanks.push({
answer: blankValue,
points: points,
index: index + 1
});
}
});
if (blanks.length === 0) {
showNotification('请至少输入一个填空项', 'error');
return;
}
} else if (currentType === 'essay') {
// 解答题处理
document.querySelectorAll('.keyword-row').forEach((row, index) => {
const keywordInput = row.querySelector('input[type="text"]');
const keywordValue = keywordInput ? keywordInput.value.trim() : '';
const pointsInput = row.querySelector('input[type="number"]');
const points = pointsInput ? parseInt(pointsInput.value) : 0;
if (keywordValue) {
keywords.push({
keyword: keywordValue,
points: points,
index: index + 1
});
}
});
if (!essayAnswer) {
showNotification('请输入完整答案', 'error');
return;
}
} else {
let hasOptions = false;
// 遍历所有选项行
document.querySelectorAll('.option-row').forEach((row, index) => {
const optionInput = row.querySelector('input[type="text"]');
const optionValue = optionInput ? optionInput.value.trim() : '';
const correctCheckbox = row.querySelector('input[type="checkbox"]');
const optionImageUrl = row.querySelector('.option-image-container input[type="text"]');
const optionImage = optionImageUrl ? optionImageUrl.value.trim() : '';
if (optionValue || optionImage) {
hasOptions = true;
options.push({
text: optionValue,
image: optionImage
});
if (correctCheckbox && correctCheckbox.checked) {
correctAnswers.push(index);
}
}
});
if (!hasOptions) {
showNotification('请至少输入一个选项或添加选项图片', 'error');
return;
}
if (correctAnswers.length === 0) {
showNotification('请至少选择一个正确答案', 'error');
return;
}
// 如果是单选题,但选择了多个答案
if (currentType === 'single' && correctAnswers.length > 1) {
showNotification('单选题只能选择一个正确答案', 'error');
return;
}
}
// 更新预览
previewQuestionContainer.innerHTML = '';
previewBlanks.innerHTML = '';
previewKeywords.innerHTML = '';
previewExplanation.style.display = 'none';
showAnswersBtn.style.display = 'none';
// 预览题目类型和分数
const typeInfo = document.createElement('div');
typeInfo.style.marginBottom = '15px';
typeInfo.style.display = 'flex';
typeInfo.style.gap = '15px';
let typeBadge = '';
if (currentType === 'single') {
typeBadge = '<span class="badge badge-single">单选题</span>';
} else if (currentType === 'multiple') {
typeBadge = '<span class="badge badge-multiple">多选题</span>';
} else if (currentType === 'truefalse') {
typeBadge = '<span class="badge badge-truefalse">判断题</span>';
} else if (currentType === 'fillblank') {
typeBadge = '<span class="badge badge-fillblank">填空题</span>';
} else if (currentType === 'essay') {
typeBadge = '<span class="badge badge-essay">解答题</span>';
}
let scoringInfo = '';
if (currentType === 'fillblank') {
scoringInfo = blankScoringMode === 'perBlank'
? '<span class="badge" style="background: rgba(46, 204, 113, 0.15); color: #2ecc71;">按空给分</span>'
: '<span class="badge" style="background: rgba(231, 76, 60, 0.15); color: #e74c3c;">全对给分</span>';
} else if (currentType === 'essay') {
scoringInfo = essayScoringMode === 'partialCredit'
? '<span class="badge" style="background: rgba(46, 204, 113, 0.15); color: #2ecc71;">按关键词给分</span>'
: '<span class="badge" style="background: rgba(231, 76, 60, 0.15); color: #e74c3c;">全对给分</span>';
}
typeInfo.innerHTML = `
${typeBadge}
${scoringInfo}
<span class="badge badge-points ${currentType === 'essay' ? 'essay-points' : ''}">${currentPoints}分</span>
`;
// 预览问题
const questionPreview = document.createElement('div');
questionPreview.className = 'preview-question';
if (currentType === 'fillblank') {
// 对于填空题,替换填空标记为占位符(显示为"1、________")
const previewText = questionText.replace(/\{(\d+)\}/g, (match, p1) => {
return `<span class="blank-placeholder">${p1}、${'________'.repeat(2)}</span>`;
});
questionPreview.innerHTML = previewText;
} else {
questionPreview.textContent = questionText;
}
previewQuestionContainer.appendChild(typeInfo);
previewQuestionContainer.appendChild(questionPreview);
// 预览题目图片
if (questionImage) {
const imagePreview = document.createElement('img');
imagePreview.src = questionImage;
imagePreview.alt = '题目图片预览';
imagePreview.className = 'preview-image';
previewQuestionContainer.appendChild(imagePreview);
}
// 预览选项
if (currentType === 'fillblank') {
previewBlanks.style.display = 'block';
showAnswersBtn.style.display = 'block';
showAnswersBtn.innerHTML = '<i class="fas fa-eye"></i> 显示答案';
blanks.forEach((blank, index) => {
const blankEl = document.createElement('div');
blankEl.className = 'preview-blank';
blankEl.innerHTML = `
<div class="preview-blank-header">
<div class="preview-blank-number">${index + 1}</div>
<div>填空项 ${index + 1}</div>
</div>
<div class="preview-blank-content">
<div>正确答案: <span class="blank-answer">${blank.answer}</span></div>
${blankScoringMode === 'perBlank' ? `<div>分值: ${blank.points}分</div>` : ''}
</div>
`;
previewBlanks.appendChild(blankEl);
});
} else if (currentType === 'essay') {
// 解答题预览
previewKeywords.style.display = 'block';
// 预览完整答案
const answerPreview = document.createElement('div');
answerPreview.className = 'preview-card';
answerPreview.innerHTML = `
<div class="preview-title">
<i class="fas fa-file-alt"></i> 完整答案
</div>
<div style="margin-top: 15px; padding: 15px; background: #fffaf0; border-radius: 8px; border: 1px solid #ffeeba;">
${essayAnswer.replace(/\n/g, '<br>')}
</div>
`;
previewQuestionContainer.appendChild(answerPreview);
// 预览评分关键词
if (keywords.length > 0) {
const keywordsPreview = document.createElement('div');
keywordsPreview.className = 'preview-card';
keywordsPreview.innerHTML = `
<div class="preview-title">
<i class="fas fa-key"></i> 评分关键词
</div>
<div id="keywords-preview-list" style="margin-top: 15px;"></div>
`;
previewQuestionContainer.appendChild(keywordsPreview);
const keywordsList = keywordsPreview.querySelector('#keywords-preview-list');
keywords.forEach((kw, index) => {
const keywordEl = document.createElement('div');
keywordEl.className = 'preview-keyword';
keywordEl.innerHTML = `
<div class="preview-keyword-header">
<div class="preview-keyword-number">${index + 1}</div>
<div>关键词 ${index + 1}</div>
</div>
<div class="preview-keyword-content">
<div>关键词: <span class="keyword-answer">${kw.keyword}</span></div>
<div>分值: ${kw.points}分</div>
</div>
`;
keywordsList.appendChild(keywordEl);
});
}
} else {
previewBlanks.style.display = 'none';
const optionsContainer = document.createElement('div');
optionsContainer.className = 'preview-options';
previewQuestionContainer.appendChild(optionsContainer);
options.forEach((opt, index) => {
const isCorrect = correctAnswers.includes(index);
if (currentType === 'truefalse') {
// 判断题特殊显示
const optionEl = document.createElement('div');
optionEl.className = `preview-option ${isCorrect ? 'correct' : ''}`;
optionEl.innerHTML = `
<div class="option-letter">${index === 0 ? '√' : '×'}</div>
<div>${opt.text}</div>
`;
optionsContainer.appendChild(optionEl);
} else {
// 图片选项显示
const optionEl = document.createElement('div');
optionEl.className = `image-option ${isCorrect ? 'correct' : ''}`;
let content = `
<div class="option-letter">${String.fromCharCode(65 + index)}</div>
`;
if (opt.image) {
content += `
<img src="${opt.image}" alt="选项图片" class="option-image">
`;
}
if (opt.text) {
content += `
<div class="option-image-label">${opt.text}</div>
`;
}
optionEl.innerHTML = content;
optionsContainer.appendChild(optionEl);
}
});
}
// 预览答案解析
if (explanation) {
explanationContent.textContent = explanation;
previewExplanation.style.display = 'block';
}
previewSection.style.display = 'block';
}
// 切换答案显示
function toggleAnswers() {
const questionText = questionInput.value.trim();
const blanks = [];
// 收集填空项
document.querySelectorAll('.blank-row').forEach((row, index) => {
const blankInput = row.querySelector('input[type="text"]');
const blankValue = blankInput ? blankInput.value.trim() : '';
if (blankValue) {
blanks.push({
answer: blankValue,
index: index + 1
});
}
});
const questionPreview = previewQuestionContainer.querySelector('.preview-question');
if (showAnswersBtn.textContent.includes('显示')) {
// 显示答案
const previewText = questionText.replace(/\{(\d+)\}/g, (match, p1) => {
const index = parseInt(p1);
const blank = blanks.find(b => b.index === index);
if (blank) {
return `<span class="blank-answer-reveal">${blank.answer}</span>`;
}
return match;
});
questionPreview.innerHTML = previewText;
showAnswersBtn.innerHTML = '<i class="fas fa-eye-slash"></i> 隐藏答案';
} else {
// 隐藏答案
const previewText = questionText.replace(/\{(\d+)\}/g, (match, p1) => {
return `<span class="blank-placeholder">${p1}、${'________'.repeat(2)}</span>`;
});
questionPreview.innerHTML = previewText;
showAnswersBtn.innerHTML = '<i class="fas fa-eye"></i> 显示答案';
}
}
// 清空所有题目
function clearAllQuestions() {
if (!confirm('确定要清空所有题目吗?此操作不可恢复。')) return;
questions = [];
renderQuestionList();
if (emptyRow) emptyRow.style.display = '';
exportExcelBtn.disabled = true;
exportCsvBtn.disabled = true;
clearAllBtn.disabled = true;
showNotification('已清空题库');
}
// 导出Excel
function exportExcel() {
if (questions.length === 0) {
showNotification('题库为空,无法导出', 'error');
return;
}
// 创建工作簿
const wb = XLSX.utils.book_new();
wb.SheetNames.push("题库");
// 准备数据
const data = [
// 标题行
["问题", "问题图片", "选项A", "选项A图片", "选项B", "选项B图片", "选项C", "选项C图片", "选项D", "选项D图片", "选项E", "选项E图片", "选项F", "选项F图片", "答案", "题目类型", "题目分数", "答案解析", "填空项1", "填空项1分值", "填空项2", "填空项2分值", "填空项3", "填空项3分值", "填空项4", "填空项4分值", "填空项5", "填空项5分值", "填空项6", "填空项6分值", "完整答案", "关键词1", "关键词1分值", "关键词2", "关键词2分值", "关键词3", "关键词3分值", "关键词4", "关键词4分值", "关键词5", "关键词5分值", "评分规则"]
];
questions.forEach(q => {
// 添加问题
const row = [q.text || "", q.image || ""];
// 添加选项(最多6个)
for (let i = 0; i < 6; i++) {
if (i < q.options.length) {
// 选项文本
const text = q.options[i].text || "";
row.push(text);
// 选项图片
const image = q.options[i].image || "";
row.push(image);
} else {
// 空选项(文本和图片)
row.push("");
row.push("");
}
}
// 添加答案
row.push(q.correct.join(''));
// 添加题目类型
row.push(q.type);
// 添加题目分数
row.push(q.points);
// 添加答案解析
row.push(q.explanation || "");
// 添加填空项(最多6个)
for (let i = 0; i < 6; i++) {
if (i < (q.blanks ? q.blanks.length : 0)) {
// 填空项答案
const answer = q.blanks[i].answer || "";
row.push(answer);
// 填空项分值
const points = q.blanks[i].points || "";
row.push(points);
} else {
// 空填空项(答案和分值)
row.push("");
row.push("");
}
}
// 添加完整答案(解答题)
row.push(q.type === 'essay' ? q.essayAnswer || "" : "");
// 添加关键词(最多5个)
for (let i = 0; i < 5; i++) {
if (i < (q.keywords ? q.keywords.length : 0)) {
// 关键词
const keyword = q.keywords[i].keyword || "";
row.push(keyword);
// 关键词分值
const points = q.keywords[i].points || "";
row.push(points);
} else {
// 空关键词(关键词和分值)
row.push("");
row.push("");
}
}
// 添加评分规则(修改:解答题使用partialCredit)
const scoringMode = q.type === 'essay' ?
(q.essayScoringMode || 'partialCredit') :
(q.blankScoringMode || "");
row.push(scoringMode);
data.push(row);
});
// 创建工作表
const ws = XLSX.utils.aoa_to_sheet(data);
wb.Sheets["题库"] = ws;
// 获取文件名
let filename = exportFilename.value.trim();
if (!filename) {
filename = `题库_${new Date().toLocaleDateString()}`;
} else {
// 移除可能存在的.xlsx扩展名
if (filename.toLowerCase().endsWith('.xlsx')) {
filename = filename.substring(0, filename.length - 5);
}
}
filename += '.xlsx';
// 导出Excel文件
XLSX.writeFile(wb, filename);
showNotification(`Excel文件已导出: ${filename}`);
}
// 导出CSV
function exportCSV() {
if (questions.length === 0) {
showNotification('题库为空,无法导出', 'error');
return;
}
// 创建CSV内容
let csvContent = '问题,问题图片,选项A,选项A图片,选项B,选项B图片,选项C,选项C图片,选项D,选项D图片,选项E,选项E图片,选项F,选项F图片,答案,题目类型,题目分数,答案解析,填空项1,填空项1分值,填空项2,填空项2分值,填空项3,填空项3分值,填空项4,填空项4分值,填空项5,填空项5分值,填空项6,填空项6分值,完整答案,关键词1,关键词1分值,关键词2,关键词2分值,关键词3,关键词3分值,关键词4,关键词4分值,关键词5,关键词5分值,评分规则\n';
questions.forEach(q => {
// 添加问题
csvContent += `"${q.text.replace(/"/g, '""')}",`;
// 添加问题图片
csvContent += `"${q.image ? q.image.replace(/"/g, '""') : ''}",`;
// 添加选项(最多6个)
for (let i = 0; i < 6; i++) {
if (i < q.options.length) {
// 选项文本
const text = q.options[i].text || '';
csvContent += `"${text.replace(/"/g, '""')}",`;
// 选项图片
const image = q.options[i].image || '';
csvContent += `"${image.replace(/"/g, '""')}",`;
} else {
// 空选项(文本和图片)
csvContent += ',';
csvContent += ',';
}
}
// 添加答案
csvContent += `${q.correct.join('')},`;
// 添加题目类型
csvContent += `${q.type},`;
// 添加题目分数
csvContent += `${q.points},`;
// 添加答案解析
csvContent += `"${q.explanation ? q.explanation.replace(/"/g, '""') : ''}",`;
// 添加填空项(最多6个)
for (let i = 0; i < 6; i++) {
if (i < (q.blanks ? q.blanks.length : 0)) {
// 填空项答案
const answer = q.blanks[i].answer || '';
csvContent += `"${answer.replace(/"/g, '""')}",`;
// 填空项分值
const points = q.blanks[i].points || '';
csvContent += `${points},`;
} else {
// 空填空项(答案和分值)
csvContent += ',';
csvContent += ',';
}
}
// 添加完整答案(解答题)
csvContent += `"${q.type === 'essay' ? (q.essayAnswer || '').replace(/"/g, '""') : ''}",`;
// 添加关键词(最多5个)
for (let i = 0; i < 5; i++) {
if (i < (q.keywords ? q.keywords.length : 0)) {
// 关键词
const keyword = q.keywords[i].keyword || '';
csvContent += `"${keyword.replace(/"/g, '""')}",`;
// 关键词分值
const points = q.keywords[i].points || '';
csvContent += `${points},`;
} else {
// 空关键词(关键词和分值)
csvContent += ',';
csvContent += ',';
}
}
// 添加评分规则(修改:解答题使用partialCredit)
const scoringMode = q.type === 'essay' ?
(q.essayScoringMode || 'partialCredit') :
(q.blankScoringMode || '');
csvContent += `${scoringMode}\n`;
});
// 获取选择的编码格式
const encoding = commonEncoding.value;
// 获取文件名
let filename = exportFilename.value.trim();
if (!filename) {
filename = `题库_${new Date().toLocaleDateString()}`;
} else {
// 移除可能存在的.csv扩展名
if (filename.toLowerCase().endsWith('.csv')) {
filename = filename.substring(0, filename.length - 4);
}
}
filename += '.csv';
// 创建Blob对象
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showNotification(`CSV文件已导出: ${filename}`);
}
// 处理文件导入
function handleFileImport(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
try {
const data = event.target.result;
// 根据文件扩展名判断格式
if (file.name.toLowerCase().endsWith('.xlsx') || file.name.toLowerCase().endsWith('.xls')) {
// Excel文件处理
handleExcelImport(data);
} else if (file.name.toLowerCase().endsWith('.csv')) {
// CSV文件处理
handleCSVImport(data);
} else {
showNotification('不支持的文件格式', 'error');
}
} catch (error) {
console.error('导入失败:', error);
showNotification('导入失败,请检查文件格式', 'error');
}
};
reader.onerror = function() {
showNotification('文件读取失败', 'error');
};
// 读取文件
if (file.name.toLowerCase().endsWith('.xlsx') || file.name.toLowerCase().endsWith('.xls')) {
reader.readAsArrayBuffer(file);
} else {
reader.readAsText(file, 'UTF-8');
}
}
// 处理Excel导入
function handleExcelImport(data) {
try {
const workbook = XLSX.read(data, { type: 'array' });
// 获取第一个工作表
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
// 将工作表转换为JSON
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
// 检查标题行
if (jsonData.length === 0 || jsonData[0][0] !== "问题") {
showNotification('Excel格式不正确,缺少标题行', 'error');
return;
}
// 跳过标题行
const parsedQuestions = [];
for (let i = 1; i < jsonData.length; i++) {
const row = jsonData[i];
if (!row || row.length === 0) continue;
// 提取问题
const questionText = row[0] || "";
const questionImage = row[1] || "";
// 提取选项(最多6个)
const options = [];
for (let j = 0; j < 6; j++) {
const textIndex = 2 + j * 2;
const imageIndex = textIndex + 1;
if (textIndex < row.length && (row[textIndex] || row[imageIndex])) {
options.push({
text: row[textIndex] || "",
image: row[imageIndex] || ""
});
}
}
// 提取答案
const answers = (row[14] || "").toString().toUpperCase().split('');
// 提取题目类型(第15列)
let type = "multiple"; // 默认多选题
if (row.length >= 16 && row[15]) {
const typeStr = row[15].toString().toLowerCase();
if (typeStr === "single") {
type = "single";
} else if (typeStr === "truefalse") {
type = "truefalse";
} else if (typeStr === "fillblank") {
type = "fillblank";
} else if (typeStr === "essay") {
type = "essay";
}
}
// 提取题目分数(第16列)
let points = 1; // 默认1分
if (row.length >= 17 && row[16]) {
const pointsValue = parseInt(row[16]);
if (!isNaN(pointsValue)) {
points = pointsValue;
}
}
// 提取答案解析(第17列)
const explanation = row[17] || "";
// 提取填空项
const blanks = [];
let blankScoringMode = "allOrNothing";
if (type === "fillblank") {
// 提取填空项(最多6个)
for (let j = 0; j < 6; j++) {
const answerIndex = 18 + j * 2;
const pointsIndex = answerIndex + 1;
if (answerIndex < row.length && row[answerIndex]) {
blanks.push({
answer: row[answerIndex] || "",
points: parseInt(row[pointsIndex]) || 1
});
}
}
}
// 提取完整答案(第30列)
const essayAnswer = row.length >= 31 ? row[30] || "" : "";
// 提取关键词
const keywords = [];
if (type === "essay") {
// 提取关键词(最多5个)
for (let j = 0; j < 5; j++) {
const keywordIndex = 31 + j * 2;
const pointsIndex = keywordIndex + 1;
if (keywordIndex < row.length && row[keywordIndex]) {
keywords.push({
keyword: row[keywordIndex] || "",
points: parseInt(row[pointsIndex]) || 1
});
}
}
}
// 提取评分规则(最后列)
let essayScoringMode = "partialCredit"; // 解答题默认按关键词给分
if (row.length >= 41 && row[40]) {
essayScoringMode = row[40] || "partialCredit";
}
// 创建题目对象
const question = {
id: Date.now() + i, // 唯一ID
text: questionText,
image: questionImage,
explanation: explanation,
type: type,
points: points,
options: options,
correct: answers,
blanks: blanks,
essayAnswer: essayAnswer,
keywords: keywords,
blankScoringMode: blankScoringMode,
essayScoringMode: essayScoringMode // 新增解答题评分规则
};
parsedQuestions.push(question);
}
if (parsedQuestions.length > 0) {
// 添加到题库
questions = questions.concat(parsedQuestions);
renderQuestionList();
// 启用按钮
exportExcelBtn.disabled = false;
exportCsvBtn.disabled = false;
clearAllBtn.disabled = false;
showNotification(`成功导入 ${parsedQuestions.length} 道题目`);
} else {
showNotification('未找到有效的题目数据', 'error');
}
} catch (error) {
console.error('导入Excel失败:', error);
showNotification('导入失败,请检查文件格式', 'error');
}
}
// 处理CSV导入
function handleCSVImport(csvText) {
try {
// 解析CSV内容
const parsedQuestions = parseCSV(csvText);
if (parsedQuestions.length > 0) {
// 添加到题库
questions = questions.concat(parsedQuestions);
renderQuestionList();
// 启用按钮
exportExcelBtn.disabled = false;
exportCsvBtn.disabled = false;
clearAllBtn.disabled = false;
showNotification(`成功导入 ${parsedQuestions.length} 道题目`);
} else {
showNotification('未找到有效的题目数据', 'error');
}
} catch (error) {
console.error('导入CSV失败:', error);
showNotification('导入失败,请检查文件格式', 'error');
}
}
// 解析CSV内容
function parseCSV(csvText) {
const questions = [];
const lines = csvText.split('\n');
// 检查标题行
if (lines.length === 0 || !lines[0].startsWith('问题,问题图片')) {
showNotification('CSV格式不正确,缺少标题行', 'error');
return [];
}
// 跳过标题行
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue; // 跳过空行
// 分割CSV行
const fields = parseCSVLine(line);
// 检查字段数量
if (fields.length < 17) {
console.warn(`跳过第 ${i+1} 行: 字段数量不足`, fields);
continue;
}
// 提取问题
const questionText = fields[0];
const questionImage = fields[1];
// 提取选项(最多6个)
const options = [];
for (let j = 0; j < 6; j++) {
const textIndex = 2 + j * 2;
const imageIndex = textIndex + 1;
if (textIndex < fields.length && (fields[textIndex] || fields[imageIndex])) {
options.push({
text: fields[textIndex] || '',
image: fields[imageIndex] || ''
});
}
}
// 提取答案
const answers = fields[14].toUpperCase().split('');
// 提取题目类型(第15列)
let type = 'multiple'; // 默认多选题
if (fields.length >= 16 && fields[15]) {
const typeStr = fields[15].toLowerCase();
if (typeStr === 'single') {
type = 'single';
} else if (typeStr === 'truefalse') {
type = 'truefalse';
} else if (typeStr === 'fillblank') {
type = 'fillblank';
} else if (typeStr === 'essay') {
type = 'essay';
}
}
// 提取题目分数(第16列)
let points = 1; // 默认1分
if (fields.length >= 17 && fields[16]) {
const pointsValue = parseInt(fields[16]);
if (!isNaN(pointsValue)) {
points = pointsValue;
}
}
// 提取答案解析(第17列)
let explanation = '';
if (fields.length >= 18) {
explanation = fields[17];
}
// 提取填空项
const blanks = [];
let blankScoringMode = 'allOrNothing';
if (type === 'fillblank') {
// 提取填空项(最多6个)
for (let j = 0; j < 6; j++) {
const answerIndex = 18 + j * 2;
const pointsIndex = answerIndex + 1;
if (answerIndex < fields.length && fields[answerIndex]) {
blanks.push({
answer: fields[answerIndex] || '',
points: parseInt(fields[pointsIndex]) || 1
});
}
}
}
// 提取完整答案(第30列)
const essayAnswer = fields.length >= 31 ? fields[30] || "" : "";
// 提取关键词
const keywords = [];
if (type === 'essay') {
// 提取关键词(最多5个)
for (let j = 0; j < 5; j++) {
const keywordIndex = 31 + j * 2;
const pointsIndex = keywordIndex + 1;
if (keywordIndex < fields.length && fields[keywordIndex]) {
keywords.push({
keyword: fields[keywordIndex] || '',
points: parseInt(fields[pointsIndex]) || 1
});
}
}
}
// 提取评分规则(最后列)
let essayScoringMode = "partialCredit"; // 解答题默认按关键词给分
if (fields.length >= 41 && fields[40]) {
essayScoringMode = fields[40];
}
// 创建题目对象
const question = {
id: Date.now() + i, // 唯一ID
text: questionText,
image: questionImage || '',
explanation: explanation,
type: type,
points: points,
options: options,
correct: answers,
blanks: blanks,
essayAnswer: essayAnswer,
keywords: keywords,
blankScoringMode: blankScoringMode,
essayScoringMode: essayScoringMode // 新增解答题评分规则
};
questions.push(question);
}
return questions;
}
// 解析CSV行
function parseCSVLine(line) {
const fields = [];
let currentField = '';
let inQuotes = false;
let escapeNext = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (escapeNext) {
currentField += char;
escapeNext = false;
continue;
}
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
// 转义引号
currentField += '"';
i++; // 跳过下一个引号
} else {
inQuotes = !inQuotes;
}
continue;
}
if (char === ',' && !inQuotes) {
fields.push(currentField);
currentField = '';
continue;
}
if (char === '\\') {
escapeNext = true;
continue;
}
currentField += char;
}
// 添加最后一个字段
fields.push(currentField);
return fields;
}
// 显示通知
function showNotification(message, type = 'success') {
const content = notification.querySelector('#notification-message');
content.textContent = message;
notification.className = 'notification';
notification.classList.add('show', type);
// 更新图标
const icon = notification.querySelector('i');
if (type === 'error') {
icon.className = 'fas fa-exclamation-circle';
} else {
icon.className = 'fas fa-check-circle';
}
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
// 初始化应用
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
成品
增强版答题系统-2025.10.7.zip
(45.92 KB, 下载次数: 400)
到此为止,除非有Bug不再更新 |
免费评分
-
查看全部评分
|