]> acesimba.cloud Git - words.git/commitdiff
refactor(v2): 测试/复习改为三阶段顺序(拼写→选词性→句子填空)
authorCodeBuddy <codebuddy@local>
Thu, 20 Aug 2026 23:19:45 +0000 (07:19 +0800)
committerCodeBuddy <codebuddy@local>
Thu, 20 Aug 2026 23:19:45 +0000 (07:19 +0800)
- 重点/拼写词整体在最前:阶段①拼写默写 → 阶段②识别词选词性 → 阶段③句子填空
- 句子挖空词取自重点+非重点全部词(现有 example_sentence),干扰项取自同组词
- 各阶段选错均重排并换掉错误混淆项,直到全对才记录完成
- 第2条(中文词义混合词性选择)、第3条(ChatGPT 例句)本次暂不实现

frontend/src/components/TestMode.js

index 5864b511c840ffc2068607a9441faf938ba98add..d9f8fdae3fab6eef65cc05ce8d7066375f50c3db 100644 (file)
@@ -22,18 +22,17 @@ function escapeRegExp(s) {
     return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
 }
 
-// 在例句中把目标词替换为空格(用于句子填空选择题)。找不到目标词则返回原句。
+// 在例句中把目标词替换为空格(用于句子填空选择题)
 function blankWordInSentence(sentence, word) {
     if (!sentence || !word) return sentence || '';
     const re = new RegExp('\\b' + escapeRegExp(word) + '\\b', 'ig');
     return sentence.replace(re, '______');
 }
 
-// ä¸ºæ\9f\90è¯\8dç\94\9fæ\88\90 4 ä¸ªè¯\8dæ\80§é\80\89项ï¼\88å\90«æ­£ç¡®é¡¹ï¼\89ã\80\82avoidSet ä¸­ç\9a\84é\94\99误项å°\86被æ\9b¿æ\8d¢æ\8e\89ï¼\88é\9c\80æ±\82 5ï¼\9aæ\8d¢æ\8e\89é\94\99误混æ·\86项并重新排序)
+// ä¸ºæ\9f\90è¯\8dç\94\9fæ\88\90 4 ä¸ªè¯\8dæ\80§é\80\89项ï¼\88å\90«æ­£ç¡®é¡¹ï¼\89ã\80\82avoidSet ä¸­ç\9a\84é\94\99误项å°\86被æ\9b¿æ\8d¢æ\8e\89ï¼\88é\80\89é\94\99æ\8d¢项并重新排序)
 function buildPosOptions(word, posPool, avoidSet) {
     const correct = (word.part_of_speech || '').trim();
     let distractors = shuffle(posPool.filter(p => p && p !== correct && !avoidSet.has(p))).slice(0, 3);
-    // 干扰项不足 3 个时,放宽到全池补充(不含正确项、不重复)
     if (distractors.length < 3) {
         const backup = shuffle(posPool.filter(p => p && p !== correct && !distractors.includes(p)));
         for (const p of backup) {
@@ -44,7 +43,7 @@ function buildPosOptions(word, posPool, avoidSet) {
     return shuffle([correct, ...distractors]);
 }
 
-// 为句子填空生成选项:正确词 + 同组非背诵干扰词。avoidSet 中的错误项将被替换
+// 为句子填空生成选项:正确词 + 同组干扰词。avoidSet 中的错误项将被替换
 function buildSentenceOptions(target, candidateWords, avoidSet) {
     const correct = target.word;
     let distractors = shuffle(
@@ -67,51 +66,44 @@ function normZh(s) {
     return (s || '').toLowerCase().replace(/[\s,。、;:?!.,;:?!'"()()]/g, '');
 }
 
-function getCategories(words) {
-    const spell = words.filter(w => (w.memory_type || 'spell') === 'spell');
-    const recognize = words.filter(w => (w.memory_type || '') === 'recognize');
-    const recite = recognize.filter(w => w.need_recite === 1);            // 非重点但需背诵 → 默写
-    const other = recognize.filter(w => w.need_recite !== 1 && (w.part_of_speech || '').trim()); // 其余 → 选词性+回忆中文
-    return { spell, recognize, recite, other };
-}
-
-// 句子填空候选:非背诵且例句包含该词(最多 5 个,best-effort 待校对)
-function buildSentenceCandidates(other) {
-    const candidates = other.filter(w => {
+// 例句可作为句子填空候选:例句包含该词(重点/非重点都算,GPT 例句后续替换)
+function sentenceCandidates(words) {
+    const cands = words.filter(w => {
         const s = (w.example_sentence || '');
         return s && (w.word || '') && s.toLowerCase().includes((w.word || '').toLowerCase());
     });
-    return shuffle(candidates).slice(0, 5);
+    return shuffle(cands).slice(0, 5);
 }
 
 function TestMode() {
     const { groupId } = useParams();
     const navigate = useNavigate();
     const [searchParams] = useSearchParams();
-    // 活动类型:test / review1 / review2 / review3(首页点击复习时通过 ?type= 传入;缺省为 test)
+    // 活动类型:test / review1 / review2 / review3
     const activityType = searchParams.get('type') || 'test';
     const isOnline = useOnlineStatus();
-    // 测试/复习会话开始时间(毫秒时间戳), 用于计算本次学习的实际时长
     const sessionStartRef = useRef(Date.now());
     const [words, setWords] = useState([]);
-    const [phase, setPhase] = useState('ready'); // ready, word, word_fb, copy, sentence, sentence_fb, complete
+    // 三阶段:spell(默写) → pos(选词性) → sentence(句子选词)
+    const [phase, setPhase] = useState('ready');
 
-    // 单词环节队列(默写 / 选词性)
-    const [wordQueue, setWordQueue] = useState([]);
-    const [wordIndex, setWordIndex] = useState(0);
-    const [wrongWords, setWrongWords] = useState([]);
+    const [spellQueue, setSpellQueue] = useState([]);
+    const [spellIndex, setSpellIndex] = useState(0);
+    const [wrongSpell, setWrongSpell] = useState([]);
+
+    const [posQueue, setPosQueue] = useState([]);
+    const [posIndex, setPosIndex] = useState(0);
+    const [wrongPos, setWrongPos] = useState([]);
 
-    // 句子填空环节队列
     const [sentenceQueue, setSentenceQueue] = useState([]);
     const [sentenceIndex, setSentenceIndex] = useState(0);
     const [wrongSentences, setWrongSentences] = useState([]);
 
-    // 交互状态
     const [userInput, setUserInput] = useState('');
     const [copyInput, setCopyInput] = useState('');
     const [posChoice, setPosChoice] = useState('');
     const [chineseRecall, setChineseRecall] = useState('');
-    const [compareResult, setCompareResult] = useState(null); // true/false/null
+    const [compareResult, setCompareResult] = useState(null);
     const [isCorrect, setIsCorrect] = useState(false);
     const [sentenceChoice, setSentenceChoice] = useState('');
 
@@ -119,14 +111,14 @@ function TestMode() {
     const copyRef = useRef(null);
     const chineseRef = useRef(null);
     const submittedRef = useRef(false);
-
-    // 构建环节所需的派生数据(存 ref,供重考时重新生成选项)
     const posPoolRef = useRef([]);
-    const otherRecognizeRef = useRef([]);
 
-    const current = phase === 'sentence' || phase === 'sentence_fb'
-        ? sentenceQueue[sentenceIndex]
-        : wordQueue[wordIndex];
+    const current = (() => {
+        if (phase === 'spell' || phase === 'spell_fb' || phase === 'copy') return spellQueue[spellIndex];
+        if (phase === 'pos' || phase === 'pos_fb') return posQueue[posIndex];
+        if (phase === 'sentence' || phase === 'sentence_fb') return sentenceQueue[sentenceIndex];
+        return null;
+    })();
 
     // 离线时检查可学习范围
     useEffect(() => {
@@ -139,7 +131,7 @@ function TestMode() {
         });
     }, [isOnline, groupId, navigate]);
 
-    // 获取单词数据 (在线走网络, 离线降级读 IndexedDB)
+    // 获取单词数据
     useEffect(() => {
         api.getWords(groupId)
             .then(res => setWords(res.data))
@@ -149,7 +141,7 @@ function TestMode() {
             });
     }, [groupId]);
 
-    // 清空所有交互输入
+    // 清空交互输入
     const resetInputs = useCallback(() => {
         setUserInput('');
         setCopyInput('');
@@ -160,59 +152,57 @@ function TestMode() {
         setSentenceChoice('');
     }, []);
 
-    // 开始测试:构建三类环节
+    // 开始:构建三阶段队列(重点词拼写最先,其次识别词选词性,最后句子填空)
     const startTest = useCallback(() => {
-        const { spell, recite, other } = getCategories(words);
+        const spell = words.filter(w => (w.memory_type || 'spell') === 'spell');
+        const recognize = words.filter(w => (w.memory_type || '') === 'recognize');
         const posPool = Array.from(new Set([
             ...words.map(w => (w.part_of_speech || '').trim()).filter(Boolean),
             ...UNIVERSAL_POS,
         ]));
         posPoolRef.current = posPool;
-        otherRecognizeRef.current = other;
 
-        const dictationItems = [...spell, ...recite].map(w => ({ ...w, mode: 'dictation' }));
-        const posItems = other.map(w => ({
+        const spellItems = shuffle(spell.map(w => ({ ...w, mode: 'dictation' })));
+        const posItems = shuffle(recognize.map(w => ({
             ...w,
             mode: 'pos',
             posOptions: buildPosOptions(w, posPool, new Set()),
             avoidPos: new Set(),
-        }));
-        const wordQ = shuffle([...dictationItems, ...posItems]);
+        })));
 
-        const sentCands = buildSentenceCandidates(other);
+        const sentCands = sentenceCandidates(words);
         const sentQ = sentCands.map(w => ({
             id: w.id,
             target: w,
             blanked: blankWordInSentence(w.example_sentence, w.word),
             answer: w.word,
-            options: buildSentenceOptions(w, other, new Set()),
+            options: buildSentenceOptions(w, words, new Set()),
             avoidWord: new Set(),
         })).filter(s => s.options.length >= 2);
 
-        setWordQueue(wordQ);
-        setWrongWords([]);
-        setWordIndex(0);
+        setSpellQueue(spellItems);
+        setWrongSpell([]);
+        setSpellIndex(0);
+        setPosQueue(posItems);
+        setWrongPos([]);
+        setPosIndex(0);
         setSentenceQueue(sentQ);
         setWrongSentences([]);
         setSentenceIndex(0);
         resetInputs();
-        setPhase('word');
+        setPhase('spell');
     }, [words, resetInputs]);
 
     // 自动聚焦
     useEffect(() => {
-        if (phase === 'word' && current && current.mode === 'dictation' && inputRef.current) {
+        if (phase === 'spell' && current && current.mode === 'dictation' && inputRef.current) {
             inputRef.current.focus();
         }
         if (phase === 'copy' && copyRef.current) {
             copyRef.current.focus();
         }
-        if (phase === 'word' && current && current.mode === 'pos' && chineseRef.current) {
-            // pos 选择后才有中文输入框,这里暂不聚焦
-        }
     }, [phase, current]);
 
-    // 朗读:通过后端 TTS 代理播放
     const speak = useCallback((text) => {
         if (!text) return;
         const audio = new Audio(api.ttsUrl(text));
@@ -226,10 +216,10 @@ function TestMode() {
         const answer = userInput.trim().toLowerCase();
         const ok = answer === correct;
         setIsCorrect(ok);
-        setPhase('word_fb');
+        setPhase('spell_fb');
         speak(current.word);
         if (!ok) {
-            setWrongWords(prev => prev.find(w => w.id === current.id) ? prev : [...prev, current]);
+            setWrongSpell(prev => prev.find(w => w.id === current.id) ? prev : [...prev, current]);
         }
     }, [current, userInput, speak]);
 
@@ -240,11 +230,11 @@ function TestMode() {
         const ok = opt === correct;
         setPosChoice(opt);
         setIsCorrect(ok);
-        setPhase('word_fb');
+        setPhase('pos_fb');
         speak(current.word);
         if (!ok) {
-            current.avoidPos.add(opt); // 记录错误混淆项,重考时换掉(需求 5)
-            setWrongWords(prev => prev.find(w => w.id === current.id) ? prev : [...prev, current]);
+            current.avoidPos.add(opt);
+            setWrongPos(prev => prev.find(w => w.id === current.id) ? prev : [...prev, current]);
         }
     }, [current, speak]);
 
@@ -256,42 +246,11 @@ function TestMode() {
         setCompareResult(!!(user && (correct.includes(user) || user.includes(correct))));
     }, [current, chineseRecall]);
 
-    // 显示抄写区(默写错误后纠正)
     const showCopy = useCallback(() => {
         setCopyInput('');
         setPhase('copy');
     }, []);
 
-    // 进入下一词
-    const nextWord = useCallback(() => {
-        if (wordIndex + 1 < wordQueue.length) {
-            setWordIndex(prev => prev + 1);
-            resetInputs();
-            setPhase('word');
-        } else {
-            // 本轮结束
-            if (wrongWords.length > 0) {
-                const rq = shuffle(wrongWords.map(w => w.mode === 'pos'
-                    ? { ...w, posOptions: buildPosOptions(w, posPoolRef.current, w.avoidPos) }
-                    : { ...w }));
-                setWordQueue(rq);
-                setWrongWords([]);
-                setWordIndex(0);
-                resetInputs();
-                setPhase('word');
-            } else {
-                // 单词环节全部通过 → 进入句子填空环节
-                if (sentenceQueue.length > 0) {
-                    setSentenceIndex(0);
-                    resetInputs();
-                    setPhase('sentence');
-                } else {
-                    setPhase('complete');
-                }
-            }
-        }
-    }, [wordIndex, wordQueue.length, wrongWords, sentenceQueue, resetInputs]);
-
     // 句子填空选择
     const handleSentenceSelect = useCallback((opt) => {
         if (!current) return;
@@ -300,35 +259,84 @@ function TestMode() {
         setIsCorrect(ok);
         setPhase('sentence_fb');
         if (!ok) {
-            current.avoidWord.add(opt); // 记录错误混淆项,重考时换掉(需求 5)
+            current.avoidWord.add(opt);
             setWrongSentences(prev => prev.find(w => w.id === current.id) ? prev : [...prev, current]);
         }
     }, [current]);
 
-    // 句子环节进入下一题
+    // 阶段①推进:拼写全部通过 → 进入阶段②选词性
+    const nextSpell = useCallback(() => {
+        if (spellIndex + 1 < spellQueue.length) {
+            setSpellIndex(prev => prev + 1);
+            resetInputs();
+            setPhase('spell');
+        } else if (wrongSpell.length > 0) {
+            const rq = shuffle(wrongSpell.map(w => ({ ...w })));
+            setSpellQueue(rq);
+            setWrongSpell([]);
+            setSpellIndex(0);
+            resetInputs();
+            setPhase('spell');
+        } else if (posQueue.length > 0) {
+            setPosIndex(0);
+            resetInputs();
+            setPhase('pos');
+        } else if (sentenceQueue.length > 0) {
+            setSentenceIndex(0);
+            resetInputs();
+            setPhase('sentence');
+        } else {
+            setPhase('complete');
+        }
+    }, [spellIndex, spellQueue.length, wrongSpell, posQueue.length, sentenceQueue.length, resetInputs]);
+
+    // 阶段②推进:选词性全部通过 → 进入阶段③句子填空
+    const nextPos = useCallback(() => {
+        if (posIndex + 1 < posQueue.length) {
+            setPosIndex(prev => prev + 1);
+            resetInputs();
+            setPhase('pos');
+        } else if (wrongPos.length > 0) {
+            const rq = shuffle(wrongPos.map(w => ({
+                ...w,
+                posOptions: buildPosOptions(w, posPoolRef.current, w.avoidPos),
+            })));
+            setPosQueue(rq);
+            setWrongPos([]);
+            setPosIndex(0);
+            resetInputs();
+            setPhase('pos');
+        } else if (sentenceQueue.length > 0) {
+            setSentenceIndex(0);
+            resetInputs();
+            setPhase('sentence');
+        } else {
+            setPhase('complete');
+        }
+    }, [posIndex, posQueue.length, wrongPos, sentenceQueue.length, resetInputs]);
+
+    // 阶段③推进:句子填空全部通过 → 完成
     const nextSentence = useCallback(() => {
         if (sentenceIndex + 1 < sentenceQueue.length) {
             setSentenceIndex(prev => prev + 1);
             resetInputs();
             setPhase('sentence');
+        } else if (wrongSentences.length > 0) {
+            const rq = shuffle(wrongSentences.map(s => ({
+                ...s,
+                options: buildSentenceOptions(s.target, words, s.avoidWord),
+            })));
+            setSentenceQueue(rq);
+            setWrongSentences([]);
+            setSentenceIndex(0);
+            resetInputs();
+            setPhase('sentence');
         } else {
-            if (wrongSentences.length > 0) {
-                const rq = shuffle(wrongSentences.map(s => ({
-                    ...s,
-                    options: buildSentenceOptions(s.target, otherRecognizeRef.current, s.avoidWord),
-                })));
-                setSentenceQueue(rq);
-                setWrongSentences([]);
-                setSentenceIndex(0);
-                resetInputs();
-                setPhase('sentence');
-            } else {
-                setPhase('complete');
-            }
+            setPhase('complete');
         }
-    }, [sentenceIndex, sentenceQueue.length, wrongSentences, resetInputs]);
+    }, [sentenceIndex, sentenceQueue.length, wrongSentences, words, resetInputs]);
 
-    // 完成:全部环节全对才记录进度(需求 8)
+    // 完成:全部阶段全对才记录进度
     const finishTest = useCallback(async () => {
         if (submittedRef.current) return;
         submittedRef.current = true;
@@ -342,33 +350,29 @@ function TestMode() {
     // 键盘事件
     const handleKeyDown = useCallback((e) => {
         if (e.key === 'Enter') {
-            if (phase === 'word' && current && current.mode === 'dictation') {
+            if (phase === 'spell' && current && current.mode === 'dictation') {
                 checkAnswer();
-            } else if (phase === 'word_fb' && isCorrect) {
-                nextWord();
-            } else if (phase === 'word_fb' && current && current.mode === 'dictation' && !isCorrect) {
+            } else if (phase === 'spell_fb' && isCorrect) {
+                nextSpell();
+            } else if (phase === 'spell_fb' && current && current.mode === 'dictation' && !isCorrect) {
                 showCopy();
             } else if (phase === 'copy' && copyInput.toLowerCase() === (current ? (current.word || '').toLowerCase() : '')) {
-                nextWord();
-            } else if (phase === 'word_fb' && current && current.mode === 'pos') {
-                nextWord();
+                nextSpell();
+            } else if (phase === 'pos_fb') {
+                nextPos();
             } else if (phase === 'sentence_fb') {
                 nextSentence();
             }
         }
-    }, [phase, isCorrect, current, checkAnswer, nextWord, showCopy, copyInput, nextSentence]);
+    }, [phase, isCorrect, current, checkAnswer, nextSpell, showCopy, copyInput, nextPos, nextSentence]);
 
     useEffect(() => {
         window.addEventListener('keydown', handleKeyDown);
         return () => window.removeEventListener('keydown', handleKeyDown);
     }, [handleKeyDown]);
 
-    // 阻止复制/剪切/粘贴/右键,防止作弊
     const blockClipboard = (e) => e.preventDefault();
-
-    // 默写输入:仅保留 ASCII 可见字符并转小写(屏蔽中文/全角/大写)
     const sanitize = (s) => s.replace(/[^\x20-\x7E]/g, '').toLowerCase();
-
     const inputProps = {
         type: "text",
         lang: "en",
@@ -382,22 +386,19 @@ function TestMode() {
         onCompositionStart: blockClipboard,
     };
 
-    // 准备阶段
     if (words.length === 0) return <div className="loading">加载中...</div>;
 
     if (phase === 'ready') {
-        const { spell, recite, other } = getCategories(words);
-        const dictCount = spell.length + recite.length;
-        const posCount = other.length;
-        const sentCount = buildSentenceCandidates(other).length;
+        const spell = words.filter(w => (w.memory_type || 'spell') === 'spell');
+        const recognize = words.filter(w => (w.memory_type || '') === 'recognize');
+        const sentCount = sentenceCandidates(words).length;
         return (
             <div className="test-container" onCopy={blockClipboard} onCut={blockClipboard} onPaste={blockClipboard} onContextMenu={blockClipboard}>
                 <div className="test-card test-start-card">
                     <h1>单元测试 - Group {groupId}</h1>
                     <p className="test-info">共 {words.length} 个单词</p>
                     <p className="test-desc">
-                        拼写/需背诵词({dictCount})默写;其余识别词({posCount})选词性并自测中文;
-                        最后 {sentCount} 道句子填空选择题。全部答对才算完成。
+                        顺序:① 拼写默写({spell.length} 个重点词)→ ② 选择词性({recognize.length} 个识别词)→ ③ 句子填空({sentCount} 道)。全部答对才算完成。
                     </p>
                     <button className="action-btn start-btn" onClick={startTest}>开始测试</button>
                     <button className="back-home" onClick={() => navigate('/')}>返回首页</button>
@@ -427,7 +428,7 @@ function TestMode() {
             <div className="test-container" onCopy={blockClipboard} onCut={blockClipboard} onPaste={blockClipboard} onContextMenu={blockClipboard}>
                 <div className="test-card">
                     <div className="test-header">
-                        <span className="test-status">句子填空 - Group {groupId}</span>
+                        <span className="test-status">③ 句子填空 - Group {groupId}</span>
                         <span className="test-progress">{sentenceIndex + 1} / {sentenceQueue.length}</span>
                     </div>
                     <div className="test-body">
@@ -464,21 +465,25 @@ function TestMode() {
         );
     }
 
-    // å\8d\95è¯\8dç\8e¯è\8a\82ï¼\88é»\98å\86\99 / 选词性)
+    // å\8d\95è¯\8dç\8e¯è\8a\82ï¼\88é\98¶æ®µâ\91 æ\8b¼å\86\99 / é\98¶æ®µâ\91¡选词性)
     const isPos = current.mode === 'pos';
+    const isSpell = current.mode === 'dictation';
+    const headerLabel = isPos ? '② 选择词性' : '① 拼写默写';
+    const progressText = isPos
+        ? `${posIndex + 1} / ${posQueue.length}`
+        : `${spellIndex + 1} / ${spellQueue.length}`;
+
     return (
         <div className="test-container" onCopy={blockClipboard} onCut={blockClipboard} onPaste={blockClipboard} onContextMenu={blockClipboard}>
             <div className="test-card">
                 <div className="test-header">
-                    <span className="test-status">
-                        {isPos ? '选词性' : '默写'} - Group {groupId}
-                    </span>
-                    <span className="test-progress">{wordIndex + 1} / {wordQueue.length}</span>
+                    <span className="test-status">{headerLabel} - Group {groupId}</span>
+                    <span className="test-progress">{progressText}</span>
                 </div>
 
                 <div className="test-body">
-                    {/* 答题中 */}
-                    {phase === 'word' && !isPos && (
+                    {/* 阶段①:默写 */}
+                    {phase === 'spell' && isSpell && (
                         <div className="testing-area">
                             <div className="prompt-info">
                                 <p className="prompt-chinese">{current.chinese_meaning}</p>
@@ -498,7 +503,8 @@ function TestMode() {
                         </div>
                     )}
 
-                    {phase === 'word' && isPos && (
+                    {/* 阶段②:选词性 */}
+                    {phase === 'spell' && isPos && (
                         <div className="testing-area">
                             <div className="prompt-info">
                                 <p className="prompt-word-en">{current.word}</p>
@@ -518,7 +524,7 @@ function TestMode() {
                     )}
 
                     {/* 反馈区 */}
-                    {(phase === 'word_fb' || phase === 'copy') && (
+                    {(phase === 'spell_fb' || phase === 'copy') && (
                         <div className="feedback-wrapper">
                             <div className="feedback-main">
                                 <div className={`fb-status ${isCorrect ? 'correct' : 'wrong'}`}>
@@ -533,7 +539,6 @@ function TestMode() {
                                 {current.affix && (<div className="fb-affix">组成: {current.affix}</div>)}
                                 {current.example_sentence && (<div className="fb-sentence">例句: {current.example_sentence}</div>)}
 
-                                {/* 选词性环节:自测回忆中文意思(仅自测,不阻塞) */}
                                 {isPos && (
                                     <div className="chinese-recall">
                                         <p className="recall-tip">自己回忆的中文意思是否一致?(仅自测)</p>
@@ -557,12 +562,12 @@ function TestMode() {
                                     </div>
                                 )}
 
-                                {!isCorrect && phase === 'word_fb' && !isPos && (
+                                {!isCorrect && phase === 'spell_fb' && isSpell && (
                                     <button className="action-btn copy-trigger" onClick={showCopy}>抄写纠正 (Enter)</button>
                                 )}
 
                                 {(isCorrect || isPos) && (
-                                    <button className="action-btn" onClick={nextWord}>下一词 (Enter)</button>
+                                    <button className="action-btn" onClick={isPos ? nextPos : nextSpell}>下一词 (Enter)</button>
                                 )}
                             </div>
 
@@ -590,7 +595,7 @@ function TestMode() {
                                 {copyInput.toLowerCase() === (current.word || '').toLowerCase() ? '✅' : '❌'}
                             </span>
                             {copyInput.toLowerCase() === (current.word || '').toLowerCase() && (
-                                <button className="action-btn" onClick={nextWord}>下一词 (Enter)</button>
+                                <button className="action-btn" onClick={nextSpell}>下一词 (Enter)</button>
                             )}
                         </div>
                     )}