206 lines
6.3 KiB
TypeScript
206 lines
6.3 KiB
TypeScript
import React, { useState, useMemo, useCallback, memo } from 'react'
|
||
import { getCorrectAnswer } from '@/utils/answerExtractor'
|
||
import {
|
||
TestResultDisplay,
|
||
HintPanel,
|
||
FillTestContainer,
|
||
TextInput
|
||
} from '@/components/review/shared'
|
||
import { FillTestProps } from '@/types/review'
|
||
|
||
interface SentenceFillTestProps extends FillTestProps {}
|
||
|
||
const SentenceFillTestComponent: React.FC<SentenceFillTestProps> = ({
|
||
cardData,
|
||
onAnswer,
|
||
onReportError,
|
||
disabled = false
|
||
}) => {
|
||
const [fillAnswer, setFillAnswer] = useState('')
|
||
const [showResult, setShowResult] = useState(false)
|
||
const [showHint, setShowHint] = useState(false)
|
||
|
||
|
||
const handleSubmit = useCallback((answer: string) => {
|
||
if (disabled || showResult) return
|
||
setShowResult(true)
|
||
onAnswer(answer)
|
||
}, [disabled, showResult, onAnswer])
|
||
|
||
const handleToggleHint = useCallback(() => {
|
||
setShowHint(prev => !prev)
|
||
}, [])
|
||
|
||
// 動態計算正確答案:從例句和挖空題目推導
|
||
const correctAnswer = useMemo(() => {
|
||
return getCorrectAnswer(cardData.example, cardData.filledQuestionText, cardData.word)
|
||
}, [cardData.example, cardData.filledQuestionText, cardData.word])
|
||
|
||
const isCorrect = useMemo(() => {
|
||
return fillAnswer.toLowerCase().trim() === correctAnswer.toLowerCase().trim()
|
||
}, [fillAnswer, correctAnswer])
|
||
|
||
// 統一的填空句子渲染邏輯
|
||
const renderFilledSentence = useCallback(() => {
|
||
const text = cardData.filledQuestionText || cardData.example
|
||
const isUsingFilledText = !!cardData.filledQuestionText
|
||
|
||
if (isUsingFilledText) {
|
||
// 使用後端提供的挖空題目
|
||
const parts = text.split('____')
|
||
return (
|
||
<div className="text-lg text-gray-700 leading-relaxed">
|
||
{parts.map((part, index) => (
|
||
<span key={index}>
|
||
{part}
|
||
{index < parts.length - 1 && (
|
||
<span className="inline-block mx-1">
|
||
<TextInput
|
||
value={fillAnswer}
|
||
onChange={setFillAnswer}
|
||
onSubmit={handleSubmit}
|
||
disabled={disabled}
|
||
showResult={showResult}
|
||
isCorrect={isCorrect}
|
||
correctAnswer={correctAnswer}
|
||
placeholder=""
|
||
/>
|
||
</span>
|
||
)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)
|
||
} else {
|
||
// 降級處理:使用前端挖空邏輯
|
||
const parts = text.split(new RegExp(`\\b${cardData.word}\\b`, 'gi'))
|
||
const matches = text.match(new RegExp(`\\b${cardData.word}\\b`, 'gi')) || []
|
||
|
||
return (
|
||
<div className="text-lg text-gray-700 leading-relaxed">
|
||
{parts.map((part, index) => (
|
||
<span key={index}>
|
||
{part}
|
||
{index < matches.length && (
|
||
<span className="inline-block mx-1">
|
||
<TextInput
|
||
value={fillAnswer}
|
||
onChange={setFillAnswer}
|
||
onSubmit={handleSubmit}
|
||
disabled={disabled}
|
||
showResult={showResult}
|
||
isCorrect={isCorrect}
|
||
correctAnswer={correctAnswer}
|
||
placeholder=""
|
||
/>
|
||
</span>
|
||
)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
}, [
|
||
cardData.filledQuestionText,
|
||
cardData.example,
|
||
cardData.word,
|
||
fillAnswer,
|
||
handleSubmit,
|
||
disabled,
|
||
showResult,
|
||
isCorrect,
|
||
correctAnswer
|
||
])
|
||
|
||
// 句子顯示區域
|
||
const sentenceArea = (
|
||
<div className="space-y-6">
|
||
{/* 圖片區(如果有) */}
|
||
{cardData.exampleImage && (
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<img
|
||
src={cardData.exampleImage}
|
||
alt="Example illustration"
|
||
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
|
||
onClick={() => {
|
||
// 圖片點擊處理 - 後續可以添加放大功能
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* 指示文字 */}
|
||
<p className="text-lg text-gray-700 text-left">
|
||
請點擊例句中的空白處輸入正確的單字:
|
||
</p>
|
||
|
||
{/* 填空句子區域 */}
|
||
<div className="bg-gray-50 rounded-lg p-6">
|
||
{renderFilledSentence()}
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
// 輸入區域(包含操作按鈕和提示)
|
||
const inputArea = (
|
||
<div className="space-y-4">
|
||
{/* 操作按鈕 */}
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => handleSubmit(fillAnswer)}
|
||
disabled={!fillAnswer.trim() || showResult}
|
||
className={`px-6 py-2 rounded-lg transition-colors ${
|
||
!fillAnswer.trim() || showResult
|
||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||
}`}
|
||
>
|
||
{!fillAnswer.trim() ? '請先輸入答案' : showResult ? '已確認' : '確認答案'}
|
||
</button>
|
||
<button
|
||
onClick={handleToggleHint}
|
||
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
|
||
>
|
||
{showHint ? '隱藏提示' : '顯示提示'}
|
||
</button>
|
||
</div>
|
||
|
||
{/* 提示區域 */}
|
||
<HintPanel
|
||
isVisible={showHint}
|
||
definition={cardData.definition}
|
||
synonyms={cardData.synonyms}
|
||
/>
|
||
</div>
|
||
)
|
||
|
||
// 結果顯示區域
|
||
const resultArea = showResult ? (
|
||
<TestResultDisplay
|
||
isCorrect={isCorrect}
|
||
correctAnswer={correctAnswer}
|
||
userAnswer={fillAnswer}
|
||
word={cardData.word}
|
||
pronunciation={cardData.pronunciation}
|
||
example={cardData.example}
|
||
exampleTranslation={cardData.exampleTranslation}
|
||
showResult={showResult}
|
||
/>
|
||
) : null
|
||
|
||
return (
|
||
<FillTestContainer
|
||
cardData={cardData}
|
||
testTitle="例句填空"
|
||
sentenceArea={sentenceArea}
|
||
inputArea={inputArea}
|
||
resultArea={resultArea}
|
||
onAnswer={onAnswer}
|
||
onReportError={onReportError}
|
||
disabled={disabled}
|
||
/>
|
||
)
|
||
}
|
||
|
||
export const SentenceFillTest = memo(SentenceFillTestComponent)
|
||
SentenceFillTest.displayName = 'SentenceFillTest' |