-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
import './StudyMode.css';
const [phase, setPhase] = useState('learning'); // learning, flashcard, correction
const [userInput, setUserInput] = useState("");
const [correctionInput, setCorrectionInput] = useState("");
- const [showDetails, setShowDetails] = useState(false);
-
+
+ const hasSpokenRef = useRef(null);
const currentWord = words[currentIndex];
+ // 朗读逻辑:语速 0.6,间隔 500ms
const speak = useCallback((text) => {
- const utterance = new SpeechSynthesisUtterance(text);
- utterance.lang = 'en-US';
- utterance.rate = 0.8;
- window.speechSynthesis.speak(utterance);
+ window.speechSynthesis.cancel();
+
+ const utterance1 = new SpeechSynthesisUtterance(text);
+ utterance1.lang = 'en-US';
+ utterance1.rate = 0.6;
+
+ utterance1.onend = () => {
+ setTimeout(() => {
+ const utterance2 = new SpeechSynthesisUtterance(text);
+ utterance2.lang = 'en-US';
+ utterance2.rate = 0.6;
+ window.speechSynthesis.speak(utterance2);
+ }, 500);
+ };
+
+ window.speechSynthesis.speak(utterance1);
}, []);
+ // 获取单词数据
useEffect(() => {
axios.get(`${API_BASE}/api/words/${groupId}`).then(res => setWords(res.data));
}, [groupId]);
+ // 自动朗读逻辑
useEffect(() => {
if (currentWord && phase === 'learning') {
- setShowDetails(false);
- speak(currentWord.word);
- setTimeout(() => speak(currentWord.word), 1500);
- setTimeout(() => {
- setShowDetails(true);
+ if (hasSpokenRef.current !== currentWord.id) {
speak(currentWord.word);
- }, 3000);
+ hasSpokenRef.current = currentWord.id;
+ }
}
}, [currentIndex, currentWord, phase, speak]);
+ // 全局键盘监听:在学习模式下按回车进入闪记
+ useEffect(() => {
+ const handleGlobalKeyDown = (event) => {
+ if (phase === 'learning' && event.key === 'Enter') {
+ setPhase('flashcard');
+ }
+ };
+ window.addEventListener('keydown', handleGlobalKeyDown);
+ return () => window.removeEventListener('keydown', handleGlobalKeyDown);
+ }, [phase]);
+
const handleNext = async () => {
if (currentIndex < words.length - 1) {
setCurrentIndex(currentIndex + 1);
setPhase('learning');
setUserInput("");
+ setCorrectionInput("");
} else {
await axios.post(`${API_BASE}/api/progress/update`, {
group_id: groupId,
} else {
setPhase('correction');
setCorrectionInput("");
+ // 错误时读一遍正确发音提醒
+ const utterance = new SpeechSynthesisUtterance(currentWord.word);
+ utterance.lang = 'en-US';
+ utterance.rate = 0.6;
+ window.speechSynthesis.speak(utterance);
}
};
return (
<div className="study-container">
<div className="study-card">
- <div className="image-section">
- {currentWord.picture_id && (
- <img src={`${API_BASE}/api/picture/${currentWord.picture_id}`} alt="mindmap" />
- )}
- </div>
+ {/* 只有在非闪记模式下显示图片 */}
+ {phase !== 'flashcard' && (
+ <div className="image-section">
+ {currentWord.picture_id && (
+ <img src={`${API_BASE}/api/picture/${currentWord.picture_id}`} alt="mindmap" />
+ )}
+ </div>
+ )}
<div className="content-section">
<div className="progress-tag">Group {groupId}: {currentIndex + 1} / {words.length}</div>
<span className="chinese">{currentWord.chinese_meaning}</span>
<span className="pos">({currentWord.part_of_speech})</span>
</div>
- {showDetails && (
- <div className="details-anim">
- {currentWord.word_root && <p className="root">词根: {currentWord.word_root}</p>}
- {currentWord.affix && <p className="affix">组成: {currentWord.affix}</p>}
- <button className="action-btn" onClick={() => setPhase('flashcard')}>进入闪记模式</button>
- </div>
- )}
+
+ <div className="details-anim">
+ {currentWord.word_root && <p className="root"><strong>词根:</strong> {currentWord.word_root}</p>}
+ {currentWord.affix && <p className="affix"><strong>组成:</strong> {currentWord.affix}</p>}
+ <button className="action-btn" onClick={() => setPhase('flashcard')}>
+ 进入闪记模式 (Enter)
+ </button>
+ </div>
</div>
) : phase === 'flashcard' ? (
<div className="flashcard-area">
- <h2>请拼写单词</h2>
+ <h2 className="flashcard-title">闪记模式:请拼写单词</h2>
+ <p className="flashcard-hint">中文含义:{currentWord.chinese_meaning}</p>
<input
autoFocus
value={userInput}
onChange={(e) => setUserInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && checkFlashcard()}
- placeholder="输入英文..."
+ placeholder="输入英文单词..."
/>
- <button onClick={checkFlashcard}>检查</button>
+ <button className="action-btn" onClick={checkFlashcard}>检查 (Enter)</button>
</div>
) : (
<div className="correction-area">
className={correctionInput === currentWord.word ? "correct" : "typing"}
value={correctionInput}
onChange={(e) => setCorrectionInput(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && correctionInput === currentWord.word) {
+ setPhase('flashcard');
+ }
+ }}
placeholder="请在此抄写..."
/>
{correctionInput === currentWord.word && (
- <button className="action-btn" onClick={() => setPhase('flashcard')}>重新尝试</button>
+ <button className="action-btn" onClick={() => setPhase('flashcard')}>
+ 重新尝试 (Enter)
+ </button>
)}
</div>
)}