survey/public/survey.html
2025-10-28 22:08:35 +08:00

1450 lines
53 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学科能力测评问卷</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
padding: 20px;
line-height: 1.6;
}
.container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff7e5f 100%);
color: white;
padding: 30px;
position: relative;
overflow: hidden;
box-shadow: 0 4px 20px rgba(255, 126, 95, 0.3);
text-align: center;
}
.header h1 {
font-size: 32px;
margin-bottom: 10px;
position: relative;
z-index: 2;
}
.header p {
position: relative;
z-index: 2;
opacity: 0.9;
}
.config-section {
padding: 30px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.config-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.config-card h3 {
color: #2d5a3d;
margin-bottom: 15px;
font-size: 18px;
}
.rule-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
border-left: 4px solid #ff7e5f;
}
.rule-title {
font-weight: 600;
color: #2d5a3d;
margin-bottom: 5px;
}
.rule-details {
font-size: 14px;
color: #666;
}
.tag-filter {
margin: 15px 0;
}
.tag-filter label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.tag-input {
width: 100%;
padding: 10px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.tag-input:focus {
outline: none;
border-color: #ff7e5f;
}
.tag-select {
width: 100%;
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
background: white;
transition: all 0.3s;
min-height: 50px;
margin-bottom: 15px;
}
.tag-select:focus {
outline: none;
border-color: #ff7e5f;
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.1);
}
.tag-select option {
padding: 8px 12px;
}
.student-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-weight: 600;
color: #2d5a3d;
margin-bottom: 8px;
font-size: 14px;
}
.form-input {
padding: 12px 15px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
background: white;
}
.form-input:focus {
outline: none;
border-color: #ff7e5f;
box-shadow: 0 0 0 3px rgba(255, 126, 95, 0.1);
}
.form-input:valid {
border-color: #4CAF50;
}
.form-input:invalid:not(:placeholder-shown) {
border-color: #f44336;
}
.selected-tags {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
border-left: 4px solid #ff7e5f;
margin-top: 10px;
}
.selected-tags #selectedTagsList {
color: #2d5a3d;
font-weight: 500;
}
.start-btn {
background: linear-gradient(135deg, #ff7e5f, #feb47b);
color: white;
border: none;
padding: 15px 30px;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: block;
margin: 20px auto;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 126, 95, 0.4);
}
.start-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.survey-content {
padding: 30px;
display: none;
}
.progress-bar {
background: #e9ecef;
height: 8px;
border-radius: 4px;
margin-bottom: 30px;
overflow: hidden;
}
.progress-fill {
background: linear-gradient(90deg, #ff7e5f, #feb47b);
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
width: 0%;
}
.question-card {
background: white;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
}
.question-card:hover {
border-color: #ff7e5f;
box-shadow: 0 4px 20px rgba(255, 126, 95, 0.15);
}
.question-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.question-number {
background: linear-gradient(135deg, #ff7e5f, #feb47b);
color: white;
width: 35px;
height: 35px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 15px;
}
.question-type {
background: linear-gradient(135deg, #fff8f3, #ffffff);
color: #ff7e5f;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
margin-left: auto;
border: 1px solid #ff7e5f;
}
.question-type.基础题 {
background: #e8f5e8;
color: #2e7d32;
}
.question-type.进阶题 {
background: #fff3e0;
color: #f57c00;
}
.question-type.竞赛题 {
background: #fce4ec;
color: #c2185b;
}
.question-text {
font-size: 18px;
color: #333;
line-height: 1.6;
margin-bottom: 20px;
font-weight: 500;
}
.options-list {
list-style: none;
}
.option-item {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 15px 20px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
}
.option-item:hover {
background: linear-gradient(135deg, #fff8f3, #ffffff);
border-color: #ff7e5f;
transform: translateY(-1px);
}
.option-item.selected {
background: linear-gradient(135deg, #fff8f3, #ffffff);
border-color: #ff7e5f;
}
.option-item input[type="radio"] {
margin-right: 12px;
transform: scale(1.2);
}
.option-text {
flex: 1;
font-size: 16px;
color: #333;
}
.submit-btn {
background: linear-gradient(135deg, #4CAF50, #66BB6A);
color: white;
border: none;
padding: 15px 30px;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
display: block;
margin: 30px auto;
}
.submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);
}
.submit-btn:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.results-section {
padding: 30px;
display: none;
}
.score-card {
background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff7e5f 100%);
color: white;
padding: 30px;
border-radius: 12px;
text-align: center;
margin-bottom: 30px;
}
.score-value {
font-size: 48px;
font-weight: 700;
margin-bottom: 10px;
}
.score-label {
font-size: 18px;
opacity: 0.9;
}
.results-details {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
}
.result-item {
background: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
border-left: 4px solid #ff7e5f;
}
.loading {
text-align: center;
padding: 50px;
color: #666;
}
.error {
background: #ffebee;
color: #c62828;
padding: 20px;
border-radius: 8px;
margin: 20px;
border-left: 4px solid #f44336;
}
.view-report-btn {
background: linear-gradient(135deg, #FF6B6B, #4ECDC4);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.view-report-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(78, 205, 196, 0.4);
}
.report-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 12px;
width: 90%;
max-width: 900px;
max-height: 85vh;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.modal-header {
background: linear-gradient(135deg, #ff7e5f 0%, #feb47b 50%, #ff7e5f 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 18px;
}
.close-modal {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s;
}
.close-modal:hover {
background: rgba(255, 255, 255, 0.2);
}
.modal-body {
padding: 20px;
max-height: calc(85vh - 80px);
overflow-y: auto;
}
.report-actions {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.copy-btn, .download-btn {
background: linear-gradient(135deg, #4CAF50, #66BB6A);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.copy-btn:hover, .download-btn:hover {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(76, 175, 80, 0.3);
}
.download-btn {
background: linear-gradient(135deg, #2196F3, #42A5F5);
}
.download-btn:hover {
box-shadow: 0 3px 10px rgba(33, 150, 243, 0.3);
}
.report-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
overflow-x: auto;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.report-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
font-size: 12px;
}
.report-table th,
.report-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
vertical-align: top;
}
.report-table th {
background: #f2f2f2;
font-weight: bold;
}
.report-table tr:nth-child(even) {
background: #f9f9f9;
}
@media (max-width: 768px) {
.container {
margin: 10px;
border-radius: 10px;
}
.header, .config-section, .survey-content, .results-section {
padding: 20px;
}
.config-grid {
grid-template-columns: 1fr;
}
.question-text {
font-size: 16px;
}
.option-text {
font-size: 14px;
}
.score-value {
font-size: 36px;
}
.modal-content {
width: 95%;
max-height: 90vh;
}
.modal-header {
padding: 15px;
}
.modal-header h3 {
font-size: 16px;
}
.modal-body {
padding: 15px;
max-height: calc(90vh - 60px);
}
.report-actions {
flex-direction: column;
}
.copy-btn, .download-btn {
width: 100%;
}
.report-content {
font-size: 12px;
padding: 15px;
}
}
.footer {
padding: 20px;
text-align: center;
color: #8b5a2b;
font-size: 14px;
background: #fef6f0;
border-top: 1px solid #ffe4d6;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 学科能力测评问卷</h1>
<p>基于科学的题目分类和标签筛选系统</p>
</div>
<div class="config-section" id="configSection">
<div class="config-grid">
<div class="config-card">
<h3>👤 学员信息</h3>
<div class="student-form">
<div class="form-group">
<label for="studentName">姓名 *</label>
<input type="text" id="studentName" class="form-input" placeholder="请输入您的姓名" required>
</div>
<div class="form-group">
<label for="studentSchool">学校 *</label>
<input type="text" id="studentSchool" class="form-input" placeholder="请输入您的学校" required>
</div>
<div class="form-group">
<label for="studentGrade">年级 *</label>
<input type="text" id="studentGrade" class="form-input" placeholder="请输入您的年级" required>
</div>
</div>
</div>
<div class="config-card">
<h3>📋 抽题规则配置</h3>
<div id="rulesList">
<div class="rule-item">
<div class="rule-title">规则 1 - 基础题</div>
<div class="rule-details">10题 × 5分 = 50分</div>
</div>
<div class="rule-item">
<div class="rule-title">规则 2 - 进阶题</div>
<div class="rule-details">2题 × 10分 = 20分</div>
</div>
<div class="rule-item">
<div class="rule-title">规则 3 - 竞赛题</div>
<div class="rule-details">2题 × 15分 = 30分</div>
</div>
</div>
<div class="rule-details" style="margin-top: 15px; padding: 10px; background: #e3f2fd; border-radius: 6px;">
<strong>总题数: 14题 | 总分数: 100分</strong>
</div>
</div>
</div>
<div class="config-grid" style="margin-top: 20px;">
<div class="config-card">
<h3>🏷️ 标签筛选</h3>
<div class="tag-filter">
<label for="gradeFilter">题目标签筛选:</label>
<select id="gradeFilter" class="tag-select">
<option value="">选择年级/册次标签...</option>
</select>
<div class="selected-tags" id="selectedTags">
<small>已选择的标签: <span id="selectedTagsList"></span></small>
</div>
</div>
</div>
<div class="config-card">
<h3>⚙️ 高级设置</h3>
<div class="tag-filter">
<label for="customRules">自定义抽题规则 (JSON格式):</label>
<textarea id="customRules" class="tag-input" rows="4" placeholder='例如: {"基础题": 10, "进阶题": 2, "竞赛题": 2}'></textarea>
</div>
</div>
</div>
<button class="start-btn" id="startSurvey">开始答题</button>
</div>
<div class="survey-content" id="surveyContent">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div id="questionsContainer"></div>
<button class="submit-btn" id="submitBtn" disabled>提交问卷</button>
</div>
<div class="results-section" id="resultsSection">
<div class="score-card">
<div class="score-value" id="scoreValue">0</div>
<div class="score-label">总分 (100分制)</div>
<button class="view-report-btn" id="viewReportBtn" style="margin-top: 20px;">📊 查看测评报告</button>
</div>
<div class="results-details" id="resultsDetails"></div>
<div class="report-modal" id="reportModal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>📊 考试答题情况分析报告</h3>
<button class="close-modal" id="closeModal">×</button>
</div>
<div class="modal-body">
<div class="report-actions">
<button class="copy-btn" id="copyReportBtn">📋 复制报告</button>
<button class="download-btn" id="downloadReportBtn">💾 下载报告</button>
</div>
<div class="report-content" id="reportContent"></div>
</div>
</div>
</div>
</div>
<div class="footer">
<p>© 2024 尚逸基石学科能力测评系统</p>
</div>
</div>
<script>
class SurveySystem {
constructor() {
this.questions = {};
this.tags = {};
this.filteredQuestions = {};
this.currentQuestions = [];
this.answers = {};
this.currentRules = {
"基础题": 10,
"进阶题": 2,
"竞赛题": 2
};
this.selectedTag = '';
this.init();
}
async init() {
try {
await this.loadQuestions();
await this.loadTags();
this.initializeTagSelectors();
this.bindEvents();
this.updateConfigDisplay();
} catch (error) {
this.showError('系统初始化失败: ' + error.message);
}
}
async loadQuestions() {
try {
const response = await fetch('/api/questions');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.questions = await response.json();
console.log('题库加载成功:', {
基础题: this.questions['基础题']?.length || 0,
进阶题: this.questions['进阶题']?.length || 0,
竞赛题: this.questions['竞赛题']?.length || 0
});
} catch (error) {
throw new Error('题库加载失败: ' + error.message);
}
}
async loadTags() {
try {
const response = await fetch('/api/tags');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.tags = await response.json();
console.log('标签数据加载成功:', this.tags.total_unique_tags, '个标签');
} catch (error) {
console.warn('标签数据加载失败,将使用备用方案:', error.message);
this.generateTagsFromQuestions();
}
}
generateTagsFromQuestions() {
const allTags = new Set();
Object.values(this.questions).flat().forEach(question => {
const tags = question['题目标签'] || question['标签'] || '';
if (tags) {
tags.split(/[\s,]+/).forEach(tag => {
if (tag.trim()) allTags.add(tag.trim());
});
}
});
this.tags = {
tags: Array.from(allTags).sort(),
tag_counts: {},
total_unique_tags: allTags.size
};
}
initializeTagSelectors() {
const gradeTags = this.tags.tags.filter(tag =>
tag.includes('年级') || tag.includes('册')
);
this.populateSelect('gradeFilter', gradeTags);
}
populateSelect(selectId, tags) {
const select = document.getElementById(selectId);
select.innerHTML = '<option value="">选择年级/册次标签...</option>';
tags.forEach(tag => {
const option = document.createElement('option');
option.value = tag;
option.textContent = tag;
select.appendChild(option);
});
}
bindEvents() {
document.getElementById('startSurvey').addEventListener('click', () => this.startSurvey());
document.getElementById('submitBtn').addEventListener('click', () => this.submitSurvey());
// 年级标签下拉菜单事件
document.getElementById('gradeFilter').addEventListener('change', () => this.updateSelectedTags());
// 自定义规则监听
document.getElementById('customRules').addEventListener('input', () => this.updateCustomRules());
// 测评报告相关事件
document.getElementById('viewReportBtn').addEventListener('click', () => this.showReportModal());
document.getElementById('closeModal').addEventListener('click', () => this.hideReportModal());
document.getElementById('copyReportBtn').addEventListener('click', () => this.copyReport());
document.getElementById('downloadReportBtn').addEventListener('click', () => this.downloadReport());
// 点击模态框外部关闭
document.getElementById('reportModal').addEventListener('click', (e) => {
if (e.target.id === 'reportModal') {
this.hideReportModal();
}
});
}
updateSelectedTags() {
const select = document.getElementById('gradeFilter');
this.selectedTag = select.value || '';
this.updateSelectedTagsDisplay();
this.updateConfigDisplay();
}
updateSelectedTagsDisplay() {
const container = document.getElementById('selectedTagsList');
if (this.selectedTag) {
container.innerHTML = this.selectedTag;
} else {
container.innerHTML = '无';
}
}
updateCustomRules() {
const customRulesText = document.getElementById('customRules').value.trim();
if (customRulesText) {
try {
const customRules = JSON.parse(customRulesText);
if (customRules && typeof customRules === 'object') {
this.currentRules = { ...this.currentRules, ...customRules };
this.updateConfigDisplay();
}
} catch (error) {
console.error('自定义规则解析失败:', error);
}
}
}
filterQuestionsByTags(selectedTag) {
if (!selectedTag) {
return this.questions;
}
const filtered = {
"基础题": [],
"进阶题": [],
"竞赛题": []
};
Object.keys(this.questions).forEach(type => {
filtered[type] = this.questions[type].filter(question => {
const questionTags = question['题目标签'] || question['标签'] || '';
if (!questionTags) return false;
// 正确分割标签并检查是否包含选中的标签
const tagList = questionTags.split(/[\s,]+/);
return tagList.includes(selectedTag);
});
});
return filtered;
}
selectRandomQuestions(questionsByType, rules) {
const selected = [];
Object.keys(rules).forEach(type => {
const count = rules[type];
const availableQuestions = questionsByType[type] || [];
if (availableQuestions.length < count) {
console.warn(`${type}可用题目不足 (${availableQuestions.length}/${count})`);
}
// 随机选择题目
const shuffled = [...availableQuestions].sort(() => 0.5 - Math.random());
const selectedForType = shuffled.slice(0, Math.min(count, availableQuestions.length));
selectedForType.forEach(question => {
selected.push({
...question,
questionType: type,
questionId: `${type}_${question['序号']}`
});
});
});
// 随机打乱题目顺序
return selected.sort(() => 0.5 - Math.random());
}
updateConfigDisplay() {
this.filteredQuestions = this.filterQuestionsByTags(this.selectedTag);
let totalQuestions = 0;
let totalScore = 0;
let rulesHTML = '';
Object.keys(this.currentRules).forEach((type, index) => {
const count = this.currentRules[type];
const available = this.filteredQuestions[type]?.length || 0;
const scorePerQuestion = type === '基础题' ? 5 : type === '进阶题' ? 10 : 15;
const ruleScore = count * scorePerQuestion;
totalQuestions += count;
totalScore += ruleScore;
const availabilityClass = available >= count ? '✅' : '❌';
rulesHTML += `
<div class="rule-item">
<div class="rule-title">规则 ${index + 1} - ${type} ${availabilityClass}</div>
<div class="rule-details">${count}× ${scorePerQuestion}分 = ${ruleScore}分 (可用: ${available}题)</div>
</div>
`;
});
document.getElementById('rulesList').innerHTML = rulesHTML;
const rulesList = document.getElementById('rulesList').parentElement;
const summaryDiv = rulesList.querySelector('.rule-details[style*="background"]');
const summaryText = `<strong>总题数: ${totalQuestions}题 | 总分数: ${totalScore}分</strong>`;
if (summaryDiv) {
summaryDiv.innerHTML = summaryText;
} else {
rulesList.innerHTML += `
<div class="rule-details" style="margin-top: 15px; padding: 10px; background: #e3f2fd; border-radius: 6px;">
${summaryText}
</div>
`;
}
// 检查是否有足够的题目
const canStart = Object.keys(this.currentRules).every(type => {
const needed = this.currentRules[type];
const available = this.filteredQuestions[type]?.length || 0;
return available >= needed;
});
const startBtn = document.getElementById('startSurvey');
startBtn.disabled = !canStart;
if (!canStart) {
startBtn.textContent = '题目不足,无法开始';
startBtn.style.background = '#ccc';
} else {
startBtn.textContent = '开始答题';
startBtn.style.background = '';
}
}
async startSurvey() {
try {
// 验证学员信息
const studentName = document.getElementById('studentName').value.trim();
const studentSchool = document.getElementById('studentSchool').value.trim();
const studentGrade = document.getElementById('studentGrade').value.trim();
if (!studentName || !studentSchool || !studentGrade) {
this.showError('请填写完整的学员信息(姓名、学校、年级)');
return;
}
// 检查题目是否充足
const canStart = Object.keys(this.currentRules).every(type => {
const needed = this.currentRules[type];
const available = this.filteredQuestions[type]?.length || 0;
return available >= needed;
});
if (!canStart) {
this.showError('题目不足,无法开始答题,请调整配置');
return;
}
// 禁用按钮并显示加载状态
const startBtn = document.getElementById('startSurvey');
const originalText = startBtn.textContent;
startBtn.textContent = '正在创建答题会话...';
startBtn.disabled = true;
// 调用后端API创建会话
const response = await fetch('/api/create-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: studentName,
school: studentSchool,
grade: studentGrade,
selectedTag: this.selectedTag,
questionsConfig: this.currentRules
})
});
if (!response.ok) {
throw new Error(`创建会话失败: ${response.status}`);
}
const result = await response.json();
if (result.success) {
// 保存会话信息
this.sessionId = result.sessionId;
this.studentId = result.studentId;
// 跳转到答题页面
window.location.href = `/quiz/${result.sessionId}`;
} else {
throw new Error('创建会话失败');
}
} catch (error) {
console.error('开始答题失败:', error);
this.showError('开始答题失败: ' + error.message);
// 恢复按钮状态
const startBtn = document.getElementById('startSurvey');
startBtn.textContent = '开始答题';
startBtn.disabled = false;
}
}
renderQuestions() {
const container = document.getElementById('questionsContainer');
container.innerHTML = '';
this.currentQuestions.forEach((question, index) => {
const questionCard = document.createElement('div');
questionCard.className = 'question-card';
questionCard.innerHTML = `
<div class="question-header">
<div class="question-number">${index + 1}</div>
<div class="question-type ${question.questionType}">${question.questionType}</div>
</div>
<div class="question-text">${question['题干']}</div>
<ul class="options-list">
${this.renderOptions(question, index)}
</ul>
`;
container.appendChild(questionCard);
});
// 绑定选项事件
this.bindOptionEvents();
}
renderOptions(question, questionIndex) {
const options = [];
const labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
// 通过遍历所有键来找到选项
for (let i = 0; i < labels.length; i++) {
let optionText = null;
// 遍历问题的所有键,寻找匹配的选项
for (const key in question) {
// 检查键是否包含当前选项字母(忽略空格数量)
if (key.replace(/\s+/g, '').includes(`选项${labels[i]}`)) {
optionText = question[key];
break;
}
}
if (optionText && optionText.trim()) {
options.push(`
<li class="option-item" data-question="${questionIndex}" data-option="${labels[i]}">
<input type="radio" name="question_${questionIndex}" value="${labels[i]}" id="option_${questionIndex}_${i}">
<label for="option_${questionIndex}_${i}" class="option-text">${labels[i]}. ${optionText}</label>
</li>
`);
}
}
return options.join('');
}
bindOptionEvents() {
document.querySelectorAll('.option-item').forEach(item => {
item.addEventListener('click', function() {
const questionIndex = this.dataset.question;
const option = this.dataset.option;
const radio = this.querySelector('input[type="radio"]');
// 清除同一题目的其他选中状态
document.querySelectorAll(`.option-item[data-question="${questionIndex}"]`).forEach(otherItem => {
otherItem.classList.remove('selected');
});
// 设置当前选中状态
this.classList.add('selected');
radio.checked = true;
// 记录答案
window.survey.answers[questionIndex] = option;
window.survey.checkCompletion();
});
});
}
updateProgress() {
const total = this.currentQuestions.length;
const answered = Object.keys(this.answers).length;
const percentage = total > 0 ? (answered / total) * 100 : 0;
document.getElementById('progressFill').style.width = percentage + '%';
}
checkCompletion() {
this.updateProgress();
const total = this.currentQuestions.length;
const answered = Object.keys(this.answers).length;
document.getElementById('submitBtn').disabled = answered !== total;
}
submitSurvey() {
try {
const results = this.calculateResults();
this.displayResults(results);
document.getElementById('surveyContent').style.display = 'none';
document.getElementById('resultsSection').style.display = 'block';
} catch (error) {
this.showError('提交失败: ' + error.message);
}
}
calculateResults() {
let totalScore = 0;
let correctCount = 0;
const results = [];
this.currentQuestions.forEach((question, index) => {
const userAnswer = this.answers[index];
const correctAnswer = question['答案'];
const score = parseInt(question['分数']) || 0;
const isCorrect = userAnswer === correctAnswer;
if (isCorrect) {
totalScore += score;
correctCount++;
}
results.push({
questionNumber: index + 1,
questionText: question['题干'],
userAnswer: userAnswer || '未作答',
correctAnswer: correctAnswer,
isCorrect: isCorrect,
score: score,
questionType: question.questionType
});
});
return {
totalScore: totalScore,
correctCount: correctCount,
totalQuestions: this.currentQuestions.length,
percentage: Math.round((totalScore / 100) * 100),
results: results
};
}
displayResults(results) {
document.getElementById('scoreValue').textContent = results.totalScore + '分';
let detailsHTML = `
<h3>答题详情</h3>
<p>正确: ${results.correctCount}/${results.totalQuestions} 题</p>
<hr style="margin: 15px 0;">
`;
results.results.forEach(result => {
const statusClass = result.isCorrect ? '✅ 正确' : '❌ 错误';
detailsHTML += `
<div class="result-item">
<strong>第${result.questionNumber}题 (${result.questionType})</strong> - ${statusClass}<br>
<small>题目: ${result.questionText}</small><br>
<small>你的答案: ${result.userAnswer} | 正确答案: ${result.correctAnswer}</small><br>
<small>得分: ${result.isCorrect ? result.score : 0}分</small>
</div>
`;
});
document.getElementById('resultsDetails').innerHTML = detailsHTML;
this.currentResults = results;
}
showReportModal() {
const reportText = this.generateReportText();
document.getElementById('reportContent').innerHTML = this.formatReportAsTable(reportText);
document.getElementById('reportModal').style.display = 'flex';
}
hideReportModal() {
document.getElementById('reportModal').style.display = 'none';
}
generateReportText() {
if (!this.currentResults) return '';
const timestamp = new Date().toLocaleString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
let report = `# 考试答题情况分析\n\n`;
report += `测评时间: ${timestamp}\n`;
report += `总分: ${this.currentResults.totalScore}\n`;
report += `正确率: ${this.currentResults.correctCount}/${this.currentResults.totalQuestions} (${Math.round((this.currentResults.correctCount/this.currentResults.totalQuestions)*100)}%)\n\n`;
// 添加一些基本信息(可以后续扩展为实际的用户信息表单)
report += `| 题目 | 题型 | 用户答案 | 正确答案 | 是否正确 |\n`;
report += `|------|------|----------|----------|----------|\n`;
report += `| 姓名 | 填空题 | 学员 | 无标准答案 | 无法判断 |\n`;
report += `| 学校 | 填空题 | 学校名称 | 无标准答案 | 无法判断 |\n`;
report += `| 年级 | 填空题 | ${this.selectedTag || '未选择'} | 无标准答案 | 无法判断 |\n`;
// 添加答题情况
this.currentResults.results.forEach(result => {
const status = result.isCorrect ? '正确' : '错误';
report += `| ${result.questionText} | ${result.questionType} | ${result.userAnswer || '未作答'} | ${result.correctAnswer} | ${status} |\n`;
});
return report;
}
formatReportAsTable(reportText) {
const lines = reportText.split('\n');
let html = '';
let inTable = false;
lines.forEach(line => {
if (line.startsWith('# ')) {
html += `<h2>${line.substring(2)}</h2>\n`;
} else if (line.startsWith('## ')) {
html += `<h3>${line.substring(3)}</h3>\n`;
} else if (line.startsWith('|') && line.includes('---')) {
// 表格头
html += `<table class="report-table"><thead><tr>\n`;
const headers = line.split('|').filter(cell => cell.trim()).map(cell => cell.trim());
headers.forEach(header => {
html += `<th>${header}</th>\n`;
});
html += `</tr></thead><tbody>\n`;
inTable = true;
} else if (line.startsWith('|') && inTable) {
// 表格行
html += `<tr>\n`;
const cells = line.split('|').filter(cell => cell !== '').map(cell => cell.trim());
cells.forEach(cell => {
html += `<td>${cell}</td>\n`;
});
html += `</tr>\n`;
} else if (line.trim() === '' && inTable) {
html += `</tbody></table>\n`;
inTable = false;
} else if (line.trim() !== '') {
html += `<p>${line}</p>\n`;
}
});
if (inTable) {
html += `</tbody></table>\n`;
}
return html;
}
copyReport() {
const reportText = this.generateReportText();
navigator.clipboard.writeText(reportText).then(() => {
this.showNotification('报告已复制到剪贴板!');
}).catch(err => {
console.error('复制失败:', err);
this.showNotification('复制失败,请手动复制', 'error');
});
}
downloadReport() {
const reportText = this.generateReportText();
const now = new Date();
const timestamp = new Date(now.toLocaleString("en-US", {timeZone: "Asia/Shanghai"}))
.toISOString().slice(0, 19).replace(/[:-]/g, '');
const filename = `测评报告_${timestamp}.txt`;
const blob = new Blob([reportText], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification(`报告已下载: ${filename}`);
}
showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? 'linear-gradient(135deg, #4CAF50, #66BB6A)' : 'linear-gradient(135deg, #f44336, #e57373)'};
color: white;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
font-size: 14px;
font-weight: 500;
animation: slideIn 0.3s ease;
`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
showError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = message;
document.querySelector('.container').insertBefore(errorDiv, document.querySelector('.config-section'));
setTimeout(() => {
errorDiv.remove();
}, 5000);
}
}
// 初始化系统
window.survey = new SurveySystem();
</script>
</body>
</html>