feat: 完成前端詞卡圖片整合與詞性簡寫顯示

🎉 前端詞卡管理功能完全整合後端圖片資料

**圖片整合功能**:
-  更新Flashcard介面:添加exampleImages、hasExampleImage、primaryImageUrl欄位
-  取代硬編碼映射:getExampleImage和hasExampleImage改用API資料
-  詞卡列表頁面:完全使用動態圖片資料顯示
-  詞卡詳細頁面:修復資料載入邏輯使用列表API獲取圖片資訊

**詞性簡寫顯示**:
-  全域詞性轉換函數:getPartOfSpeechDisplay()
-  標準英語縮寫:noun→n., verb→v., adjective→adj.等
-  複合詞性處理:preposition/adverb→prep./adv.
-  應用到所有詞性顯示位置:列表和詳細頁面

**系統整合成果**:
-  完全移除硬編碼圖片映射依賴
-  前端直接使用後端API返回的圖片URL
-  支援AI生成圖片的即時顯示
-  Mock資料相容性:添加圖片欄位避免錯誤

前端詞卡管理系統現已完全整合AI圖片生成功能!

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-25 01:57:04 +08:00
parent 2028a57a1e
commit cb3309295b
3 changed files with 95 additions and 35 deletions

View File

@ -50,7 +50,11 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) {
cardSet: { name: '基礎詞彙', color: 'bg-blue-500' },
difficultyLevel: 'A1',
createdAt: '2025-09-17',
synonyms: ['hi', 'greetings', 'good day']
synonyms: ['hi', 'greetings', 'good day'],
// 添加圖片欄位
exampleImages: [],
hasExampleImage: false,
primaryImageUrl: null
},
'mock2': {
id: 'mock2',
@ -68,7 +72,11 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) {
cardSet: { name: '高級詞彙', color: 'bg-purple-500' },
difficultyLevel: 'B2',
createdAt: '2025-09-14',
synonyms: ['explain', 'detail', 'expand', 'clarify']
synonyms: ['explain', 'detail', 'expand', 'clarify'],
// 添加圖片欄位
exampleImages: [],
hasExampleImage: false,
primaryImageUrl: null
}
}
@ -86,13 +94,18 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) {
return
}
// 載入真實詞卡
const result = await flashcardsService.getFlashcard(cardId)
// 載入真實詞卡 - 使用列表 API 然後找到對應詞卡 (因為列表 API 有圖片資訊)
const result = await flashcardsService.getFlashcards()
if (result.success && result.data) {
setFlashcard(result.data)
setEditedCard(result.data)
const targetCard = result.data.flashcards.find(card => card.id === cardId)
if (targetCard) {
setFlashcard(targetCard)
setEditedCard(targetCard)
} else {
throw new Error(result.error || '詞卡不存在')
throw new Error('詞卡不存在')
}
} else {
throw new Error(result.error || '載入詞卡失敗')
}
} catch (err) {
setError('載入詞卡時發生錯誤')
@ -117,14 +130,34 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) {
}
}
// 獲取例句圖片
const getExampleImage = (word: string) => {
const imageMap: {[key: string]: string} = {
'hello': '/images/examples/bring_up.png',
'elaborate': '/images/examples/instinct.png',
'beautiful': '/images/examples/warrant.png'
// 獲取例句圖片 - 使用 API 資料
const getExampleImage = (card: Flashcard): string | null => {
return card.primaryImageUrl || null
}
return imageMap[word?.toLowerCase()] || '/images/examples/bring_up.png'
// 檢查詞彙是否有例句圖片 - 使用 API 資料
const hasExampleImage = (card: Flashcard): boolean => {
return card.hasExampleImage
}
// 詞性簡寫轉換
const getPartOfSpeechDisplay = (partOfSpeech: string): string => {
const shortMap: {[key: string]: string} = {
'noun': 'n.',
'verb': 'v.',
'adjective': 'adj.',
'adverb': 'adv.',
'preposition': 'prep.',
'interjection': 'int.',
'phrase': 'phr.'
}
// 處理複合詞性 (如 "preposition/adverb")
if (partOfSpeech?.includes('/')) {
return partOfSpeech.split('/').map(p => shortMap[p.trim()] || p.trim()).join('/')
}
return shortMap[partOfSpeech] || partOfSpeech || ''
}
// 處理收藏切換
@ -277,7 +310,7 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) {
<h1 className="text-4xl font-bold text-gray-900 mb-3">{flashcard.word}</h1>
<div className="flex items-center gap-3">
<span className="text-sm bg-gray-100 text-gray-700 px-3 py-1 rounded-full">
{flashcard.partOfSpeech}
{getPartOfSpeechDisplay(flashcard.partOfSpeech)}
</span>
<span className="text-lg text-gray-600">{flashcard.pronunciation}</span>
<button className="w-10 h-10 bg-blue-600 rounded-full flex items-center justify-center text-white hover:bg-blue-700 transition-colors">
@ -351,7 +384,7 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) {
{/* 例句圖片 */}
<div className="mb-4">
<img
src={getExampleImage(flashcard.word)}
src={getExampleImage(flashcard) || '/images/examples/bring_up.png'}
alt={`${flashcard.word} example`}
className="w-full max-w-md mx-auto rounded-lg border border-blue-300"
/>
@ -418,7 +451,7 @@ function FlashcardDetailContent({ cardId }: { cardId: string }) {
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">:</span>
<span className="ml-2 font-medium">{flashcard.partOfSpeech}</span>
<span className="ml-2 font-medium">{getPartOfSpeechDisplay(flashcard.partOfSpeech)}</span>
</div>
<div>
<span className="text-gray-600">:</span>

View File

@ -10,6 +10,26 @@ import { useToast } from '@/components/Toast'
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
import { useFlashcardSearch, type SearchActions } from '@/hooks/useFlashcardSearch'
// 詞性簡寫轉換 (全域函數)
const getPartOfSpeechDisplay = (partOfSpeech: string): string => {
const shortMap: {[key: string]: string} = {
'noun': 'n.',
'verb': 'v.',
'adjective': 'adj.',
'adverb': 'adv.',
'preposition': 'prep.',
'interjection': 'int.',
'phrase': 'phr.'
}
// 處理複合詞性 (如 "preposition/adverb")
if (partOfSpeech?.includes('/')) {
return partOfSpeech.split('/').map(p => shortMap[p.trim()] || p.trim()).join('/')
}
return shortMap[partOfSpeech] || partOfSpeech || ''
}
// 重構後的FlashcardsContent組件
function FlashcardsContent() {
const router = useRouter()
@ -23,22 +43,16 @@ function FlashcardsContent() {
// 使用新的搜尋Hook
const [searchState, searchActions] = useFlashcardSearch(activeTab)
// 例句圖片邏輯
const getExampleImage = (word: string): string | null => {
// 只列出真正有例句圖片的詞彙
const imageMap: {[key: string]: string} = {
'evidence': '/images/examples/bring_up.png',
// warrants 和 recovering 暫時移除,將顯示新增按鈕
// 例句圖片邏輯 - 使用 API 資料
const getExampleImage = (card: Flashcard): string | null => {
return card.primaryImageUrl || null
}
// 只返回已確認存在的圖片,沒有則返回 null
return imageMap[word?.toLowerCase()] || null
// 檢查詞彙是否有例句圖片 - 使用 API 資料
const hasExampleImage = (card: Flashcard): boolean => {
return card.hasExampleImage
}
// 檢查詞彙是否有例句圖片
const hasExampleImage = (word: string): boolean => {
return getExampleImage(word) !== null
}
// 處理AI生成例句圖片 (預留接口)
const handleGenerateExampleImage = (card: Flashcard) => {
@ -558,10 +572,10 @@ function FlashcardItem({ card, searchTerm, onEdit, onDelete, onToggleFavorite, g
<div className="flex flex-col md:flex-row md:items-center gap-3 md:gap-4 flex-1">
{/* 例句圖片區域 - 響應式設計 */}
<div className="w-32 h-20 sm:w-40 sm:h-24 md:w-48 md:h-32 lg:w-54 lg:h-36 bg-gray-100 rounded-lg overflow-hidden border border-gray-200 flex items-center justify-center flex-shrink-0">
{hasExampleImage(card.word) ? (
{hasExampleImage(card) ? (
// 有例句圖片時顯示圖片
<img
src={getExampleImage(card.word)!}
src={getExampleImage(card)!}
alt={`${card.word} example`}
className="w-full h-full object-cover"
onError={(e) => {
@ -601,7 +615,7 @@ function FlashcardItem({ card, searchTerm, onEdit, onDelete, onToggleFavorite, g
{searchTerm ? highlightSearchTerm(card.word || '未設定', searchTerm) : (card.word || '未設定')}
</h3>
<span className="text-sm bg-gray-100 text-gray-700 px-2 py-1 rounded">
{card.partOfSpeech || 'unknown'}
{getPartOfSpeechDisplay(card.partOfSpeech)}
</span>
</div>

View File

@ -1,5 +1,14 @@
// Flashcards API service
export interface ExampleImage {
id: string;
imageUrl: string;
isPrimary: boolean;
qualityScore?: number;
fileSize?: number;
createdAt: string;
}
export interface Flashcard {
id: string;
word: string;
@ -15,8 +24,12 @@ export interface Flashcard {
nextReviewDate: string;
difficultyLevel: string;
createdAt: string;
updatedAt?: string; // 設為可選,因為模擬資料可能沒有
// 移除 cardSet 屬性
updatedAt?: string;
// 新增圖片相關欄位
exampleImages: ExampleImage[];
hasExampleImage: boolean;
primaryImageUrl?: string;
}
export interface CreateFlashcardRequest {