]> acesimba.cloud Git - words.git/commitdiff
codebuddy开始接手
authorroot <root@localhost.localdomain>
Sun, 7 Jun 2026 03:00:11 +0000 (11:00 +0800)
committerroot <root@localhost.localdomain>
Sun, 7 Jun 2026 03:00:11 +0000 (11:00 +0800)
frontend/src/components/StudyMode.css
frontend/src/components/StudyMode.js

index 76ad199a3b2c432540ac4124293448e337ea2e3f..af328dd6c8a906d89ad199d3bc73bbc8a1a2888f 100644 (file)
@@ -1,3 +1,5 @@
+/* /home/words/frontend/src/components/StudyMode.css */
+
 .study-container {
     display: flex;
     flex-direction: column;
@@ -16,6 +18,7 @@
     max-width: 1100px;
     min-height: 600px;
     overflow: hidden;
+    transition: all 0.3s ease; /* 增加平滑过渡 */
 }
 
 .image-section {
@@ -38,6 +41,7 @@
     padding: 40px;
     display: flex;
     flex-direction: column;
+    justify-content: center; /* 居中对齐内容 */
 }
 
 .word-text {
     margin-bottom: 10px;
 }
 
+.meaning-row {
+    margin-bottom: 20px;
+}
+
 .chinese {
     font-size: 28px;
     font-weight: 500;
     border-radius: 10px;
     font-size: 18px;
     cursor: pointer;
+    transition: background 0.2s;
+}
+
+.action-btn:hover {
+    background: #1e3a8a;
+}
+
+/* 闪记模式专用样式 */
+.flashcard-area {
+    text-align: center;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.flashcard-title {
+    font-size: 24px;
+    color: #64748b;
+    margin-bottom: 20px;
+}
+
+.flashcard-hint {
+    font-size: 36px;
+    color: #1e293b;
+    font-weight: bold;
+    margin-bottom: 30px;
 }
 
 input {
-    width: 100%;
+    width: 80%;
+    max-width: 400px;
     padding: 15px;
     font-size: 24px;
     border: 2px solid #ddd;
     border-radius: 10px;
     margin-top: 20px;
+    outline: none;
+}
+
+input:focus {
+    border-color: #1e40af;
+}
+
+.correction-area {
+    text-align: center;
+}
+
+.correction-area .error-tip { 
+    color: #dc2626; 
+    font-size: 28px;
+    margin-bottom: 20px;
 }
 
-.correction-area .error-tip { color: #dc2626; }
-.correction-area input.correct { border-color: #10b981; background: #ecfdf5; }
+.correction-area input.correct { 
+    border-color: #10b981; 
+    background: #ecfdf5; 
+}
 
 .back-home {
     margin-top: 20px;
@@ -98,3 +150,9 @@ input {
     text-decoration: underline;
     cursor: pointer;
 }
+
+.loading {
+    font-size: 24px;
+    margin-top: 100px;
+    color: #64748b;
+}
index a410ccf8f371afac6687cb9e2af2e172c7b75af4..463fab427b4712a1df4ddcecf41bc3885bb56f10 100644 (file)
@@ -1,4 +1,4 @@
-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';
@@ -13,38 +13,62 @@ function StudyMode() {
     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,
@@ -61,6 +85,11 @@ function StudyMode() {
         } else {
             setPhase('correction');
             setCorrectionInput("");
+            // 错误时读一遍正确发音提醒
+            const utterance = new SpeechSynthesisUtterance(currentWord.word);
+            utterance.lang = 'en-US';
+            utterance.rate = 0.6;
+            window.speechSynthesis.speak(utterance);
         }
     };
 
@@ -69,11 +98,14 @@ function StudyMode() {
     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>
@@ -85,25 +117,27 @@ function StudyMode() {
                                 <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">
@@ -114,10 +148,17 @@ function StudyMode() {
                                 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>
                     )}