505 lines
20 KiB
JavaScript
505 lines
20 KiB
JavaScript
// 题目数据数组
|
||
const questions = [
|
||
// 简单难度题目 (5道)
|
||
{
|
||
difficulty: 'easy',
|
||
text: '下列哪个物理量是矢量?',
|
||
options: ['A. 质量', 'B. 时间', 'C. 位移', 'D. 路程'],
|
||
answer: 'C',
|
||
explanation: '矢量具有大小和方向,位移是矢量,其他选项均为标量。'
|
||
},
|
||
{
|
||
difficulty: 'easy',
|
||
text: '下列哪个物理量是矢量?',
|
||
options: ['A. 质量', 'B. 时间', 'C. 位移', 'D. 路程'],
|
||
answer: 'C',
|
||
explanation: '矢量具有大小和方向,位移是矢量,其他选项均为标量。'
|
||
},
|
||
{
|
||
difficulty: 'easy',
|
||
text: '下列哪个物理量是矢量?',
|
||
options: ['A. 质量', 'B. 时间', 'C. 位移', 'D. 路程'],
|
||
answer: 'C',
|
||
explanation: '矢量具有大小和方向,位移是矢量,其他选项均为标量。'
|
||
},
|
||
{
|
||
difficulty: 'easy',
|
||
text: '下列哪个物理量是矢量?',
|
||
options: ['A. 质量', 'B. 时间', 'C. 位移', 'D. 路程'],
|
||
answer: 'C',
|
||
explanation: '矢量具有大小和方向,位移是矢量,其他选项均为标量。'
|
||
},
|
||
{
|
||
difficulty: 'easy',
|
||
text: '下列哪个物理量是矢量?',
|
||
options: ['A. 质量', 'B. 时间', 'C. 位移', 'D. 路程'],
|
||
answer: 'C',
|
||
explanation: '矢量具有大小和方向,位移是矢量,其他选项均为标量。'
|
||
},
|
||
// 中等难度题目 (5道)
|
||
{
|
||
difficulty: 'medium',
|
||
text: '关于万有引力定律,下列说法正确的是?',
|
||
options: [
|
||
'A. 万有引力只存在于天体之间',
|
||
'B. 万有引力与物体质量成正比,与距离成反比',
|
||
'C. 万有引力的发现者是牛顿',
|
||
'D. 地球对苹果的引力大于苹果对地球的引力'
|
||
],
|
||
answer: 'C',
|
||
explanation: '万有引力存在于任何有质量的物体之间,A错误;万有引力与距离的平方成反比,B错误;牛顿发现了万有引力定律,C正确;物体间的引力是相互的,大小相等,D错误。'
|
||
},
|
||
{
|
||
difficulty: 'medium',
|
||
text: '关于万有引力定律,下列说法正确的是?',
|
||
options: [
|
||
'A. 万有引力只存在于天体之间',
|
||
'B. 万有引力与物体质量成正比,与距离成反比',
|
||
'C. 万有引力的发现者是牛顿',
|
||
'D. 地球对苹果的引力大于苹果对地球的引力'
|
||
],
|
||
answer: 'C',
|
||
explanation: '万有引力存在于任何有质量的物体之间,A错误;万有引力与距离的平方成反比,B错误;牛顿发现了万有引力定律,C正确;物体间的引力是相互的,大小相等,D错误。'
|
||
},
|
||
{
|
||
difficulty: 'medium',
|
||
text: '关于万有引力定律,下列说法正确的是?',
|
||
options: [
|
||
'A. 万有引力只存在于天体之间',
|
||
'B. 万有引力与物体质量成正比,与距离成反比',
|
||
'C. 万有引力的发现者是牛顿',
|
||
'D. 地球对苹果的引力大于苹果对地球的引力'
|
||
],
|
||
answer: 'C',
|
||
explanation: '万有引力存在于任何有质量的物体之间,A错误;万有引力与距离的平方成反比,B错误;牛顿发现了万有引力定律,C正确;物体间的引力是相互的,大小相等,D错误。'
|
||
},
|
||
{
|
||
difficulty: 'medium',
|
||
text: '关于万有引力定律,下列说法正确的是?',
|
||
options: [
|
||
'A. 万有引力只存在于天体之间',
|
||
'B. 万有引力与物体质量成正比,与距离成反比',
|
||
'C. 万有引力的发现者是牛顿',
|
||
'D. 地球对苹果的引力大于苹果对地球的引力'
|
||
],
|
||
answer: 'C',
|
||
explanation: '万有引力存在于任何有质量的物体之间,A错误;万有引力与距离的平方成反比,B错误;牛顿发现了万有引力定律,C正确;物体间的引力是相互的,大小相等,D错误。'
|
||
},
|
||
{
|
||
difficulty: 'medium',
|
||
text: '关于万有引力定律,下列说法正确的是?',
|
||
options: [
|
||
'A. 万有引力只存在于天体之间',
|
||
'B. 万有引力与物体质量成正比,与距离成反比',
|
||
'C. 万有引力的发现者是牛顿',
|
||
'D. 地球对苹果的引力大于苹果对地球的引力'
|
||
],
|
||
answer: 'C',
|
||
explanation: '万有引力存在于任何有质量的物体之间,A错误;万有引力与距离的平方成反比,B错误;牛顿发现了万有引力定律,C正确;物体间的引力是相互的,大小相等,D错误。'
|
||
},
|
||
// 困难难度题目 (5道)
|
||
{
|
||
difficulty: 'hard',
|
||
text: '相对论中质量与能量的关系表达式是?',
|
||
options: ['A. E=mc²', 'B. F=ma', 'C. E=hv', 'D. P=mv'],
|
||
answer: 'A',
|
||
explanation: '爱因斯坦的质能方程E=mc²表明质量和能量可以相互转换。'
|
||
},
|
||
{
|
||
difficulty: 'hard',
|
||
text: '相对论中质量与能量的关系表达式是?',
|
||
options: ['A. E=mc²', 'B. F=ma', 'C. E=hv', 'D. P=mv'],
|
||
answer: 'A',
|
||
explanation: '爱因斯坦的质能方程E=mc²表明质量和能量可以相互转换。'
|
||
},
|
||
{
|
||
difficulty: 'hard',
|
||
text: '相对论中质量与能量的关系表达式是?',
|
||
options: ['A. E=mc²', 'B. F=ma', 'C. E=hv', 'D. P=mv'],
|
||
answer: 'A',
|
||
explanation: '爱因斯坦的质能方程E=mc²表明质量和能量可以相互转换。'
|
||
},
|
||
{
|
||
difficulty: 'hard',
|
||
text: '相对论中质量与能量的关系表达式是?',
|
||
options: ['A. E=mc²', 'B. F=ma', 'C. E=hv', 'D. P=mv'],
|
||
answer: 'A',
|
||
explanation: '爱因斯坦的质能方程E=mc²表明质量和能量可以相互转换。'
|
||
},
|
||
{
|
||
difficulty: 'hard',
|
||
text: '相对论中质量与能量的关系表达式是?',
|
||
options: ['A. E=mc²', 'B. F=ma', 'C. E=hv', 'D. P=mv'],
|
||
answer: 'A',
|
||
explanation: '爱因斯坦的质能方程E=mc²表明质量和能量可以相互转换。'
|
||
},
|
||
];
|
||
|
||
// 当前难度级别,初始为中等
|
||
let currentDifficulty = 'medium';
|
||
// 已完成的难度
|
||
const completedDifficulties = [];
|
||
// 各难度得分情况
|
||
const scores = {
|
||
easy: { score: 0, correct: 0, incorrect: 0 },
|
||
medium: { score: 0, correct: 0, incorrect: 0 },
|
||
hard: { score: 0, correct: 0, incorrect: 0 }
|
||
};
|
||
|
||
// 学伴功能相关变量
|
||
let mediaRecorder; let audioChunks = []; let isRecording = false;
|
||
let audioElement = null; let isPlaying = false;
|
||
let currentQuestion = null;
|
||
|
||
// DOM元素
|
||
const questionContainer = document.getElementById('question-container');
|
||
const submitBtn = document.getElementById('submit-btn');
|
||
const resultSection = document.getElementById('result-section');
|
||
const totalScoreElement = document.getElementById('total-score');
|
||
const correctCountElement = document.getElementById('correct-count');
|
||
const incorrectCountElement = document.getElementById('incorrect-count');
|
||
const navigationSection = document.getElementById('navigation-section');
|
||
const navigationMessage = document.getElementById('navigation-message');
|
||
const nextBtn = document.getElementById('next-btn');
|
||
const difficultyIndicator = document.querySelector('.difficulty-indicator');
|
||
// 学伴功能DOM元素
|
||
const recordingIndicator = document.getElementById('recordingIndicator');
|
||
const thinkingIndicator = document.getElementById('thinkingIndicator');
|
||
const resultContainer = document.getElementById('resultContainer');
|
||
const asrResultText = document.getElementById('asrResultText');
|
||
const feedbackResultText = document.getElementById('feedbackResultText');
|
||
const playAudioBtn = document.getElementById('playAudioBtn');
|
||
const audioProgress = document.getElementById('audioProgress');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const audioTime = document.getElementById('audioTime');
|
||
|
||
// 初始化页面
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
renderQuestions(currentDifficulty);
|
||
// 绑定学伴答疑按钮事件
|
||
document.addEventListener('click', function(e) {
|
||
if (e.target && e.target.classList.contains('ask-xueban-btn')) {
|
||
currentQuestion = {
|
||
number: e.target.dataset.question,
|
||
difficulty: e.target.dataset.difficulty
|
||
};
|
||
startRecording();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 渲染指定难度的题目
|
||
function renderQuestions(difficulty) {
|
||
questionContainer.innerHTML = '';
|
||
|
||
// 筛选当前难度的题目
|
||
const filteredQuestions = questions.filter(q => q.difficulty === difficulty);
|
||
|
||
// 生成题目HTML
|
||
filteredQuestions.forEach((q, index) => {
|
||
const questionNumber = index + 1;
|
||
const questionElement = document.createElement('div');
|
||
questionElement.className = 'question';
|
||
questionElement.dataset.difficulty = q.difficulty;
|
||
questionElement.dataset.question = questionNumber;
|
||
|
||
questionElement.innerHTML = `
|
||
<div class="question-header">
|
||
<div class="question-number">${getDifficultyName(q.difficulty)}难度 - 问题 ${questionNumber}</div>
|
||
<div class="question-points">20分</div>
|
||
</div>
|
||
<p class="question-text">${q.text}</p>
|
||
<div class="options">
|
||
${q.options.map((option, optIndex) => {
|
||
const optionLetter = String.fromCharCode(65 + optIndex);
|
||
return `
|
||
<div class="option">
|
||
<input type="radio" name="q${q.difficulty}-${questionNumber}" id="q${q.difficulty}-${questionNumber}-${optionLetter.toLowerCase()}" value="${optionLetter}">
|
||
<label for="q${q.difficulty}-${questionNumber}-${optionLetter.toLowerCase()}">${option}</label>
|
||
</div>
|
||
`;
|
||
}).join('')}
|
||
</div>
|
||
<div class="explanation" id="explanation-${q.difficulty}-${questionNumber}">${q.explanation}</div>
|
||
<button class="ask-xueban-btn hidden" data-question="${questionNumber}" data-difficulty="${q.difficulty}">学伴答疑</button>
|
||
`;
|
||
|
||
questionContainer.appendChild(questionElement);
|
||
});
|
||
}
|
||
|
||
// 获取难度名称
|
||
function getDifficultyName(difficulty) {
|
||
const names = {
|
||
easy: '简单',
|
||
medium: '中等',
|
||
hard: '高级'
|
||
};
|
||
return names[difficulty] || difficulty;
|
||
}
|
||
|
||
// 提交答案
|
||
submitBtn.addEventListener('click', () => {
|
||
checkAnswers();
|
||
submitBtn.disabled = true;
|
||
submitBtn.textContent = '已提交';
|
||
});
|
||
|
||
// 检查答案
|
||
function checkAnswers() {
|
||
let correctCount = 0;
|
||
let incorrectCount = 0;
|
||
let totalScore = 0;
|
||
|
||
// 获取当前难度的题目
|
||
const currentQuestions = questions.filter(q => q.difficulty === currentDifficulty);
|
||
|
||
// 检查每道题的答案
|
||
currentQuestions.forEach((q, index) => {
|
||
const questionNumber = index + 1;
|
||
const selectedOption = document.querySelector(`input[name="q${q.difficulty}-${questionNumber}"]:checked`);
|
||
const explanationElement = document.getElementById(`explanation-${q.difficulty}-${questionNumber}`);
|
||
const optionsContainer = document.querySelector(`div[data-difficulty="${q.difficulty}"][data-question="${questionNumber}"] .options`);
|
||
const allOptions = optionsContainer.querySelectorAll('.option');
|
||
|
||
// 显示解析
|
||
explanationElement.classList.add('show');
|
||
|
||
if (!selectedOption) {
|
||
// 未答题
|
||
optionsContainer.style.border = '2px solid #f39c12';
|
||
return;
|
||
}
|
||
|
||
// 标记正确或错误
|
||
const selectedValue = selectedOption.value;
|
||
if (selectedValue === q.answer) {
|
||
// 正确
|
||
selectedOption.closest('.option').classList.add('correct');
|
||
correctCount++;
|
||
totalScore += 20;
|
||
} else {
|
||
// 错误
|
||
selectedOption.closest('.option').classList.add('incorrect');
|
||
// 标记正确答案
|
||
allOptions.forEach(option => {
|
||
if (option.querySelector(`input[value="${q.answer}"]`)) {
|
||
option.classList.add('correct');
|
||
}
|
||
});
|
||
incorrectCount++;
|
||
}
|
||
});
|
||
|
||
// 更新当前难度得分
|
||
scores[currentDifficulty] = {
|
||
score: totalScore,
|
||
correct: correctCount,
|
||
incorrect: incorrectCount
|
||
};
|
||
|
||
// 添加到已完成难度
|
||
if (!completedDifficulties.includes(currentDifficulty)) {
|
||
completedDifficulties.push(currentDifficulty);
|
||
}
|
||
|
||
// 显示结果
|
||
showResults();
|
||
|
||
// 显示下一步建议
|
||
showNextRecommendation();
|
||
}
|
||
|
||
// 显示结果
|
||
function showResults() {
|
||
resultSection.style.display = 'block';
|
||
|
||
// 计算总得分
|
||
let totalScore = 0;
|
||
let totalCorrect = 0;
|
||
let totalIncorrect = 0;
|
||
|
||
completedDifficulties.forEach(difficulty => {
|
||
totalScore += scores[difficulty].score;
|
||
totalCorrect += scores[difficulty].correct;
|
||
totalIncorrect += scores[difficulty].incorrect;
|
||
});
|
||
|
||
// 更新结果显示
|
||
totalScoreElement.textContent = totalScore;
|
||
correctCountElement.textContent = totalCorrect;
|
||
incorrectCountElement.textContent = totalIncorrect;
|
||
|
||
// 滚动到结果区域
|
||
resultSection.scrollIntoView({ behavior: 'smooth' });
|
||
}
|
||
|
||
// 显示下一步建议
|
||
function showNextRecommendation() {
|
||
navigationSection.style.display = 'block';
|
||
const currentScore = scores[currentDifficulty];
|
||
|
||
if (currentDifficulty === 'medium') {
|
||
if (currentScore.correct >= 3) {
|
||
navigationMessage.textContent = `恭喜!您在${getDifficultyName(currentDifficulty)}难度中答对了${currentScore.correct}题,表现优秀,建议挑战高级难度。`;
|
||
nextBtn.onclick = () => switchDifficulty('hard');
|
||
} else {
|
||
navigationMessage.textContent = `您在${getDifficultyName(currentDifficulty)}难度中答对了${currentScore.correct}题,建议先尝试简单难度巩固基础。`;
|
||
nextBtn.onclick = () => switchDifficulty('easy');
|
||
}
|
||
} else if (currentDifficulty === 'easy') {
|
||
navigationMessage.textContent = `您已完成${getDifficultyName(currentDifficulty)}难度,建议继续挑战中等难度。`;
|
||
nextBtn.onclick = () => switchDifficulty('medium');
|
||
} else if (currentDifficulty === 'hard') {
|
||
navigationMessage.textContent = `恭喜您完成了所有难度级别!您的总分为${scores.easy.score + scores.medium.score + scores.hard.score}分。`;
|
||
nextBtn.style.display = 'none';
|
||
}
|
||
|
||
navigationSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
}
|
||
|
||
// 切换难度
|
||
function switchDifficulty(nextDifficulty) {
|
||
currentDifficulty = nextDifficulty;
|
||
difficultyIndicator.textContent = `当前难度:${getDifficultyName(nextDifficulty)}`;
|
||
difficultyIndicator.className = `difficulty-indicator difficulty-${nextDifficulty}`;
|
||
|
||
// 重置按钮状态
|
||
submitBtn.disabled = false;
|
||
submitBtn.textContent = '提交答案';
|
||
|
||
// 隐藏结果和导航区域
|
||
resultSection.style.display = 'none';
|
||
navigationSection.style.display = 'none';
|
||
|
||
// 渲染新难度题目
|
||
renderQuestions(nextDifficulty);
|
||
|
||
// 滚动到页面顶部
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
// 学伴功能 - 开始录音
|
||
function startRecording() {
|
||
if (isRecording) return;
|
||
|
||
navigator.mediaDevices.getUserMedia({ audio: true })
|
||
.then(stream => {
|
||
mediaRecorder = new MediaRecorder(stream);
|
||
audioChunks = [];
|
||
|
||
mediaRecorder.ondataavailable = event => {
|
||
if (event.data.size > 0) audioChunks.push(event.data);
|
||
};
|
||
|
||
mediaRecorder.onstop = () => {
|
||
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
||
uploadAudioToServer(audioBlob);
|
||
};
|
||
|
||
mediaRecorder.start();
|
||
isRecording = true;
|
||
recordingIndicator.style.display = 'flex';
|
||
|
||
// 设置最长录音时间为60秒
|
||
setTimeout(stopRecording, 60000);
|
||
})
|
||
.catch(error => {
|
||
console.error("获取麦克风权限失败:", error);
|
||
alert("请授权麦克风权限以使用录音功能");
|
||
});
|
||
}
|
||
|
||
// 学伴功能 - 停止录音
|
||
function stopRecording() {
|
||
if (!isRecording || !mediaRecorder) return;
|
||
|
||
mediaRecorder.stop();
|
||
isRecording = false;
|
||
recordingIndicator.style.display = 'none';
|
||
|
||
if (mediaRecorder.stream) {
|
||
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||
}
|
||
}
|
||
|
||
// 学伴功能 - 上传音频到服务器
|
||
function uploadAudioToServer(audioBlob) {
|
||
thinkingIndicator.style.display = 'flex';
|
||
resultContainer.style.display = 'none';
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', audioBlob, 'recording.wav');
|
||
if (currentQuestion) {
|
||
formData.append('question_number', currentQuestion.number);
|
||
formData.append('difficulty', currentQuestion.difficulty);
|
||
}
|
||
|
||
fetch('/api/xueban/upload-audio', { method: 'POST', body: formData })
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
thinkingIndicator.style.display = 'none';
|
||
if (data.success) {
|
||
asrResultText.textContent = data.asr_text || '未识别到内容';
|
||
feedbackResultText.textContent = data.feedback_text || '无反馈内容';
|
||
resultContainer.style.display = 'flex';
|
||
|
||
if (data.audio_url) {
|
||
audioElement = new Audio(data.audio_url);
|
||
audioElement.onloadedmetadata = function() { updateAudioTimeDisplay(); };
|
||
audioElement.ontimeupdate = function() { updateAudioProgress(); updateAudioTimeDisplay(); };
|
||
audioElement.onended = function() { isPlaying = false; updatePlayButton(); };
|
||
playAudioBtn.onclick = togglePlayAudio;
|
||
audioProgress.onclick = function(e) {
|
||
const rect = audioProgress.getBoundingClientRect();
|
||
const clickPosition = (e.clientX - rect.left) / rect.width;
|
||
audioElement.currentTime = clickPosition * audioElement.duration;
|
||
};
|
||
}
|
||
} else {
|
||
alert('音频处理失败: ' + data.message);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
thinkingIndicator.style.display = 'none';
|
||
alert('上传音频失败: ' + error.message);
|
||
});
|
||
}
|
||
|
||
// 学伴功能 - 切换音频播放/暂停
|
||
function togglePlayAudio() {
|
||
if (!audioElement) return;
|
||
isPlaying ? audioElement.pause() : audioElement.play();
|
||
isPlaying = !isPlaying;
|
||
updatePlayButton();
|
||
}
|
||
|
||
// 学伴功能 - 更新播放按钮状态
|
||
function updatePlayButton() {
|
||
playAudioBtn.innerHTML = isPlaying ?
|
||
`<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M6 19H10V5H6V19ZM14 19H18V5H14V19Z" fill="white"/></svg>` :
|
||
`<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M8 5V19L19 12L8 5Z" fill="white"/></svg>`;
|
||
}
|
||
|
||
// 学伴功能 - 更新音频进度条
|
||
function updateAudioProgress() {
|
||
if (audioElement && audioElement.duration) {
|
||
progressBar.style.width = `${(audioElement.currentTime / audioElement.duration) * 100}%`;
|
||
}
|
||
}
|
||
|
||
// 学伴功能 - 更新音频时间显示
|
||
function updateAudioTimeDisplay() {
|
||
if (audioElement && audioElement.duration) {
|
||
const currentTime = formatTime(audioElement.currentTime);
|
||
const duration = formatTime(audioElement.duration);
|
||
audioTime.textContent = `${currentTime} / ${duration}`;
|
||
}
|
||
}
|
||
|
||
// 学伴功能 - 格式化时间为 MM:SS
|
||
function formatTime(seconds) {
|
||
const mins = Math.floor(seconds / 60);
|
||
const secs = Math.floor(seconds % 60);
|
||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||
} |