From 2a8af19ed0990ea774eb6be058a92a68f1c5d43f Mon Sep 17 00:00:00 2001 From: root Date: Sun, 7 Jun 2026 22:37:51 +0800 Subject: [PATCH] =?utf8?q?=E6=B5=8B=E8=AF=95=E6=A8=A1=E5=BC=8F=E5=88=9D?= =?utf8?q?=E7=89=88,=E5=AD=A6=E4=B9=A0=E6=A8=A1=E5=BC=8F=E9=97=AE=E9=A2=98?= =?utf8?q?=E4=BF=AE=E5=A4=8D,by=20codebuddy?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- frontend/src/App.js | 2 + frontend/src/components/Home.js | 47 +++- frontend/src/components/StudyMode.js | 6 +- frontend/src/components/TestMode.css | 313 +++++++++++++++++++++++++++ frontend/src/components/TestMode.js | 299 +++++++++++++++++++++++++ 6 files changed, 661 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/TestMode.css create mode 100644 frontend/src/components/TestMode.js diff --git a/backend/main.py b/backend/main.py index d18b5d8..e6d4753 100644 --- a/backend/main.py +++ b/backend/main.py @@ -198,7 +198,7 @@ async def get_picture(pic_id: int): raise HTTPException(status_code=500, detail=str(e)) @app.post("/api/progress/update") -async def update_progress(ProgressUpdate): +async def update_progress(data: ProgressUpdate): try: conn = sqlite3.connect(DATABASE_PATH) cursor = conn.cursor() diff --git a/frontend/src/App.js b/frontend/src/App.js index 9448d70..8d74f68 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -2,6 +2,7 @@ import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import Home from './components/Home'; import StudyMode from './components/StudyMode'; +import TestMode from './components/TestMode'; function App() { return ( @@ -9,6 +10,7 @@ function App() { } /> } /> + } /> ); diff --git a/frontend/src/components/Home.js b/frontend/src/components/Home.js index f9bc7fa..2ea0518 100644 --- a/frontend/src/components/Home.js +++ b/frontend/src/components/Home.js @@ -46,10 +46,21 @@ function Home() { 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 ( @@ -90,10 +101,34 @@ function Home() { > {item.study} - {item.test} - {item.review1} - {item.review2} - {item.review3} + handleCellClick(item, 'test')} + style={{ cursor: item.test && item.test.includes('请开始') ? 'pointer' : 'default' }} + > + {item.test} + + handleCellClick(item, 'review1')} + style={{ cursor: item.review1 && (item.review1.includes('请开始') || item.review1.includes('请尽快')) ? 'pointer' : 'default' }} + > + {item.review1} + + handleCellClick(item, 'review2')} + style={{ cursor: item.review2 && (item.review2.includes('请开始') || item.review2.includes('请尽快')) ? 'pointer' : 'default' }} + > + {item.review2} + + handleCellClick(item, 'review3')} + style={{ cursor: item.review3 && (item.review3.includes('请开始') || item.review3.includes('请尽快')) ? 'pointer' : 'default' }} + > + {item.review3} + ))} diff --git a/frontend/src/components/StudyMode.js b/frontend/src/components/StudyMode.js index 463fab4..ffc9a14 100644 --- a/frontend/src/components/StudyMode.js +++ b/frontend/src/components/StudyMode.js @@ -13,8 +13,9 @@ function StudyMode() { const [phase, setPhase] = useState('learning'); // learning, flashcard, correction const [userInput, setUserInput] = useState(""); const [correctionInput, setCorrectionInput] = useState(""); - + const hasSpokenRef = useRef(null); + const transitioningRef = useRef(false); const currentWord = words[currentIndex]; // 朗读逻辑:语速 0.6,间隔 500ms @@ -55,6 +56,7 @@ function StudyMode() { // 全局键盘监听:在学习模式下按回车进入闪记 useEffect(() => { const handleGlobalKeyDown = (event) => { + if (transitioningRef.current) return; if (phase === 'learning' && event.key === 'Enter') { setPhase('flashcard'); } @@ -64,11 +66,13 @@ function StudyMode() { }, [phase]); const handleNext = async () => { + transitioningRef.current = true; if (currentIndex < words.length - 1) { setCurrentIndex(currentIndex + 1); setPhase('learning'); setUserInput(""); setCorrectionInput(""); + setTimeout(() => { transitioningRef.current = false; }, 300); } else { await axios.post(`${API_BASE}/api/progress/update`, { group_id: groupId, diff --git a/frontend/src/components/TestMode.css b/frontend/src/components/TestMode.css new file mode 100644 index 0000000..1802b83 --- /dev/null +++ b/frontend/src/components/TestMode.css @@ -0,0 +1,313 @@ +/* /home/words/frontend/src/components/TestMode.css */ + +.test-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; + background-color: #fef9f0; + min-height: 100vh; +} + +.test-card { + background: white; + border-radius: 20px; + box-shadow: 0 10px 30px rgba(0,0,0,0.1); + width: 95%; + max-width: 1100px; + min-height: 500px; + overflow: hidden; +} + +.test-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 30px; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; +} + +.test-status { + font-size: 20px; + font-weight: bold; + color: #495057; +} + +.test-progress { + font-size: 18px; + color: #868e96; + font-weight: 500; +} + +.test-body { + padding: 30px 40px; +} + +/* 开始/完成/重考提示卡片 */ +.test-start-card, +.test-complete-card, +.test-retest-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + text-align: center; +} + +.test-start-card h1, +.test-complete-card h1, +.test-retest-card h1 { + font-size: 36px; + color: #2c3e50; + margin-bottom: 15px; +} + +.test-info { + font-size: 22px; + color: #64748b; + margin-bottom: 10px; +} + +.test-desc { + font-size: 16px; + color: #94a3b8; + margin-bottom: 30px; +} + +.start-btn { + background: #e67e22; +} + +.start-btn:hover { + background: #d35400; +} + +/* 答题区域 */ +.testing-area { + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 0; +} + +.prompt-info { + text-align: center; + margin-bottom: 30px; +} + +.prompt-chinese { + font-size: 42px; + font-weight: bold; + color: #1e293b; +} + +.prompt-pos { + font-size: 22px; + color: #1e40af; + margin-top: 8px; +} + +.input-row { + display: flex; + align-items: center; + gap: 15px; +} + +.test-input { + width: 400px; + padding: 15px; + font-size: 24px; + border: 2px solid #ddd; + border-radius: 10px; + outline: none; +} + +.test-input:focus { + border-color: #e67e22; +} + +/* 反馈区域 */ +.feedback-wrapper { + display: flex; + gap: 20px; + padding: 10px 0; +} + +.feedback-main { + flex: 1; + padding: 20px; + background: #f8f9fa; + border-radius: 12px; +} + +.fb-status { + font-size: 22px; + font-weight: bold; + margin-bottom: 15px; +} + +.fb-status.correct { + color: #10b981; +} + +.fb-status.wrong { + color: #dc2626; +} + +.fb-word { + font-size: 36px; + font-weight: bold; + color: #1e40af; + margin-bottom: 8px; +} + +.fb-chinese { + font-size: 20px; + color: #374151; + margin-bottom: 4px; +} + +.fb-pos { + font-size: 18px; + color: #374151; + margin-bottom: 12px; +} + +.fb-root { + font-size: 18px; + color: #7c3aed; + margin-bottom: 4px; +} + +.fb-affix { + font-size: 18px; + color: #2563eb; + margin-bottom: 4px; +} + +.fb-sentence { + font-size: 16px; + color: #374151; + margin-top: 10px; + line-height: 1.6; +} + +.fb-image { + width: 300px; + display: flex; + align-items: flex-start; + justify-content: center; + background: #fafafa; + border-radius: 12px; + padding: 10px; +} + +.fb-image img { + max-width: 100%; + max-height: 300px; + object-fit: contain; +} + +.copy-trigger { + margin-top: 15px; + background: #dc2626; +} + +.copy-trigger:hover { + background: #b91c1c; +} + +/* 抄写纠正区域 */ +.copy-area { + display: flex; + align-items: center; + gap: 10px; + padding: 15px 0; + margin-top: 15px; +} + +.copy-label { + font-size: 20px; + color: #dc2626; + font-weight: bold; +} + +.copy-input { + width: 350px; + padding: 12px; + font-size: 22px; + border: 2px solid #ddd; + border-radius: 10px; + outline: none; +} + +.copy-input:focus { + border-color: #dc2626; +} + +.copy-input.correct { + border-color: #10b981; + background: #ecfdf5; +} + +.copy-check { + font-size: 22px; +} + +.copy-check.ok { + color: #10b981; +} + +/* 完成卡片 */ +.test-complete-card h1 { + font-size: 40px; + color: #10b981; +} + +.test-complete-card p { + font-size: 20px; + color: #64748b; + margin-bottom: 20px; +} + +.test-retest-card h1 { + color: #e67e22; +} + +.test-retest-card p { + font-size: 20px; + color: #64748b; + margin-bottom: 20px; +} + +.action-btn { + padding: 12px 28px; + background: #1e40af; + color: white; + border: none; + border-radius: 10px; + font-size: 18px; + cursor: pointer; + transition: background 0.2s; +} + +.action-btn:hover { + background: #1e3a8a; +} + +.back-home { + margin-top: 20px; + background: none; + border: none; + color: #64748b; + text-decoration: underline; + cursor: pointer; +} + +.loading { + font-size: 24px; + margin-top: 100px; + color: #64748b; +} diff --git a/frontend/src/components/TestMode.js b/frontend/src/components/TestMode.js new file mode 100644 index 0000000..74e4741 --- /dev/null +++ b/frontend/src/components/TestMode.js @@ -0,0 +1,299 @@ +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
加载中...
; + + if (phase === 'ready') { + return ( +
+
+

单元测试 - Group {groupId}

+

共 {words.length} 个单词

+

根据中文含义拼写英文单词,拼写错误需要抄写纠正

+ + +
+
+ ); + } + + if (phase === 'complete') { + return ( +
+
+

🎉 测试完成!

+

Group {groupId} 全部通过!

+ +
+
+ ); + } + + if (phase === 'retest_info') { + return ( +
+
+

📝 开始重考

+

还有 {wrongQueue.length} 个错题需要重考

+ +
+
+ ); + } + + if (!currentWord) return null; + + return ( +
+
+ {/* 状态栏 */} +
+ + {isRetesting ? '重考' : '测试'} - Group {groupId} + + + {currentIndex + 1} / {totalCount} + +
+ + {/* 答题区域 */} +
+ {phase === 'testing' && ( +
+
+

{currentWord.chinese_meaning}

+

({currentWord.part_of_speech})

+
+
+ setUserInput(e.target.value)} + placeholder="请输入英文单词..." + className="test-input" + /> + +
+
+ )} + + {/* 反馈区域 */} + {(phase === 'feedback' || phase === 'copy') && ( +
+
+
+ {isCorrect ? '✅ 正确!' : `❌ 错误,你的答案: ${userInput}`} +
+ +
{currentWord.word}
+
中文: {currentWord.chinese_meaning}
+
词性: {currentWord.part_of_speech}
+ + {currentWord.word_root && ( +
词根: {currentWord.word_root}
+ )} + {currentWord.affix && ( +
组成: {currentWord.affix}
+ )} + {currentWord.example_sentence && ( +
例句: {currentWord.example_sentence}
+ )} + + {!isCorrect && phase === 'feedback' && ( + + )} + + {isCorrect && ( + + )} +
+ + {/* 图片 */} + {currentWord.picture_id && ( +
+ mindmap +
+ )} +
+ )} + + {/* 抄写纠正区域 */} + {phase === 'copy' && ( +
+ + setCopyInput(e.target.value)} + className={`copy-input ${copyInput === currentWord.word ? 'correct' : ''}`} + placeholder="请在此抄写正确单词..." + /> + + {copyInput === currentWord.word ? '✅' : '❌'} + + {copyInput === currentWord.word && ( + + )} +
+ )} +
+
+ +
+ ); +} + +export default TestMode; -- 2.43.0