survey/public/quiz.html
2025-11-30 23:12:06 +08:00

800 lines
30 KiB
HTML
Raw Permalink 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>{{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;
}
.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(180px, 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: #e8f5e8;
color: #2e7d32;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
margin-left: auto;
border: 1px solid #2e7d32;
}
.question-type.基础题 {
background: #e8f5e8;
color: #2e7d32;
border: 1px solid #2e7d32;
}
.question-type.进阶题 {
background: #fff3e0;
color: #f57c00;
border: 1px solid #f57c00;
}
.question-type.竞赛题 {
background: #fce4ec;
color: #c2185b;
border: 1px solid #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: #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;
}
.result-section {
text-align: center;
padding: 40px 30px;
display: none;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 20px;
margin: 20px 0;
}
.score-display {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px;
border-radius: 20px;
margin: 0 auto 30px;
max-width: 500px;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
position: relative;
overflow: hidden;
}
.score-display::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, transparent 50%, rgba(255,255,255,0.1) 100%);
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
50% { transform: translateX(100%) translateY(100%) rotate(45deg); }
100% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
}
.score-value {
font-size: 48px;
font-weight: 700;
margin-bottom: 10px;
position: relative;
z-index: 2;
}
.score-label {
font-size: 18px;
opacity: 0.9;
margin-bottom: 20px;
position: relative;
z-index: 2;
}
.success-icon {
font-size: 60px;
margin-bottom: 20px;
animation: bounce 2s ease-in-out infinite;
position: relative;
z-index: 2;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
60% { transform: translateY(-5px); }
}
.report-status-container {
background: white;
padding: 30px;
border-radius: 15px;
margin: 0 auto;
max-width: 600px;
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.status-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 25px;
}
.status-icon {
font-size: 40px;
margin-right: 15px;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
100% { transform: scale(1); opacity: 1; }
}
.status-title {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.status-message {
font-size: 16px;
color: #666;
margin-bottom: 25px;
line-height: 1.6;
}
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 25px;
}
.loading-dots {
display: inline-flex;
gap: 8px;
}
.loading-dot {
width: 12px;
height: 12px;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
border-radius: 50%;
animation: bounce-dots 1.4s ease-in-out infinite both;
}
.loading-dot:nth-child(1) { animation-delay: -0.32s; }
.loading-dot:nth-child(2) { animation-delay: -0.16s; }
.loading-dot:nth-child(3) { animation-delay: 0s; }
@keyframes bounce-dots {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 15px;
align-items: center;
}
.primary-action {
background: linear-gradient(135deg, #4CAF50, #66BB6A);
color: white;
padding: 15px 30px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
}
.primary-action:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
}
.secondary-info {
font-size: 14px;
color: #888;
text-align: center;
margin-top: 15px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #4facfe;
}
.report-link {
display: inline-block;
background: linear-gradient(135deg, #ff7e5f, #feb47b);
color: white;
padding: 12px 24px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
margin-top: 20px;
transition: all 0.3s;
}
.report-link:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 126, 95, 0.3);
}
</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" id="selectedTagDisplay">正在加载...</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 class="result-section" id="resultSection">
<div class="score-display">
<div class="success-icon">🎉</div>
<div class="score-value" id="scoreValue">0分</div>
<div class="score-label">测评完成!</div>
</div>
<div class="report-status-container">
<div id="reportStatus">
<div class="status-header">
<div class="status-icon"></div>
<div>
<div class="status-title">智能分析报告生成中</div>
<div class="status-message">系统正在基于您的答题情况AI正在深度分析并生成个性化学习建议...</div>
</div>
</div>
<div class="loading-indicator">
<div class="loading-dots">
<div class="loading-dot"></div>
<div class="loading-dot"></div>
<div class="loading-dot"></div>
</div>
<span style="margin-left: 15px; color: #4facfe; font-weight: 600;">AI正在分析中...</span>
</div>
<div class="action-buttons">
<a href="/quiz-results/{{session_id}}" class="primary-action">
📊 立即查看答题结果
</a>
<div class="secondary-info">
💡 您现在就可以查看详细的答题情况AI报告生成完成后将在这里显示
</div>
</div>
</div>
</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(`/api/questions/{{session_id}}`);
if (!response.ok) {
throw new Error('题目加载失败');
}
const data = await response.json();
this.questions = data.questions;
this.selectedTag = data.selectedTag;
this.questionsConfig = data.questionsConfig;
// 更新选题范围显示
const selectedTagDisplay = document.getElementById('selectedTagDisplay');
if (selectedTagDisplay) {
selectedTagDisplay.textContent = this.selectedTag || '全部题目';
}
if (this.questions.length === 0) {
throw new Error('没有可用的题目');
}
}
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 < 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.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) => {
// 提取完整的选项数据
const options = {};
const labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
for (const label of labels) {
// 遍历问题的所有键,寻找匹配的选项
for (const key in question) {
// 检查键是否包含当前选项字母(忽略空格数量)
if (key.replace(/\s+/g, '').includes(`选项${label}`)) {
options[label] = question[key];
break;
}
}
}
return {
questionId: question.questionId,
questionText: question['题干'],
questionType: question.questionType,
userAnswer: this.answers[index] || '',
correctAnswer: question['答案'],
score: parseInt(question['分数']) || 0,
options: options // 添加完整的选项数据
};
});
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);
this.checkReportStatus();
} else {
throw new Error('提交失败');
}
} catch (error) {
this.showError('提交失败: ' + error.message);
const submitBtn = document.querySelector('.submit-btn');
submitBtn.textContent = '提交答题';
submitBtn.disabled = false;
}
}
showResults(totalScore) {
document.getElementById('questionsContainer').style.display = 'none';
document.getElementById('resultSection').style.display = 'block';
// 获取总分信息并显示为"分数/总分"格式
this.fetchSessionInfo().then(sessionInfo => {
const maxScore = sessionInfo.maxScore || 100;
document.getElementById('scoreValue').innerHTML =
`<span style="font-size: 48px; font-weight: bold;">${totalScore}</span>` +
`<span style="font-size: 24px; opacity: 0.8;"> / ${maxScore}分</span>`;
}).catch(error => {
// 如果获取失败,使用默认值
document.getElementById('scoreValue').innerHTML =
`<span style="font-size: 48px; font-weight: bold;">${totalScore}</span>` +
`<span style="font-size: 24px; opacity: 0.8;"> / 100分</span>`;
});
this.checkReportStatus();
}
async fetchSessionInfo() {
try {
const response = await fetch(`/api/quiz-results/${this.sessionId}`);
if (response.ok) {
const result = await response.json();
if (result.success) {
return {
maxScore: result.maxScore,
totalQuestions: result.totalQuestions
};
}
}
// 如果API不可用返回默认值
return { maxScore: 100, totalQuestions: 14 };
} catch (error) {
console.warn('获取会话信息失败,使用默认值:', error);
return { maxScore: 100, totalQuestions: 14 };
}
}
checkReportStatus() {
// HTML已经包含完整的加载状态不需要在这里设置
// 继续检查报告生成状态
let checkCount = 0;
const maxChecks = 30;
const checkInterval = setInterval(async () => {
checkCount++;
try {
const response = await fetch('/api/reports');
const data = await response.json();
const latestReport = data.reports.find(r => r.session_id === this.sessionId);
if (latestReport) {
clearInterval(checkInterval);
this.showReportLink(latestReport.id);
} else if (checkCount >= maxChecks) {
clearInterval(checkInterval);
this.showTimeoutMessage();
}
} catch (error) {
console.error('检查报告状态失败:', error);
}
}, 2000);
}
showReportLink(reportId) {
document.getElementById('reportStatus').innerHTML =
`<div class="status-header">
<div class="status-icon">🎯</div>
<div>
<div class="status-title">测评完成!</div>
<div class="status-message">🎉 恭喜您完成测评AI已为您生成了个性化的学习建议报告。</div>
</div>
</div>
<div class="action-buttons">
<a href="/quiz-results/${this.sessionId}" class="primary-action">
📊 查看答题结果
</a>
<a href="/report.html?id=${reportId}" class="primary-action" style="background: linear-gradient(135deg, #ff7e5f, #feb47b); box-shadow: 0 4px 15px rgba(255, 126, 95, 0.3);">
🧠 查看AI智能报告
</a>
<div class="secondary-info">
💡 建议先查看答题结果了解具体错题再查看AI报告获得深度学习建议
</div>
</div>`;
}
showTimeoutMessage() {
document.getElementById('reportStatus').innerHTML =
`<div class="status-header">
<div class="status-icon">⏰</div>
<div>
<div class="status-title">报告生成时间较长</div>
<div class="status-message">当前用户较多AI报告正在加急处理中您可以先查看答题结果。</div>
</div>
</div>
<div class="action-buttons">
<a href="/quiz-results/${this.sessionId}" class="primary-action">
📊 查看答题结果
</a>
<div class="secondary-info">
⏱️ AI报告完成后可在首页报告列表中查看给您带来不便敬请谅解
</div>
</div>`;
}
showError(message) {
const container = document.getElementById('questionsContainer');
container.innerHTML = `<div style="text-align: center; padding: 50px; color: #e74c3c; background: #ffebee; border-radius: 8px; margin: 20px;">${message}</div>`;
}
}
window.quiz = new QuizSystem();
</script>
</body>
</html>