From 1661eccf24ba1c0b14106ee583ed85133155b79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Thu, 25 Sep 2025 09:09:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B9=E9=80=B2=E8=A9=9E=E5=8D=A1?= =?UTF-8?q?=E7=B7=A8=E8=BC=AF=E6=B5=81=E7=A8=8B=EF=BC=8C=E5=BE=9E=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E5=B0=8E=E8=88=AA=E5=88=B0=E8=A9=B3=E7=B4=B0=E9=A0=81?= =?UTF-8?q?=E9=9D=A2=E7=B7=A8=E8=BC=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ UX改進: - 點擊列表中的編輯按鈕直接導航到詳細頁面 - 詳細頁面自動開啟編輯模式,提供專注的編輯環境 - 移除列表頁面底部的編輯表單,簡化界面 🔧 技術實作: - 使用URL參數(?edit=true)傳遞編輯狀態 - 詳細頁面檢查URL參數自動開啟編輯模式 - 清理不必要的編輯表單狀態管理 🚀 編輯體驗提升: - 在詳細頁面編輯,享有完整功能(圖片生成、統計資訊等) - 避免在列表頁面編輯時的干擾和空間限制 - 統一所有編輯操作在同一位置進行 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/flashcards/[id]/page.tsx | 128 ++++++++++++++++++++++---- frontend/app/flashcards/page.tsx | 33 +------ 2 files changed, 112 insertions(+), 49 deletions(-) diff --git a/frontend/app/flashcards/[id]/page.tsx b/frontend/app/flashcards/[id]/page.tsx index 3fd50e4..f94fe15 100644 --- a/frontend/app/flashcards/[id]/page.tsx +++ b/frontend/app/flashcards/[id]/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useState, useEffect, use } from 'react' -import { useRouter } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import { Navigation } from '@/components/Navigation' import { ProtectedRoute } from '@/components/ProtectedRoute' import { useToast } from '@/components/Toast' @@ -25,6 +25,7 @@ export default function FlashcardDetailPage({ params }: FlashcardDetailPageProps function FlashcardDetailContent({ cardId }: { cardId: string }) { const router = useRouter() + const searchParams = useSearchParams() const toast = useToast() const [flashcard, setFlashcard] = useState(null) const [loading, setLoading] = useState(true) @@ -32,6 +33,10 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { const [isEditing, setIsEditing] = useState(false) const [editedCard, setEditedCard] = useState(null) + // 圖片生成狀態 + const [isGeneratingImage, setIsGeneratingImage] = useState(false) + const [generationProgress, setGenerationProgress] = useState('') + // 假資料 - 用於展示效果 const mockCards: {[key: string]: any} = { 'mock1': { @@ -94,18 +99,13 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { return } - // 載入真實詞卡 - 使用列表 API 然後找到對應詞卡 (因為列表 API 有圖片資訊) - const result = await flashcardsService.getFlashcards() + // 載入真實詞卡 - 使用直接 API 調用 + const result = await flashcardsService.getFlashcard(cardId) if (result.success && result.data) { - const targetCard = result.data.flashcards.find(card => card.id === cardId) - if (targetCard) { - setFlashcard(targetCard) - setEditedCard(targetCard) - } else { - throw new Error('詞卡不存在') - } + setFlashcard(result.data) + setEditedCard(result.data) } else { - throw new Error(result.error || '載入詞卡失敗') + throw new Error(result.error || '詞卡不存在') } } catch (err) { setError('載入詞卡時發生錯誤') @@ -117,6 +117,15 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { loadFlashcard() }, [cardId]) + // 檢查 URL 參數,自動開啟編輯模式 + useEffect(() => { + if (searchParams.get('edit') === 'true' && flashcard) { + setIsEditing(true) + // 清理 URL 參數,保持 URL 乾淨 + router.replace(`/flashcards/${cardId}`) + } + }, [flashcard, searchParams, cardId, router]) + // 獲取CEFR等級顏色 const getCEFRColor = (level: string) => { switch (level) { @@ -251,6 +260,53 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) { } } + // 處理圖片生成 + 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) => { + 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 (
@@ -382,12 +438,50 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) {

例句

{/* 例句圖片 */} -
- {`${flashcard.word} +
+ {getExampleImage(flashcard) ? ( + {`${flashcard.word} + ) : ( +
+
+ + + +

尚無例句圖片

+ +
+
+ )} + + {/* 圖片上的生成按鈕 */} + {getExampleImage(flashcard) && !isGeneratingImage && ( + + )} + + {/* 生成進度覆蓋 */} + {isGeneratingImage && getExampleImage(flashcard) && ( +
+
+
+

{generationProgress}

+
+
+ )}
diff --git a/frontend/app/flashcards/page.tsx b/frontend/app/flashcards/page.tsx index a1e52b3..9e4f5a6 100644 --- a/frontend/app/flashcards/page.tsx +++ b/frontend/app/flashcards/page.tsx @@ -36,8 +36,6 @@ function FlashcardsContent() { const router = useRouter() const toast = useToast() const [activeTab, setActiveTab] = useState<'all-cards' | 'favorites'>('all-cards') - const [showForm, setShowForm] = useState(false) - const [editingCard, setEditingCard] = useState(null) const [showAdvancedSearch, setShowAdvancedSearch] = useState(false) const [totalCounts, setTotalCounts] = useState({ all: 0, favorites: 0 }) @@ -150,17 +148,9 @@ function FlashcardsContent() { } } - // 處理表單操作 - const handleFormSuccess = async () => { - setShowForm(false) - setEditingCard(null) - await searchActions.refresh() - await loadTotalCounts() - } const handleEdit = (card: Flashcard) => { - setEditingCard(card) - setShowForm(true) + router.push(`/flashcards/${card.id}?edit=true`) } const handleDelete = async (card: Flashcard) => { @@ -345,27 +335,6 @@ function FlashcardsContent() { />
- {/* Form Modal */} - {showForm && ( - { - setShowForm(false) - setEditingCard(null) - }} - /> - )} {/* Toast */}