feat: 完成例句重組與例句口說功能設計
## 例句重組功能實現 - 實現完整的點擊式單字重組功能 - 添加例句圖片顯示支援 - 創建直觀的重組區域和可用單字區域 - 實現答案檢查和結果回饋系統 - 提供重新開始功能 ## 例句口說功能優化 - 添加例句圖片顯示 - 重新設計為完整例句口說練習 - 使用統一的區塊化布局設計 - 移除單獨的詞彙發音區塊,專注於例句練習 - 調整VoiceRecorder目標為完整例句 ## 技術改進 - 改進拖拉操作為更簡單的點擊操作 - 統一所有測驗模式的視覺設計 - 優化用戶互動體驗和學習流程 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a20fa9004d
commit
7203346134
|
|
@ -26,6 +26,11 @@ export default function LearnPage() {
|
|||
const [quizOptions, setQuizOptions] = useState<string[]>(['brought', 'determine', 'achieve', 'consider'])
|
||||
const [cardHeight, setCardHeight] = useState<number>(400)
|
||||
|
||||
// Sentence reorder states
|
||||
const [shuffledWords, setShuffledWords] = useState<string[]>([])
|
||||
const [arrangedWords, setArrangedWords] = useState<string[]>([])
|
||||
const [reorderResult, setReorderResult] = useState<boolean | null>(null)
|
||||
|
||||
// Refs for measuring card content heights
|
||||
const cardFrontRef = useRef<HTMLDivElement>(null)
|
||||
const cardBackRef = useRef<HTMLDivElement>(null)
|
||||
|
|
@ -143,6 +148,46 @@ export default function LearnPage() {
|
|||
setShowResult(false);
|
||||
}, [currentCardIndex])
|
||||
|
||||
// Initialize sentence reorder when card changes or mode switches to sentence-reorder
|
||||
useEffect(() => {
|
||||
if (mode === 'sentence-reorder') {
|
||||
const words = currentCard.example.split(/\s+/).filter(word => word.length > 0)
|
||||
const shuffled = [...words].sort(() => Math.random() - 0.5)
|
||||
setShuffledWords(shuffled)
|
||||
setArrangedWords([])
|
||||
setReorderResult(null)
|
||||
}
|
||||
}, [currentCardIndex, mode, currentCard.example])
|
||||
|
||||
// Sentence reorder handlers
|
||||
const handleWordClick = (word: string) => {
|
||||
// Move word from shuffled to arranged
|
||||
setShuffledWords(prev => prev.filter(w => w !== word))
|
||||
setArrangedWords(prev => [...prev, word])
|
||||
setReorderResult(null)
|
||||
}
|
||||
|
||||
const handleRemoveFromArranged = (word: string) => {
|
||||
setArrangedWords(prev => prev.filter(w => w !== word))
|
||||
setShuffledWords(prev => [...prev, word])
|
||||
setReorderResult(null)
|
||||
}
|
||||
|
||||
const handleCheckReorderAnswer = () => {
|
||||
const userSentence = arrangedWords.join(' ')
|
||||
const correctSentence = currentCard.example
|
||||
const isCorrect = userSentence.toLowerCase().trim() === correctSentence.toLowerCase().trim()
|
||||
setReorderResult(isCorrect)
|
||||
}
|
||||
|
||||
const handleResetReorder = () => {
|
||||
const words = currentCard.example.split(/\s+/).filter(word => word.length > 0)
|
||||
const shuffled = [...words].sort(() => Math.random() - 0.5)
|
||||
setShuffledWords(shuffled)
|
||||
setArrangedWords([])
|
||||
setReorderResult(null)
|
||||
}
|
||||
|
||||
const handleFlip = () => {
|
||||
setIsFlipped(!isFlipped)
|
||||
}
|
||||
|
|
@ -891,39 +936,46 @@ export default function LearnPage() {
|
|||
{currentCard.difficulty}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Example Image */}
|
||||
{currentCard.exampleImage && (
|
||||
<div className="mb-6">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<img
|
||||
src={currentCard.exampleImage}
|
||||
alt="Example illustration"
|
||||
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
|
||||
onClick={() => setModalImage(currentCard.exampleImage)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-lg text-gray-700 mb-2 text-left">
|
||||
請看例句圖片並大聲說出這個英文單字:
|
||||
請看例句圖片並大聲說出完整的例句:
|
||||
</p>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<div className="bg-gray-50 rounded-lg p-6 mb-6">
|
||||
<p className="text-lg text-gray-700 mb-4">
|
||||
<strong>中文翻譯:</strong>{currentCard.translation}
|
||||
</p>
|
||||
<p className="text-lg text-gray-700 mb-4">
|
||||
<strong>定義:</strong>{currentCard.definition}
|
||||
</p>
|
||||
<p className="text-gray-600">
|
||||
<strong>發音:</strong>{currentCard.pronunciation}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-lg text-gray-700 mb-6">
|
||||
請說出這個英文單字:
|
||||
</p>
|
||||
<div className="mb-6">
|
||||
<AudioPlayer text={currentCard.word} />
|
||||
<p className="text-sm text-gray-500 mt-2">
|
||||
點擊播放聽一遍正確發音
|
||||
</p>
|
||||
<div className="space-y-4 mb-8">
|
||||
{/* Example Sentence */}
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2 text-left">例句</h3>
|
||||
<div className="relative">
|
||||
<p className="text-gray-700 italic mb-2 text-left pr-12">"{currentCard.example}"</p>
|
||||
<div className="absolute bottom-0 right-0">
|
||||
<AudioPlayer text={currentCard.example} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-gray-600 text-sm text-left">"{currentCard.exampleTranslation}"</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="max-w-md mx-auto">
|
||||
<VoiceRecorder
|
||||
targetText={currentCard.word}
|
||||
targetText={currentCard.example}
|
||||
onRecordingComplete={() => {
|
||||
// 簡化處理:直接顯示結果
|
||||
handleSpeakingAnswer(currentCard.word)
|
||||
handleSpeakingAnswer(currentCard.example)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1072,25 +1124,106 @@ export default function LearnPage() {
|
|||
</div>
|
||||
)}
|
||||
<p className="text-lg text-gray-700 mb-2 text-left">
|
||||
請將下方的單字重新排列成正確的句子:
|
||||
點擊下方單字,依序重組成正確的句子:
|
||||
</p>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<div className="bg-gray-50 rounded-lg p-6 mb-6">
|
||||
<p className="text-lg text-gray-700 mb-4">
|
||||
<strong>詞彙:</strong>{currentCard.word}
|
||||
</p>
|
||||
<p className="text-lg text-gray-700">
|
||||
<strong>翻譯:</strong>{currentCard.exampleTranslation}
|
||||
</p>
|
||||
{/* Arranged Sentence Area */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left">重組區域:</h3>
|
||||
<div className="min-h-[120px] bg-gray-50 rounded-lg p-4 border-2 border-dashed border-gray-300">
|
||||
{arrangedWords.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-gray-400 text-lg">
|
||||
點擊下方單字來重組句子
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{arrangedWords.map((word, index) => (
|
||||
<div
|
||||
key={`arranged-${index}`}
|
||||
className="inline-flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-blue-200 transition-colors"
|
||||
onClick={() => handleRemoveFromArranged(word)}
|
||||
>
|
||||
{word}
|
||||
<span className="ml-2 text-blue-600 hover:text-blue-800">×</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shuffled Words */}
|
||||
<div className="mb-6">
|
||||
<div className="text-center text-gray-500">
|
||||
[例句重組題功能開發中...]
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left">可用單字:</h3>
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4 min-h-[80px]">
|
||||
{shuffledWords.length === 0 ? (
|
||||
<div className="text-center text-gray-400">
|
||||
所有單字都已使用
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{shuffledWords.map((word, index) => (
|
||||
<button
|
||||
key={`shuffled-${index}`}
|
||||
onClick={() => handleWordClick(word)}
|
||||
className="bg-gray-100 text-gray-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-gray-200 active:bg-gray-300 transition-colors select-none"
|
||||
>
|
||||
{word}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control Buttons */}
|
||||
<div className="flex gap-3 mb-6">
|
||||
{arrangedWords.length > 0 && (
|
||||
<button
|
||||
onClick={handleCheckReorderAnswer}
|
||||
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
|
||||
>
|
||||
檢查答案
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleResetReorder}
|
||||
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
重新開始
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Result Feedback */}
|
||||
{reorderResult !== null && (
|
||||
<div className={`p-6 rounded-lg w-full mb-6 ${
|
||||
reorderResult
|
||||
? 'bg-green-50 border border-green-200'
|
||||
: 'bg-red-50 border border-red-200'
|
||||
}`}>
|
||||
<p className={`font-semibold text-left text-xl mb-4 ${
|
||||
reorderResult ? 'text-green-700' : 'text-red-700'
|
||||
}`}>
|
||||
{reorderResult ? '正確!' : '錯誤!'}
|
||||
</p>
|
||||
|
||||
{!reorderResult && (
|
||||
<div className="mb-4">
|
||||
<p className="text-gray-700 text-left">
|
||||
正確答案是:<strong className="text-lg">"{currentCard.example}"</strong>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="text-left">
|
||||
<p className="text-gray-600">
|
||||
<strong>中文翻譯:</strong>{currentCard.exampleTranslation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue