From 738d83609994c5ad11da945ae789d6853b772fc5 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 00:09:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E8=A9=9E=E5=8D=A1?= =?UTF-8?q?=E8=A9=B3=E6=83=85=E9=A0=81=E9=87=8D=E6=A7=8B=20-=20=E6=A8=A1?= =?UTF-8?q?=E7=B5=84=E5=8C=96=E6=9E=B6=E6=A7=8B=E5=A4=A7=E5=B9=85=E5=84=AA?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重構成果: - 主檔案代碼減少64% (543→193行) - 新建5個UI組件 + 2個Custom Hooks - 業務邏輯與UI完全分離 - TypeScript類型安全,編譯無錯誤 - 組件可重用性大幅提升 新建組件: - LoadingState: 統一載入狀態 - ErrorState: 統一錯誤處理 - FlashcardInfoBlock: 詞卡資訊區塊 - FlashcardActions: 操作按鈕組 - EditingControls: 編輯模式控制 新建Hooks: - useFlashcardActions: 詞卡操作邏輯 - useImageGeneration: 圖片生成邏輯 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/flashcards/[id]/page.tsx | 494 +++--------------- .../components/flashcards/EditingControls.tsx | 48 ++ frontend/components/flashcards/ErrorState.tsx | 52 ++ .../flashcards/FlashcardActions.tsx | 88 ++++ .../flashcards/FlashcardInfoBlock.tsx | 89 ++++ .../components/flashcards/LoadingState.tsx | 20 + .../hooks/flashcards/useFlashcardActions.ts | 129 +++++ .../hooks/flashcards/useImageGeneration.ts | 69 +++ 詞卡詳情頁重構計劃.md | 271 ++++++++++ 9 files changed, 838 insertions(+), 422 deletions(-) create mode 100644 frontend/components/flashcards/EditingControls.tsx create mode 100644 frontend/components/flashcards/ErrorState.tsx create mode 100644 frontend/components/flashcards/FlashcardActions.tsx create mode 100644 frontend/components/flashcards/FlashcardInfoBlock.tsx create mode 100644 frontend/components/flashcards/LoadingState.tsx create mode 100644 frontend/hooks/flashcards/useFlashcardActions.ts create mode 100644 frontend/hooks/flashcards/useImageGeneration.ts create mode 100644 詞卡詳情頁重構計劃.md diff --git a/frontend/app/flashcards/[id]/page.tsx b/frontend/app/flashcards/[id]/page.tsx index d32dda2..9436151 100644 --- a/frontend/app/flashcards/[id]/page.tsx +++ b/frontend/app/flashcards/[id]/page.tsx @@ -1,18 +1,22 @@ 'use client' -import { useState, useEffect, use } from 'react' -import { useRouter, useSearchParams } from 'next/navigation' +import { use } from 'react' +import { useRouter } from 'next/navigation' import { Navigation } from '@/components/shared/Navigation' import { ProtectedRoute } from '@/components/shared/ProtectedRoute' import { useToast } from '@/components/shared/Toast' -import { flashcardsService, type Flashcard } from '@/lib/services/flashcards' -import { imageGenerationService } from '@/lib/services/imageGeneration' -import { getPartOfSpeechDisplay, getCEFRColor, getFlashcardImageUrl } from '@/lib/utils/flashcardUtils' +import { getCEFRColor } from '@/lib/utils/flashcardUtils' import { useTTSPlayer } from '@/hooks/shared/useTTSPlayer' import { useFlashcardDetailData } from '@/hooks/flashcards/useFlashcardDetailData' -import { TTSButton } from '@/components/shared/TTSButton' +import { useFlashcardActions } from '@/hooks/flashcards/useFlashcardActions' +import { useImageGeneration } from '@/hooks/flashcards/useImageGeneration' import { FlashcardDetailHeader } from '@/components/flashcards/FlashcardDetailHeader' import { FlashcardContentBlocks } from '@/components/flashcards/FlashcardContentBlocks' +import { FlashcardInfoBlock } from '@/components/flashcards/FlashcardInfoBlock' +import { FlashcardActions } from '@/components/flashcards/FlashcardActions' +import { EditingControls } from '@/components/flashcards/EditingControls' +import { LoadingState } from '@/components/flashcards/LoadingState' +import { ErrorState } from '@/components/flashcards/ErrorState' interface FlashcardDetailPageProps { params: Promise<{ @@ -46,184 +50,58 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { setEditedCard } = useFlashcardDetailData(cardId) - // 圖片生成狀態 - const [isGeneratingImage, setIsGeneratingImage] = useState(false) - const [generationProgress, setGenerationProgress] = useState('') - // 使用TTS Hook const { isPlayingWord, isPlayingExample, toggleWordTTS, toggleExampleTTS } = useTTSPlayer() + // 使用業務邏輯Hooks + const { toggleFavorite, saveEdit, deleteFlashcard, isLoading: isActionLoading } = useFlashcardActions({ + flashcard, + editedCard, + onFlashcardUpdate: setFlashcard, + onEditingChange: setIsEditing + }) + + const { generateImage, isGenerating: isGeneratingImage, progress: generationProgress } = useImageGeneration({ + flashcard, + onFlashcardUpdate: setFlashcard + }) + // 編輯變更處理函數 const handleEditChange = (field: string, value: string) => { setEditedCard((prev: any) => ({ ...prev, [field]: value })) } - - - - - - - - // 處理收藏切換 - const handleToggleFavorite = async () => { - if (!flashcard) return - - try { - // 假資料處理 - if (flashcard.id.startsWith('mock')) { - const updated = { ...flashcard, isFavorite: !flashcard.isFavorite } - setFlashcard(updated) - setEditedCard(updated) - toast.success(`${flashcard.isFavorite ? '已取消收藏' : '已加入收藏'}「${flashcard.word}」`) - return - } - - // 真實API調用 - const result = await flashcardsService.toggleFavorite(flashcard.id) - if (result.success) { - setFlashcard(prev => prev ? { ...prev, isFavorite: !prev.isFavorite } : null) - toast.success(`${flashcard.isFavorite ? '已取消收藏' : '已加入收藏'}「${flashcard.word}」`) - } - } catch (error) { - toast.error('操作失敗,請重試') + // 編輯操作處理 + const handleToggleEdit = () => { + if (isEditing) { + setEditedCard(flashcard) } + setIsEditing(!isEditing) } - // 處理編輯保存 - const handleSaveEdit = async () => { - if (!flashcard || !editedCard) return - - try { - // 假資料處理 - if (flashcard.id.startsWith('mock')) { - setFlashcard(editedCard) - setIsEditing(false) - toast.success('詞卡更新成功!') - return - } - - // 真實API調用 - const result = await flashcardsService.updateFlashcard(flashcard.id, { - word: editedCard.word, - translation: editedCard.translation, - definition: editedCard.definition, - pronunciation: editedCard.pronunciation, - partOfSpeech: editedCard.partOfSpeech, - example: editedCard.example, - exampleTranslation: editedCard.exampleTranslation, - cefr: editedCard.cefr - }) - - if (result.success) { - setFlashcard(editedCard) - setIsEditing(false) - toast.success('詞卡更新成功!') - } else { - toast.error(result.error || '更新失敗') - } - } catch (error) { - toast.error('更新失敗,請重試') - } + const handleCancelEdit = () => { + setIsEditing(false) + setEditedCard(flashcard) } - // 處理刪除 - const handleDelete = async () => { - if (!flashcard) return - if (!confirm(`確定要刪除詞卡「${flashcard.word}」嗎?`)) { - return - } - try { - // 假資料處理 - if (flashcard.id.startsWith('mock')) { - toast.success('詞卡已刪除(模擬)') - router.push('/flashcards') - return - } - // 真實API調用 - const result = await flashcardsService.deleteFlashcard(flashcard.id) - if (result.success) { - toast.success('詞卡已刪除') - router.push('/flashcards') - } else { - toast.error(result.error || '刪除失敗') - } - } catch (error) { - toast.error('刪除失敗,請重試') - } - } - // 處理圖片生成 - const handleGenerateImage = async () => { - if (!flashcard || isGeneratingImage) return - try { - setIsGeneratingImage(true) - setGenerationProgress('啟動生成中...') - toast.info(`開始為「${flashcard.word}」生成例句圖片...`) - const generateResult = await imageGenerationService.generateImage(flashcard.id) - if (!generateResult.success || !generateResult.data) { - throw new Error(generateResult.error || '啟動生成失敗') - } - const requestId = generateResult.data.requestId - setGenerationProgress('Gemini 生成描述中...') - - const finalStatus = await imageGenerationService.pollUntilComplete( - requestId, - (status: any) => { - const stage = status.stages.gemini.status === 'completed' - ? 'Replicate 生成圖片中...' : 'Gemini 生成描述中...' - setGenerationProgress(stage) - }, - 5 - ) - - if (finalStatus.overallStatus === 'completed') { - setGenerationProgress('生成完成,載入中...') - // 重新載入詞卡資料 - const result = await flashcardsService.getFlashcard(cardId) - if (result.success && result.data) { - setFlashcard(result.data) - setEditedCard(result.data) - } - toast.success(`「${flashcard.word}」的例句圖片生成完成!`) - } else { - throw new Error('圖片生成未完成') - } - } catch (error: any) { - toast.error(`圖片生成失敗: ${error.message || '未知錯誤'}`) - } finally { - setIsGeneratingImage(false) - setGenerationProgress('') - } - } if (loading) { - return ( -
-
載入中...
-
- ) + return } if (error || !flashcard) { return ( -
-
-
{error || '詞卡不存在'}
- -
-
+ router.push('/flashcards')} + /> ) } @@ -247,7 +125,7 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { - {/* 主要詞卡內容 - 學習功能風格 */} + {/* 主要詞卡內容 */}
{/* CEFR標籤 - 右上角 */}
@@ -264,276 +142,48 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { onToggleWordTTS={toggleWordTTS} /> - {/* 內容區 - 學習卡片風格 */} -
- {/* 翻譯區塊 */} -
-

中文翻譯

- {isEditing ? ( - setEditedCard((prev: any) => ({ ...prev, translation: e.target.value }))} - className="w-full p-3 border border-green-300 rounded-lg focus:ring-2 focus:ring-green-500 bg-white" - placeholder="輸入中文翻譯" - /> - ) : ( -

- {flashcard.translation} -

- )} -
+ {/* 內容區塊 */} + - {/* 定義區塊 */} -
-

英文定義

- {isEditing ? ( -