745 lines
27 KiB
Python
745 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import sqlite3
|
|
import json
|
|
import uuid
|
|
from datetime import datetime, timezone, timedelta
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from urllib.parse import urlparse, parse_qs
|
|
import cgi
|
|
|
|
def get_east8_time_string():
|
|
"""获取东八区时间字符串格式,用于数据库存储"""
|
|
east8_tz = timezone(timedelta(hours=8))
|
|
return datetime.now(east8_tz).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
class SurveyAPI:
|
|
def __init__(self, db_path='data/survey.db'):
|
|
self.db_path = db_path
|
|
|
|
def get_connection(self):
|
|
conn = sqlite3.connect(self.db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
def create_student(self, name, school, grade, phone, selected_tag=''):
|
|
"""创建学员记录"""
|
|
student_id = str(uuid.uuid4())
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
INSERT INTO students (id, name, school, grade, phone, selected_tag)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
''', (student_id, name, school, grade, phone, selected_tag))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
return student_id
|
|
|
|
def create_quiz_session(self, student_id, questions_config):
|
|
"""创建答题会话"""
|
|
session_id = str(uuid.uuid4())
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
INSERT INTO quiz_sessions (id, student_id, questions_config, status)
|
|
VALUES (?, ?, ?, 'created')
|
|
''', (session_id, student_id, json.dumps(questions_config)))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
return session_id
|
|
|
|
def save_answers(self, session_id, answers):
|
|
"""保存答题结果"""
|
|
conn = self.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
total_score = 0
|
|
|
|
for answer in answers:
|
|
answer_id = str(uuid.uuid4())
|
|
user_answer = answer.get('user_answer', '')
|
|
correct_answer = answer.get('correct_answer', '')
|
|
is_correct = user_answer == correct_answer
|
|
score = answer['score'] if is_correct else 0
|
|
total_score += score
|
|
|
|
cursor.execute('''
|
|
INSERT INTO quiz_answers
|
|
(id, session_id, question_id, question_text, question_type,
|
|
user_answer, correct_answer, is_correct, score)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (answer_id, session_id, answer.get('question_id', ''), answer.get('question_text', ''),
|
|
answer.get('question_type', ''), user_answer, correct_answer,
|
|
is_correct, score))
|
|
|
|
# 更新会话状态
|
|
cursor.execute('''
|
|
UPDATE quiz_sessions
|
|
SET status = 'completed', total_score = ?, completed_at = ?
|
|
WHERE id = ?
|
|
''', (total_score, get_east8_time_string(), session_id))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
return total_score
|
|
|
|
class SurveyHandler(BaseHTTPRequestHandler):
|
|
def __init__(self, *args, **kwargs):
|
|
self.api = SurveyAPI()
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def do_OPTIONS(self):
|
|
"""处理预检请求"""
|
|
self.send_response(200)
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
self.end_headers()
|
|
|
|
def send_cors_headers(self):
|
|
"""发送CORS头"""
|
|
self.send_header('Access-Control-Allow-Origin', '*')
|
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
|
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
|
|
|
def do_POST(self):
|
|
"""处理POST请求"""
|
|
parsed_path = urlparse(self.path)
|
|
path = parsed_path.path
|
|
|
|
try:
|
|
if path == '/api/create-session':
|
|
self.handle_create_session()
|
|
elif path == '/api/save-answers':
|
|
self.handle_save_answers()
|
|
else:
|
|
self.send_error(404, "API endpoint not found")
|
|
except Exception as e:
|
|
self.send_error(500, str(e))
|
|
|
|
def handle_create_session(self):
|
|
"""创建答题会话"""
|
|
content_length = int(self.headers['Content-Length'])
|
|
post_data = self.rfile.read(content_length)
|
|
data = json.loads(post_data.decode('utf-8'))
|
|
|
|
name = data.get('name', '').strip()
|
|
school = data.get('school', '').strip()
|
|
grade = data.get('grade', '').strip()
|
|
phone = data.get('phone', '').strip()
|
|
selected_subject = data.get('selectedSubject', '').strip()
|
|
selected_semester = data.get('selectedSemester', '').strip()
|
|
selected_exam_type = data.get('selectedExamType', '').strip()
|
|
|
|
# 构建描述性标签
|
|
selected_tag = ""
|
|
if selected_subject and grade and selected_semester and selected_exam_type:
|
|
selected_tag = f"{selected_subject}-{grade}{selected_semester}-{selected_exam_type}考试"
|
|
elif selected_subject and grade and selected_semester:
|
|
selected_tag = f"{selected_subject}-{grade}{selected_semester}"
|
|
elif selected_subject and grade:
|
|
selected_tag = f"{selected_subject}-{grade}"
|
|
elif selected_subject:
|
|
selected_tag = selected_subject
|
|
|
|
questions_config = data.get('questionsConfig', {})
|
|
|
|
if not name or not school or not grade or not phone:
|
|
self.send_error(400, "姓名、学校、年级和手机号不能为空")
|
|
return
|
|
|
|
# 创建学员记录
|
|
student_id = self.api.create_student(name, school, grade, phone, selected_tag)
|
|
|
|
# 创建答题会话
|
|
session_id = self.api.create_quiz_session(student_id, questions_config)
|
|
|
|
response = {
|
|
'success': True,
|
|
'sessionId': session_id,
|
|
'studentId': student_id
|
|
}
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.send_cors_headers()
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
|
|
|
|
def handle_save_answers(self):
|
|
"""保存答题结果"""
|
|
content_length = int(self.headers['Content-Length'])
|
|
post_data = self.rfile.read(content_length)
|
|
data = json.loads(post_data.decode('utf-8'))
|
|
|
|
session_id = data.get('sessionId')
|
|
answers = data.get('answers', [])
|
|
|
|
if not session_id or not answers:
|
|
self.send_error(400, "会话ID和答题结果不能为空")
|
|
return
|
|
|
|
print(f"Debug: session_id = {session_id}")
|
|
print(f"Debug: answers count = {len(answers)}")
|
|
print(f"Debug: first answer keys = {list(answers[0].keys()) if answers else 'None'}")
|
|
|
|
total_score = self.api.save_answers(session_id, answers)
|
|
|
|
response = {
|
|
'success': True,
|
|
'totalScore': total_score
|
|
}
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'application/json')
|
|
self.send_cors_headers()
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(response, ensure_ascii=False).encode('utf-8'))
|
|
|
|
def do_GET(self):
|
|
"""处理GET请求"""
|
|
parsed_path = urlparse(self.path)
|
|
path = parsed_path.path
|
|
|
|
try:
|
|
if path.startswith('/quiz/'):
|
|
self.handle_quiz_page(path)
|
|
else:
|
|
self.send_error(404, "Page not found")
|
|
except Exception as e:
|
|
self.send_error(500, str(e))
|
|
|
|
def handle_quiz_page(self, path):
|
|
"""处理答题页面"""
|
|
session_id = path.split('/')[-1]
|
|
|
|
if not session_id:
|
|
self.send_error(400, "Session ID is required")
|
|
return
|
|
|
|
# 验证会话是否存在
|
|
conn = self.api.get_connection()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
SELECT s.*, qs.questions_config, qs.status
|
|
FROM students s
|
|
JOIN quiz_sessions qs ON s.id = qs.student_id
|
|
WHERE qs.id = ?
|
|
''', (session_id,))
|
|
|
|
session_data = cursor.fetchone()
|
|
conn.close()
|
|
|
|
if not session_data:
|
|
self.send_error(404, "Session not found")
|
|
return
|
|
|
|
# 生成答题页面HTML
|
|
html_content = self.generate_quiz_page(session_data)
|
|
|
|
self.send_response(200)
|
|
self.send_header('Content-Type', 'text/html; charset=utf-8')
|
|
self.send_cors_headers()
|
|
self.end_headers()
|
|
self.wfile.write(html_content.encode('utf-8'))
|
|
|
|
def generate_quiz_page(self, session_data):
|
|
"""生成答题页面HTML"""
|
|
questions_config = json.loads(session_data['questions_config'])
|
|
student_name = session_data['name']
|
|
school = session_data['school']
|
|
grade = session_data['grade']
|
|
phone = session_data['phone']
|
|
|
|
return f'''<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{student_name} - 学科能力测评</title>
|
|
<style>
|
|
* {{
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
font-family: 'Microsoft YaHei', sans-serif;
|
|
}}
|
|
|
|
body {{
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}}
|
|
|
|
.container {{
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.header {{
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
text-align: center;
|
|
}}
|
|
|
|
.student-info {{
|
|
background: #f8f9fa;
|
|
padding: 20px;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}}
|
|
|
|
.info-grid {{
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
}}
|
|
|
|
.info-item {{
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #4facfe;
|
|
}}
|
|
|
|
.info-label {{
|
|
font-weight: 600;
|
|
color: #2d5a3d;
|
|
margin-bottom: 5px;
|
|
}}
|
|
|
|
.info-value {{
|
|
color: #333;
|
|
font-size: 16px;
|
|
}}
|
|
|
|
.progress-bar {{
|
|
background: #e9ecef;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
margin: 20px 30px;
|
|
overflow: hidden;
|
|
}}
|
|
|
|
.progress-fill {{
|
|
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
width: 0%;
|
|
}}
|
|
|
|
.questions-container {{
|
|
padding: 30px;
|
|
}}
|
|
|
|
.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);
|
|
}}
|
|
|
|
.question-header {{
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}}
|
|
|
|
.question-number {{
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
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: #e3f2fd;
|
|
color: #1976d2;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
margin-left: auto;
|
|
}}
|
|
|
|
.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: #e3f2fd;
|
|
border-color: #4facfe;
|
|
transform: translateY(-1px);
|
|
}}
|
|
|
|
.option-item.selected {{
|
|
background: #e3f2fd;
|
|
border-color: #4facfe;
|
|
}}
|
|
|
|
.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;
|
|
}}
|
|
|
|
.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;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🎯 学科能力测评</h1>
|
|
<p>请认真回答以下问题</p>
|
|
</div>
|
|
|
|
<div class="student-info">
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">姓名</div>
|
|
<div class="info-value">{student_name}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">学校</div>
|
|
<div class="info-value">{school}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">年级</div>
|
|
<div class="info-value">{grade}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">手机号</div>
|
|
<div class="info-value">{phone}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progressFill"></div>
|
|
</div>
|
|
|
|
<div class="questions-container" id="questionsContainer">
|
|
<div class="loading">正在加载题目...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
class QuizSystem {{
|
|
constructor() {{
|
|
this.sessionId = '{session_id}';
|
|
this.questions = [];
|
|
this.answers = {{}};
|
|
this.init();
|
|
}}
|
|
|
|
async init() {{
|
|
try {{
|
|
await this.loadQuestions();
|
|
this.renderQuestions();
|
|
}} catch (error) {{
|
|
this.showError('系统初始化失败: ' + error.message);
|
|
}}
|
|
}}
|
|
|
|
async loadQuestions() {{
|
|
const response = await fetch('questions.json');
|
|
if (!response.ok) {{
|
|
throw new Error('题库加载失败');
|
|
}}
|
|
const allQuestions = await response.json();
|
|
|
|
// 根据配置选择题目
|
|
const questionsConfig = {json.dumps(questions_config)};
|
|
this.questions = this.selectQuestions(allQuestions, questionsConfig);
|
|
|
|
if (this.questions.length === 0) {{
|
|
throw new Error('没有可用的题目');
|
|
}}
|
|
}}
|
|
|
|
selectQuestions(allQuestions, config) {{
|
|
const selected = [];
|
|
|
|
Object.keys(config).forEach(type => {{
|
|
const count = config[type];
|
|
const availableQuestions = allQuestions[type] || [];
|
|
|
|
// 随机选择题目
|
|
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());
|
|
}}
|
|
|
|
renderQuestions() {{
|
|
const container = document.getElementById('questionsContainer');
|
|
container.innerHTML = '';
|
|
|
|
this.questions.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 < 8; i++) {{
|
|
const optionKey = `选项 ${{labels[i]}}`.trim();
|
|
const optionText = question[optionKey];
|
|
|
|
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.quiz.answers[questionIndex] = option;
|
|
window.quiz.updateProgress();
|
|
window.quiz.checkCompletion();
|
|
}});
|
|
}});
|
|
|
|
// 添加提交按钮
|
|
const container = document.getElementById('questionsContainer');
|
|
const submitBtn = document.createElement('button');
|
|
submitBtn.className = 'submit-btn';
|
|
submitBtn.textContent = '提交答题';
|
|
submitBtn.disabled = true;
|
|
submitBtn.onclick = () => this.submitQuiz();
|
|
container.appendChild(submitBtn);
|
|
}}
|
|
|
|
updateProgress() {{
|
|
const total = this.questions.length;
|
|
const answered = Object.keys(this.answers).length;
|
|
const percentage = total > 0 ? (answered / total) * 100 : 0;
|
|
|
|
document.getElementById('progressFill').style.width = percentage + '%';
|
|
}}
|
|
|
|
checkCompletion() {{
|
|
const total = this.questions.length;
|
|
const answered = Object.keys(this.answers).length;
|
|
const submitBtn = document.querySelector('.submit-btn');
|
|
submitBtn.disabled = answered !== total;
|
|
}}
|
|
|
|
async submitQuiz() {{
|
|
try {{
|
|
const submitBtn = document.querySelector('.submit-btn');
|
|
submitBtn.textContent = '正在提交...';
|
|
submitBtn.disabled = true;
|
|
|
|
const answersData = this.questions.map((question, index) => ({{
|
|
questionId: question.questionId,
|
|
questionText: question['题干'],
|
|
questionType: question.questionType,
|
|
userAnswer: this.answers[index] || '',
|
|
correctAnswer: question['答案'],
|
|
score: parseInt(question['分数']) || 0
|
|
}}));
|
|
|
|
const response = await fetch('/api/save-answers', {{
|
|
method: 'POST',
|
|
headers: {{
|
|
'Content-Type': 'application/json'
|
|
}},
|
|
body: JSON.stringify({{
|
|
sessionId: this.sessionId,
|
|
answers: answersData
|
|
}})
|
|
}});
|
|
|
|
if (response.ok) {{
|
|
const result = await response.json();
|
|
this.showResults(result.totalScore);
|
|
}} else {{
|
|
throw new Error('提交失败');
|
|
}}
|
|
}} catch (error) {{
|
|
this.showError('提交失败: ' + error.message);
|
|
const submitBtn = document.querySelector('.submit-btn');
|
|
submitBtn.textContent = '提交答题';
|
|
submitBtn.disabled = false;
|
|
}}
|
|
}}
|
|
|
|
showResults(totalScore) {{
|
|
const container = document.getElementById('questionsContainer');
|
|
const totalPossible = this.questions.reduce((sum, q) => sum + (parseInt(q['分数']) || 0), 0);
|
|
const correctCount = this.calculateCorrectCount();
|
|
|
|
container.innerHTML = `
|
|
<div style="text-align: center; padding: 50px;">
|
|
<h2 style="color: #2d5a3d; margin-bottom: 20px;">🎉 答题完成!</h2>
|
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; margin: 20px 0;">
|
|
<div style="font-size: 48px; font-weight: 700; margin-bottom: 10px;">${{totalScore}}分</div>
|
|
<div style="font-size: 18px; opacity: 0.9;">总分 ${{totalPossible}}分</div>
|
|
<div style="margin-top: 10px;">正确率: ${{correctCount}}/${{this.questions.length}}</div>
|
|
</div>
|
|
<p style="color: #666; margin-top: 20px;">感谢您的参与!测评结果已保存。</p>
|
|
</div>
|
|
`;
|
|
}}
|
|
|
|
calculateCorrectCount() {{
|
|
let correctCount = 0;
|
|
this.questions.forEach((question, index) => {{
|
|
if (this.answers[index] === question['答案']) {{
|
|
correctCount++;
|
|
}}
|
|
}});
|
|
return correctCount;
|
|
}}
|
|
|
|
showError(message) {{
|
|
const container = document.getElementById('questionsContainer');
|
|
container.innerHTML = `<div class="error">${{message}}</div>`;
|
|
}}
|
|
}}
|
|
|
|
// 初始化答题系统
|
|
window.quiz = new QuizSystem();
|
|
</script>
|
|
</body>
|
|
</html>'''
|
|
|
|
def run_server(port=5678):
|
|
"""运行HTTP服务器"""
|
|
server_address = ('', port)
|
|
httpd = HTTPServer(server_address, SurveyHandler)
|
|
print(f"Survey API服务器已启动: http://localhost:{port}")
|
|
httpd.serve_forever()
|
|
|
|
if __name__ == "__main__":
|
|
run_server() |