feat: 完成例句重組與例句口說功能設計

## 例句重組功能實現
- 實現完整的點擊式單字重組功能
- 添加例句圖片顯示支援
- 創建直觀的重組區域和可用單字區域
- 實現答案檢查和結果回饋系統
- 提供重新開始功能

## 例句口說功能優化
- 添加例句圖片顯示
- 重新設計為完整例句口說練習
- 使用統一的區塊化布局設計
- 移除單獨的詞彙發音區塊,專注於例句練習
- 調整VoiceRecorder目標為完整例句

## 技術改進
- 改進拖拉操作為更簡單的點擊操作
- 統一所有測驗模式的視覺設計
- 優化用戶互動體驗和學習流程

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-20 04:00:14 +08:00
parent a20fa9004d
commit 7203346134
1 changed files with 167 additions and 34 deletions

View File

@ -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 */}