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:
parent
2028a57a1e
commit
cb3309295b
|
|
@ -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('詞卡不存在')
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error || '詞卡不存在')
|
||||
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
|
||||
}
|
||||
|
||||
// 檢查詞彙是否有例句圖片 - 使用 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.'
|
||||
}
|
||||
return imageMap[word?.toLowerCase()] || '/images/examples/bring_up.png'
|
||||
|
||||
// 處理複合詞性 (如 "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>
|
||||
|
|
|
|||
|
|
@ -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,23 +43,17 @@ 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 暫時移除,將顯示新增按鈕
|
||||
}
|
||||
|
||||
// 只返回已確認存在的圖片,沒有則返回 null
|
||||
return imageMap[word?.toLowerCase()] || null
|
||||
// 例句圖片邏輯 - 使用 API 資料
|
||||
const getExampleImage = (card: Flashcard): string | null => {
|
||||
return card.primaryImageUrl || null
|
||||
}
|
||||
|
||||
// 檢查詞彙是否有例句圖片
|
||||
const hasExampleImage = (word: string): boolean => {
|
||||
return getExampleImage(word) !== null
|
||||
// 檢查詞彙是否有例句圖片 - 使用 API 資料
|
||||
const hasExampleImage = (card: Flashcard): boolean => {
|
||||
return card.hasExampleImage
|
||||
}
|
||||
|
||||
|
||||
// 處理AI生成例句圖片 (預留接口)
|
||||
const handleGenerateExampleImage = (card: Flashcard) => {
|
||||
console.log('準備為詞彙生成例句圖片:', card.word)
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue