const handleCellClick = (item, type) => {
const groupId = item.group.replace('Group ', '');
- if (type === 'study' && item.study.includes('请开始学习')) {
+ if (type === 'study' && item.study && item.study.includes('请开始学习')) {
navigate(`/study/${groupId}`);
}
- // 后续可以在这里添加 test 和 review 的跳转逻辑
+ if (type === 'test' && item.test && item.test.includes('请开始测试')) {
+ navigate(`/test/${groupId}`);
+ }
+ if (type === 'review1' && item.review1 && (item.review1.includes('请开始复习') || item.review1.includes('请尽快复习'))) {
+ navigate(`/test/${groupId}`);
+ }
+ if (type === 'review2' && item.review2 && (item.review2.includes('请开始复习') || item.review2.includes('请尽快复习'))) {
+ navigate(`/test/${groupId}`);
+ }
+ if (type === 'review3' && item.review3 && (item.review3.includes('请开始复习') || item.review3.includes('请尽快复习'))) {
+ navigate(`/test/${groupId}`);
+ }
};
return (
>
{item.study}
</td>
- <td className={getStatusClass(item.test)}>{item.test}</td>
- <td className={getStatusClass(item.review1)}>{item.review1}</td>
- <td className={getStatusClass(item.review2)}>{item.review2}</td>
- <td className={getStatusClass(item.review3)}>{item.review3}</td>
+ <td
+ className={getStatusClass(item.test)}
+ onClick={() => handleCellClick(item, 'test')}
+ style={{ cursor: item.test && item.test.includes('请开始') ? 'pointer' : 'default' }}
+ >
+ {item.test}
+ </td>
+ <td
+ className={getStatusClass(item.review1)}
+ onClick={() => handleCellClick(item, 'review1')}
+ style={{ cursor: item.review1 && (item.review1.includes('请开始') || item.review1.includes('请尽快')) ? 'pointer' : 'default' }}
+ >
+ {item.review1}
+ </td>
+ <td
+ className={getStatusClass(item.review2)}
+ onClick={() => handleCellClick(item, 'review2')}
+ style={{ cursor: item.review2 && (item.review2.includes('请开始') || item.review2.includes('请尽快')) ? 'pointer' : 'default' }}
+ >
+ {item.review2}
+ </td>
+ <td
+ className={getStatusClass(item.review3)}
+ onClick={() => handleCellClick(item, 'review3')}
+ style={{ cursor: item.review3 && (item.review3.includes('请开始') || item.review3.includes('请尽快')) ? 'pointer' : 'default' }}
+ >
+ {item.review3}
+ </td>
</tr>
))}
</tbody>
--- /dev/null
+import React, { useState, useEffect, useCallback, useRef } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import axios from 'axios';
+import './TestMode.css';
+
+const API_BASE = "http://124.222.157.124:1060";
+
+function TestMode() {
+ const { groupId } = useParams();
+ const navigate = useNavigate();
+ const [words, setWords] = useState([]);
+ const [queue, setQueue] = useState([]); // 当前测试队列
+ const [wrongQueue, setWrongQueue] = useState([]); // 错词重考队列
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [totalCount, setTotalCount] = useState(0);
+ const [phase, setPhase] = useState('ready'); // ready, testing, feedback, copy, retest_info, complete
+ const [userInput, setUserInput] = useState("");
+ const [copyInput, setCopyInput] = useState("");
+ const [isCorrect, setIsCorrect] = useState(false);
+ const [isRetesting, setIsRetesting] = useState(false);
+
+ const inputRef = useRef(null);
+ const copyRef = useRef(null);
+ const currentWord = queue[currentIndex];
+
+ // 获取单词数据
+ useEffect(() => {
+ axios.get(`${API_BASE}/api/words/${groupId}`).then(res => {
+ setWords(res.data);
+ });
+ }, [groupId]);
+
+ // 开始测试:随机打乱
+ const startTest = useCallback(() => {
+ const shuffled = [...words].sort(() => Math.random() - 0.5);
+ setQueue(shuffled);
+ setWrongQueue([]);
+ setCurrentIndex(0);
+ setTotalCount(shuffled.length);
+ setPhase('testing');
+ setUserInput("");
+ setIsRetesting(false);
+ }, [words]);
+
+ // 自动聚焦输入框
+ useEffect(() => {
+ if (phase === 'testing' && inputRef.current) {
+ inputRef.current.focus();
+ }
+ if (phase === 'copy' && copyRef.current) {
+ copyRef.current.focus();
+ }
+ }, [phase, currentIndex]);
+
+ // 朗读
+ const speak = useCallback((text) => {
+ window.speechSynthesis.cancel();
+ const utterance = new SpeechSynthesisUtterance(text);
+ utterance.lang = 'en-US';
+ utterance.rate = 0.6;
+ window.speechSynthesis.speak(utterance);
+ }, []);
+
+ // 提交答案
+ const checkAnswer = useCallback(() => {
+ if (!currentWord || !userInput.trim()) return;
+ const correct = currentWord.word.trim().toLowerCase();
+ const answer = userInput.trim().toLowerCase();
+ const ok = answer === correct;
+
+ setIsCorrect(ok);
+ setPhase('feedback');
+ speak(currentWord.word);
+
+ if (!ok) {
+ setWrongQueue(prev => {
+ if (!prev.find(w => w.id === currentWord.id)) {
+ return [...prev, currentWord];
+ }
+ return prev;
+ });
+ }
+ }, [currentWord, userInput, speak]);
+
+ // 显示抄写区
+ const showCopy = useCallback(() => {
+ setCopyInput("");
+ setPhase('copy');
+ }, []);
+
+ // 下一个词
+ const nextWord = useCallback(() => {
+ if (currentIndex + 1 < queue.length) {
+ setCurrentIndex(prev => prev + 1);
+ setUserInput("");
+ setPhase('testing');
+ } else {
+ // 本轮结束
+ if (wrongQueue.length > 0) {
+ // 有错题,进入重考
+ setPhase('retest_info');
+ } else {
+ // 全部通过
+ setPhase('complete');
+ }
+ }
+ }, [currentIndex, queue.length, wrongQueue.length]);
+
+ // 开始重考
+ const startRetest = useCallback(() => {
+ const retestWords = [...wrongQueue].sort(() => Math.random() - 0.5);
+ setQueue(retestWords);
+ setWrongQueue([]);
+ setCurrentIndex(0);
+ setTotalCount(retestWords.length);
+ setUserInput("");
+ setPhase('testing');
+ setIsRetesting(true);
+ }, [wrongQueue]);
+
+ // 完成测试,记录进度
+ const finishTest = useCallback(async () => {
+ await axios.post(`${API_BASE}/api/progress/update`, {
+ group_id: groupId,
+ op_type: 'test'
+ });
+ navigate('/');
+ }, [groupId, navigate]);
+
+ // 键盘事件
+ const handleKeyDown = useCallback((e) => {
+ if (e.key === 'Enter') {
+ if (phase === 'testing') {
+ checkAnswer();
+ } else if (phase === 'feedback' && isCorrect) {
+ nextWord();
+ } else if (phase === 'copy' && copyInput === currentWord.word) {
+ nextWord();
+ } else if (phase === 'feedback' && !isCorrect) {
+ showCopy();
+ }
+ }
+ }, [phase, isCorrect, checkAnswer, nextWord, showCopy, copyInput, currentWord]);
+
+ useEffect(() => {
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [handleKeyDown]);
+
+ // 准备阶段
+ if (words.length === 0) return <div className="loading">加载中...</div>;
+
+ if (phase === 'ready') {
+ return (
+ <div className="test-container">
+ <div className="test-card test-start-card">
+ <h1>单元测试 - Group {groupId}</h1>
+ <p className="test-info">共 {words.length} 个单词</p>
+ <p className="test-desc">根据中文含义拼写英文单词,拼写错误需要抄写纠正</p>
+ <button className="action-btn start-btn" onClick={startTest}>开始测试</button>
+ <button className="back-home" onClick={() => navigate('/')}>返回首页</button>
+ </div>
+ </div>
+ );
+ }
+
+ if (phase === 'complete') {
+ return (
+ <div className="test-container">
+ <div className="test-card test-complete-card">
+ <h1>🎉 测试完成!</h1>
+ <p>Group {groupId} 全部通过!</p>
+ <button className="action-btn" onClick={finishTest}>完成</button>
+ </div>
+ </div>
+ );
+ }
+
+ if (phase === 'retest_info') {
+ return (
+ <div className="test-container">
+ <div className="test-card test-retest-card">
+ <h1>📝 开始重考</h1>
+ <p>还有 {wrongQueue.length} 个错题需要重考</p>
+ <button className="action-btn" onClick={startRetest}>开始重考</button>
+ </div>
+ </div>
+ );
+ }
+
+ if (!currentWord) return null;
+
+ return (
+ <div className="test-container">
+ <div className="test-card">
+ {/* 状态栏 */}
+ <div className="test-header">
+ <span className="test-status">
+ {isRetesting ? '重考' : '测试'} - Group {groupId}
+ </span>
+ <span className="test-progress">
+ {currentIndex + 1} / {totalCount}
+ </span>
+ </div>
+
+ {/* 答题区域 */}
+ <div className="test-body">
+ {phase === 'testing' && (
+ <div className="testing-area">
+ <div className="prompt-info">
+ <p className="prompt-chinese">{currentWord.chinese_meaning}</p>
+ <p className="prompt-pos">({currentWord.part_of_speech})</p>
+ </div>
+ <div className="input-row">
+ <input
+ ref={inputRef}
+ value={userInput}
+ onChange={(e) => setUserInput(e.target.value)}
+ placeholder="请输入英文单词..."
+ className="test-input"
+ />
+ <button className="action-btn" onClick={checkAnswer}>提交</button>
+ </div>
+ </div>
+ )}
+
+ {/* 反馈区域 */}
+ {(phase === 'feedback' || phase === 'copy') && (
+ <div className="feedback-wrapper">
+ <div className="feedback-main">
+ <div className={`fb-status ${isCorrect ? 'correct' : 'wrong'}`}>
+ {isCorrect ? '✅ 正确!' : `❌ 错误,你的答案: ${userInput}`}
+ </div>
+
+ <div className="fb-word">{currentWord.word}</div>
+ <div className="fb-chinese">中文: {currentWord.chinese_meaning}</div>
+ <div className="fb-pos">词性: {currentWord.part_of_speech}</div>
+
+ {currentWord.word_root && (
+ <div className="fb-root">词根: {currentWord.word_root}</div>
+ )}
+ {currentWord.affix && (
+ <div className="fb-affix">组成: {currentWord.affix}</div>
+ )}
+ {currentWord.example_sentence && (
+ <div className="fb-sentence">例句: {currentWord.example_sentence}</div>
+ )}
+
+ {!isCorrect && phase === 'feedback' && (
+ <button className="action-btn copy-trigger" onClick={showCopy}>
+ 抄写纠正 (Enter)
+ </button>
+ )}
+
+ {isCorrect && (
+ <button className="action-btn" onClick={nextWord}>
+ 下一词 (Enter)
+ </button>
+ )}
+ </div>
+
+ {/* 图片 */}
+ {currentWord.picture_id && (
+ <div className="fb-image">
+ <img src={`${API_BASE}/api/picture/${currentWord.picture_id}`} alt="mindmap" />
+ </div>
+ )}
+ </div>
+ )}
+
+ {/* 抄写纠正区域 */}
+ {phase === 'copy' && (
+ <div className="copy-area">
+ <label className="copy-label">抄写: </label>
+ <input
+ ref={copyRef}
+ value={copyInput}
+ onChange={(e) => setCopyInput(e.target.value)}
+ className={`copy-input ${copyInput === currentWord.word ? 'correct' : ''}`}
+ placeholder="请在此抄写正确单词..."
+ />
+ <span className={`copy-check ${copyInput === currentWord.word ? 'ok' : ''}`}>
+ {copyInput === currentWord.word ? '✅' : '❌'}
+ </span>
+ {copyInput === currentWord.word && (
+ <button className="action-btn" onClick={nextWord}>
+ 下一词 (Enter)
+ </button>
+ )}
+ </div>
+ )}
+ </div>
+ </div>
+ <button className="back-home" onClick={() => navigate('/')}>返回首页</button>
+ </div>
+ );
+}
+
+export default TestMode;