220 lines
6.8 KiB
TypeScript
220 lines
6.8 KiB
TypeScript
import { useState, useMemo } from 'react'
|
||
import { FillTestProps, ReviewCardData } from '@/types/review'
|
||
import { useReviewLogic } from '@/hooks/useReviewLogic'
|
||
import {
|
||
CardHeader,
|
||
AudioSection,
|
||
ErrorReportButton
|
||
} from '@/components/review/shared'
|
||
import { getCorrectAnswer } from '@/utils/answerExtractor'
|
||
|
||
// 優化後的 SentenceFillTest 組件
|
||
export const SentenceFillTest: React.FC<FillTestProps> = ({
|
||
cardData,
|
||
onAnswer,
|
||
onReportError,
|
||
disabled = false
|
||
}) => {
|
||
// 使用共用邏輯 Hook
|
||
const {
|
||
userAnswer,
|
||
feedback,
|
||
isSubmitted,
|
||
setUserAnswer,
|
||
submitAnswer
|
||
} = useReviewLogic({
|
||
cardData,
|
||
testType: 'SentenceFillTest'
|
||
})
|
||
|
||
// 填空測試特定狀態
|
||
const [inputValue, setInputValue] = useState('')
|
||
|
||
// 獲取正確答案
|
||
const correctAnswer = useMemo(() => {
|
||
return getCorrectAnswer(cardData.filledQuestionText || cardData.example, cardData.word)
|
||
}, [cardData.filledQuestionText, cardData.example, cardData.word])
|
||
|
||
// 處理答案提交
|
||
const handleSubmit = () => {
|
||
if (isSubmitted || disabled || !inputValue.trim()) return
|
||
|
||
const result = submitAnswer(inputValue.trim())
|
||
onAnswer(inputValue.trim())
|
||
}
|
||
|
||
// 處理 Enter 鍵提交
|
||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||
if (e.key === 'Enter') {
|
||
handleSubmit()
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto">
|
||
{/* 詞卡標題 */}
|
||
<CardHeader
|
||
cardData={cardData}
|
||
showTranslation={false}
|
||
className="mb-8"
|
||
/>
|
||
|
||
<div className="bg-white rounded-xl shadow-lg p-8">
|
||
{/* 標題區 */}
|
||
<div className="flex justify-between items-start mb-6">
|
||
<h2 className="text-2xl font-bold text-gray-900">句子填空</h2>
|
||
</div>
|
||
|
||
{/* 填空句子顯示 */}
|
||
<div className="bg-blue-50 rounded-lg p-6 mb-6">
|
||
<h3 className="font-semibold text-gray-900 mb-3">完成下列句子</h3>
|
||
<div className="text-lg text-gray-800 leading-relaxed mb-4">
|
||
{(cardData.filledQuestionText || cardData.example).split('____').map((part, index, array) => (
|
||
<span key={index}>
|
||
{part}
|
||
{index < array.length - 1 && (
|
||
<span className="inline-block min-w-24 border-b-2 border-blue-400 mx-1 text-center">
|
||
<span className="text-blue-600 font-medium">____</span>
|
||
</span>
|
||
)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 答案輸入區 */}
|
||
<div className="mb-6">
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
你的答案:
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={inputValue}
|
||
onChange={(e) => setInputValue(e.target.value)}
|
||
onKeyPress={handleKeyPress}
|
||
disabled={disabled || isSubmitted}
|
||
placeholder="請輸入答案..."
|
||
className={`
|
||
w-full px-4 py-3 rounded-lg border-2 text-lg
|
||
${isSubmitted
|
||
? feedback?.isCorrect
|
||
? 'border-green-500 bg-green-50'
|
||
: 'border-red-500 bg-red-50'
|
||
: 'border-gray-300 focus:border-blue-500 focus:outline-none'
|
||
}
|
||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
||
`}
|
||
/>
|
||
</div>
|
||
|
||
{/* 提交按鈕 */}
|
||
{!isSubmitted && (
|
||
<div className="flex justify-center mb-6">
|
||
<button
|
||
onClick={handleSubmit}
|
||
disabled={disabled || !inputValue.trim() || isSubmitted}
|
||
className={`
|
||
px-8 py-3 rounded-lg font-medium text-white
|
||
${!inputValue.trim() || disabled
|
||
? 'bg-gray-400 cursor-not-allowed'
|
||
: 'bg-blue-600 hover:bg-blue-700 active:scale-95'
|
||
}
|
||
transition-all duration-200
|
||
`}
|
||
>
|
||
提交答案
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* 結果回饋 */}
|
||
{feedback && (
|
||
<div className={`p-4 rounded-lg mb-6 ${
|
||
feedback.isCorrect ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'
|
||
}`}>
|
||
<p className={`font-medium ${
|
||
feedback.isCorrect ? 'text-green-800' : 'text-red-800'
|
||
}`}>
|
||
{feedback.explanation}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 翻譯和音頻 */}
|
||
<div className="space-y-4">
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<h3 className="font-semibold text-gray-900 mb-2">中文翻譯</h3>
|
||
<p className="text-gray-700">{cardData.exampleTranslation}</p>
|
||
</div>
|
||
|
||
<AudioSection
|
||
word={cardData.word}
|
||
pronunciation={cardData.pronunciation}
|
||
className="justify-center"
|
||
/>
|
||
</div>
|
||
|
||
{/* 例句圖片 */}
|
||
{cardData.exampleImage && (
|
||
<div className="mt-6 text-center">
|
||
<img
|
||
src={cardData.exampleImage}
|
||
alt={`Example for ${cardData.word}`}
|
||
className="max-w-full h-auto rounded-lg shadow-md mx-auto"
|
||
style={{ maxHeight: '300px' }}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* 底部按鈕 */}
|
||
<div className="flex justify-center mt-6">
|
||
<ErrorReportButton
|
||
onClick={onReportError}
|
||
disabled={disabled}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// 向後相容包裝器
|
||
interface LegacySentenceFillTestProps {
|
||
word: string
|
||
definition: string
|
||
example: string
|
||
filledQuestionText?: string
|
||
exampleTranslation: string
|
||
pronunciation?: string
|
||
difficultyLevel: string
|
||
exampleImage?: string
|
||
onAnswer: (answer: string) => void
|
||
onReportError: () => void
|
||
onImageClick?: (image: string) => void
|
||
disabled?: boolean
|
||
}
|
||
|
||
export const SentenceFillTestLegacy: React.FC<LegacySentenceFillTestProps> = (props) => {
|
||
const cardData: ReviewCardData = {
|
||
id: `temp_${props.word}`,
|
||
word: props.word,
|
||
definition: props.definition,
|
||
example: props.example,
|
||
translation: props.exampleTranslation || '',
|
||
pronunciation: props.pronunciation,
|
||
synonyms: [],
|
||
difficultyLevel: props.difficultyLevel,
|
||
exampleTranslation: props.exampleTranslation,
|
||
filledQuestionText: props.filledQuestionText,
|
||
exampleImage: props.exampleImage
|
||
}
|
||
|
||
return (
|
||
<SentenceFillTest
|
||
cardData={cardData}
|
||
onAnswer={props.onAnswer}
|
||
onReportError={props.onReportError}
|
||
disabled={props.disabled}
|
||
/>
|
||
)
|
||
} |