From d742cf52f9af440492061f12caa83aecad23ea3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Thu, 2 Oct 2025 16:51:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20BluePlayButton=20=E5=85=A7=E5=BB=BA=20T?= =?UTF-8?q?TS=20=E9=82=8F=E8=BC=AF=E9=87=8D=E6=A7=8B=20+=20TypeScript=20?= =?UTF-8?q?=E9=8C=AF=E8=AA=A4=E4=BF=AE=E5=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重構亮點: • BluePlayButton 內建完整 TTS 播放邏輯 • 移除 8 個組件中 97 行重複代碼 • 組件使用極度簡化:複雜配置 → 一行代碼 技術優化: • 修復 TypeScript "Type 'never'" 錯誤 • 重新設計邏輯流程,清晰的條件分支 • 支援標準 TTS + 自定義播放兩種模式 使用簡化: • 從: • 到: 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../flashcards/FlashcardContentBlocks.tsx | 5 +- .../flashcards/FlashcardDetailHeader.tsx | 4 +- .../components/flashcards/FlashcardForm.tsx | 22 ------- .../components/generate/IdiomDetailModal.tsx | 22 ------- .../review/review-tests/FlipMemoryTest.tsx | 49 --------------- .../review-tests/SentenceListeningTest.tsx | 23 -------- frontend/components/shared/BluePlayButton.tsx | 59 ++++++++++++++++--- frontend/components/word/WordPopup.tsx | 22 ------- 8 files changed, 52 insertions(+), 154 deletions(-) diff --git a/frontend/components/flashcards/FlashcardContentBlocks.tsx b/frontend/components/flashcards/FlashcardContentBlocks.tsx index 9283842..c703023 100644 --- a/frontend/components/flashcards/FlashcardContentBlocks.tsx +++ b/frontend/components/flashcards/FlashcardContentBlocks.tsx @@ -146,11 +146,8 @@ export const FlashcardContentBlocks: React.FC = ({ diff --git a/frontend/components/flashcards/FlashcardDetailHeader.tsx b/frontend/components/flashcards/FlashcardDetailHeader.tsx index eec0b5f..16a20f5 100644 --- a/frontend/components/flashcards/FlashcardDetailHeader.tsx +++ b/frontend/components/flashcards/FlashcardDetailHeader.tsx @@ -30,11 +30,9 @@ export const FlashcardDetailHeader: React.FC = ({ diff --git a/frontend/components/flashcards/FlashcardForm.tsx b/frontend/components/flashcards/FlashcardForm.tsx index a36febd..f81f61f 100644 --- a/frontend/components/flashcards/FlashcardForm.tsx +++ b/frontend/components/flashcards/FlashcardForm.tsx @@ -13,26 +13,6 @@ interface FlashcardFormProps { } export function FlashcardForm({ initialData, isEdit = false, onSuccess, onCancel }: FlashcardFormProps) { - const [isPlayingWord, setIsPlayingWord] = useState(false) - - // TTS 播放邏輯 - const handleToggleWordTTS = (text: string, lang?: string) => { - if (isPlayingWord) { - speechSynthesis.cancel() - setIsPlayingWord(false) - return - } - - const utterance = new SpeechSynthesisUtterance(text) - utterance.lang = lang || 'en-US' - utterance.rate = 0.8 - - utterance.onstart = () => setIsPlayingWord(true) - utterance.onend = () => setIsPlayingWord(false) - utterance.onerror = () => setIsPlayingWord(false) - - speechSynthesis.speak(utterance) - } const [formData, setFormData] = useState({ word: initialData?.word || '', @@ -151,8 +131,6 @@ export function FlashcardForm({ initialData, isEdit = false, onSuccess, onCancel {formData.pronunciation && ( diff --git a/frontend/components/generate/IdiomDetailModal.tsx b/frontend/components/generate/IdiomDetailModal.tsx index 29f28fc..6811a65 100644 --- a/frontend/components/generate/IdiomDetailModal.tsx +++ b/frontend/components/generate/IdiomDetailModal.tsx @@ -34,32 +34,12 @@ export const IdiomDetailModal: React.FC = ({ onSaveIdiom, className = '' }) => { - const [isPlaying, setIsPlaying] = useState(false) - if (!idiomPopup) { return null } const { analysis } = idiomPopup - const handlePlayPronunciation = (text: string, lang?: string) => { - if (isPlaying) { - speechSynthesis.cancel() - setIsPlaying(false) - return - } - - const utterance = new SpeechSynthesisUtterance(text) - utterance.lang = lang || 'en-US' - utterance.rate = 0.8 - - utterance.onstart = () => setIsPlaying(true) - utterance.onend = () => setIsPlaying(false) - utterance.onerror = () => setIsPlaying(false) - - speechSynthesis.speak(utterance) - } - const handleSave = async () => { if (onSaveIdiom) { await onSaveIdiom(idiomPopup.idiom, analysis) @@ -81,8 +61,6 @@ export const IdiomDetailModal: React.FC = ({ diff --git a/frontend/components/review/review-tests/FlipMemoryTest.tsx b/frontend/components/review/review-tests/FlipMemoryTest.tsx index 716aec9..340a6ad 100644 --- a/frontend/components/review/review-tests/FlipMemoryTest.tsx +++ b/frontend/components/review/review-tests/FlipMemoryTest.tsx @@ -16,51 +16,6 @@ const FlipMemoryTestComponent: React.FC = ({ disabled = false }) => { const [isFlipped, setIsFlipped] = useState(false) - const [isPlayingWord, setIsPlayingWord] = useState(false) - const [isPlayingExample, setIsPlayingExample] = useState(false) - - // TTS 播放邏輯 - const handleToggleWordTTS = useCallback((text: string, lang?: string) => { - if (isPlayingWord) { - speechSynthesis.cancel() - setIsPlayingWord(false) - return - } - - setIsPlayingExample(false) - speechSynthesis.cancel() - - const utterance = new SpeechSynthesisUtterance(text) - utterance.lang = lang || 'en-US' - utterance.rate = 0.8 - - utterance.onstart = () => setIsPlayingWord(true) - utterance.onend = () => setIsPlayingWord(false) - utterance.onerror = () => setIsPlayingWord(false) - - speechSynthesis.speak(utterance) - }, [isPlayingWord]) - - const handleToggleExampleTTS = useCallback((text: string, lang?: string) => { - if (isPlayingExample) { - speechSynthesis.cancel() - setIsPlayingExample(false) - return - } - - setIsPlayingWord(false) - speechSynthesis.cancel() - - const utterance = new SpeechSynthesisUtterance(text) - utterance.lang = lang || 'en-US' - utterance.rate = 0.8 - - utterance.onstart = () => setIsPlayingExample(true) - utterance.onend = () => setIsPlayingExample(false) - utterance.onerror = () => setIsPlayingExample(false) - - speechSynthesis.speak(utterance) - }, [isPlayingExample]) const [selectedConfidence, setSelectedConfidence] = useState(null) const [cardHeight, setCardHeight] = useState(400) const frontRef = useRef(null) @@ -158,8 +113,6 @@ const FlipMemoryTestComponent: React.FC = ({
e.stopPropagation()}> @@ -201,8 +154,6 @@ const FlipMemoryTestComponent: React.FC = ({
e.stopPropagation()}> diff --git a/frontend/components/review/review-tests/SentenceListeningTest.tsx b/frontend/components/review/review-tests/SentenceListeningTest.tsx index 41a052f..da12d8a 100644 --- a/frontend/components/review/review-tests/SentenceListeningTest.tsx +++ b/frontend/components/review/review-tests/SentenceListeningTest.tsx @@ -23,8 +23,6 @@ const SentenceListeningTestComponent: React.FC = ({ }) => { const [selectedAnswer, setSelectedAnswer] = useState(null) const [showResult, setShowResult] = useState(false) - const [isPlayingExample, setIsPlayingExample] = useState(false) - // 判斷是否已答題(選擇了答案) const hasAnswered = selectedAnswer !== null @@ -37,32 +35,11 @@ const SentenceListeningTestComponent: React.FC = ({ const isCorrect = useMemo(() => selectedAnswer === cardData.example, [selectedAnswer, cardData.example]) - // TTS 播放邏輯 - const handleToggleTTS = useCallback((text: string, lang?: string) => { - if (isPlayingExample) { - speechSynthesis.cancel() - setIsPlayingExample(false) - return - } - - const utterance = new SpeechSynthesisUtterance(text) - utterance.lang = lang || 'en-US' - utterance.rate = 0.8 - - utterance.onstart = () => setIsPlayingExample(true) - utterance.onend = () => setIsPlayingExample(false) - utterance.onerror = () => setIsPlayingExample(false) - - speechSynthesis.speak(utterance) - }, [isPlayingExample]) - // 音頻播放區域 const audioArea = (
diff --git a/frontend/components/shared/BluePlayButton.tsx b/frontend/components/shared/BluePlayButton.tsx index 575d0b8..0b75f19 100644 --- a/frontend/components/shared/BluePlayButton.tsx +++ b/frontend/components/shared/BluePlayButton.tsx @@ -1,26 +1,28 @@ -import React from 'react' +import React, { useState } from 'react' interface BluePlayButtonProps { text?: string lang?: string - isPlaying: boolean - onToggle: (text: string, lang?: string) => void disabled?: boolean className?: string size?: 'sm' | 'md' | 'lg' title?: string + onPlayStart?: () => void + onPlayEnd?: () => void } export const BluePlayButton: React.FC = ({ text, lang = 'en-US', - isPlaying, - onToggle, disabled = false, className = '', size = 'md', - title + title, + onPlayStart, + onPlayEnd }) => { + const [isPlaying, setIsPlaying] = useState(false) + const sizeClasses = { sm: 'w-8 h-8', md: 'w-10 h-10', @@ -33,13 +35,52 @@ export const BluePlayButton: React.FC = ({ lg: 'w-6 h-6' } - const handleClick = () => { - onToggle(text || '', lang) + // 內建 TTS 邏輯 + const handleToggle = () => { + // 停止播放邏輯 + if (isPlaying) { + speechSynthesis.cancel() + setIsPlaying(false) + if (onPlayEnd) onPlayEnd() + return + } + + // 開始播放邏輯 + if (onPlayStart) { + // 自定義播放場景(如錄音回放) + setIsPlaying(true) + onPlayStart() + setTimeout(() => { + setIsPlaying(false) + if (onPlayEnd) onPlayEnd() + }, 3000) + } else if (text) { + // 標準 TTS 播放 + const utterance = new SpeechSynthesisUtterance(text) + utterance.lang = lang + utterance.rate = 0.8 + + utterance.onstart = () => { + setIsPlaying(true) + } + + utterance.onend = () => { + setIsPlaying(false) + if (onPlayEnd) onPlayEnd() + } + + utterance.onerror = () => { + setIsPlaying(false) + if (onPlayEnd) onPlayEnd() + } + + speechSynthesis.speak(utterance) + } } return (