feat: 完善詞卡管理頁面的搜尋和交互體驗

- 實現進階搜尋功能,支援CEFR等級、詞性、掌握度、收藏狀態篩選
- 新增搜尋結果高亮顯示,關鍵字會被黃色標記
- 重新設計右側操作按鈕,增大尺寸提升點擊體驗
- 修正tab高亮邏輯,避免多個tab同時亮起的問題
- 優化卡片交互邏輯,移除整卡點擊,只保留右側導航按鈕
- 修正例句圖片映射邏輯,確保所有詞卡都有對應圖片
- 添加完整的假資料展示六個CEFR等級效果
- 實現快速篩選按鈕,一鍵篩選常用條件
- 修正TypeScript類型錯誤,確保編譯正常

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-20 18:19:31 +08:00
parent 4e69030bc2
commit 33b291b505
1 changed files with 369 additions and 73 deletions

View File

@ -11,6 +11,13 @@ function FlashcardsContent() {
const [activeTab, setActiveTab] = useState('my-cards')
const [selectedSet, setSelectedSet] = useState<string | null>(null)
const [searchTerm, setSearchTerm] = useState('')
const [showAdvancedSearch, setShowAdvancedSearch] = useState(false)
const [searchFilters, setSearchFilters] = useState({
cefrLevel: '',
partOfSpeech: '',
masteryLevel: '',
onlyFavorites: false
})
// Real data from API
const [cardSets, setCardSets] = useState<CardSet[]>([])
@ -20,18 +27,34 @@ function FlashcardsContent() {
// 臨時使用學習功能的例句圖片作為測試
const getExampleImage = (word: string): string => {
const availableImages = [
'/images/examples/bring_up.png',
'/images/examples/instinct.png',
'/images/examples/warrant.png'
]
const imageMap: {[key: string]: string} = {
'brought': '/images/examples/bring_up.png',
'instincts': '/images/examples/instinct.png',
'warrants': '/images/examples/warrant.png',
'elaborate': '/images/examples/bring_up.png', // 作為預設
'hello': '/images/examples/instinct.png',
'good': '/images/examples/warrant.png'
'hello': '/images/examples/bring_up.png',
'beautiful': '/images/examples/instinct.png',
'understand': '/images/examples/warrant.png',
'elaborate': '/images/examples/bring_up.png',
'sophisticated': '/images/examples/instinct.png',
'ubiquitous': '/images/examples/warrant.png'
}
// 根據詞彙返回對應圖片,如果沒有則返回隨機圖片
return imageMap[word?.toLowerCase()] ||
imageMap[Object.keys(imageMap)[Math.floor(Math.random() * Object.keys(imageMap).length)]]
// 根據詞彙返回對應圖片,如果沒有則根據字母分配
const mappedImage = imageMap[word?.toLowerCase()]
if (mappedImage) return mappedImage
// 根據首字母分配圖片
const firstChar = (word || 'a')[0].toLowerCase()
const charCode = firstChar.charCodeAt(0) - 97 // a=0, b=1, c=2...
const imageIndex = charCode % availableImages.length
return availableImages[imageIndex]
}
// Form states
@ -40,12 +63,12 @@ function FlashcardsContent() {
// 添加假資料用於展示CEFR效果
const mockFlashcards = [
{ id: 'mock1', word: 'hello', translation: '你好', partOfSpeech: 'interjection', pronunciation: '/həˈloʊ/', masteryLevel: 95, timesReviewed: 15, isFavorite: true, nextReviewDate: '2025-09-21', cardSet: { name: '基礎詞彙', color: 'bg-blue-500' }, difficultyLevel: 'A1' },
{ id: 'mock2', word: 'beautiful', translation: '美麗的', partOfSpeech: 'adjective', pronunciation: '/ˈbjuːtɪfəl/', masteryLevel: 78, timesReviewed: 8, isFavorite: false, nextReviewDate: '2025-09-22', cardSet: { name: '描述詞彙', color: 'bg-green-500' }, difficultyLevel: 'A2' },
{ id: 'mock3', word: 'understand', translation: '理解', partOfSpeech: 'verb', pronunciation: '/ˌʌndərˈstænd/', masteryLevel: 65, timesReviewed: 12, isFavorite: true, nextReviewDate: '2025-09-20', cardSet: { name: '常用動詞', color: 'bg-yellow-500' }, difficultyLevel: 'B1' },
{ id: 'mock4', word: 'elaborate', translation: '詳細說明', partOfSpeech: 'verb', pronunciation: '/ɪˈlæbərət/', masteryLevel: 45, timesReviewed: 5, isFavorite: false, nextReviewDate: '2025-09-19', cardSet: { name: '高級詞彙', color: 'bg-purple-500' }, difficultyLevel: 'B2' },
{ id: 'mock5', word: 'sophisticated', translation: '精密的', partOfSpeech: 'adjective', pronunciation: '/səˈfɪstɪkeɪtɪd/', masteryLevel: 30, timesReviewed: 3, isFavorite: true, nextReviewDate: '2025-09-18', cardSet: { name: '進階詞彙', color: 'bg-indigo-500' }, difficultyLevel: 'C1' },
{ id: 'mock6', word: 'ubiquitous', translation: '無處不在的', partOfSpeech: 'adjective', pronunciation: '/juːˈbɪkwɪtəs/', masteryLevel: 15, timesReviewed: 1, isFavorite: false, nextReviewDate: '2025-09-17', cardSet: { name: '學術詞彙', color: 'bg-red-500' }, difficultyLevel: 'C2' }
{ id: 'mock1', word: 'hello', translation: '你好', partOfSpeech: 'interjection', pronunciation: '/həˈloʊ/', masteryLevel: 95, timesReviewed: 15, isFavorite: true, nextReviewDate: '2025-09-21', cardSet: { name: '基礎詞彙', color: 'bg-blue-500' }, difficultyLevel: 'A1', definition: 'A greeting word', example: 'Hello, how are you?', createdAt: '2025-09-17' },
{ id: 'mock2', word: 'beautiful', translation: '美麗的', partOfSpeech: 'adjective', pronunciation: '/ˈbjuːtɪfəl/', masteryLevel: 78, timesReviewed: 8, isFavorite: false, nextReviewDate: '2025-09-22', cardSet: { name: '描述詞彙', color: 'bg-green-500' }, difficultyLevel: 'A2', definition: 'Pleasing to look at', example: 'The beautiful sunset', createdAt: '2025-09-16' },
{ id: 'mock3', word: 'understand', translation: '理解', partOfSpeech: 'verb', pronunciation: '/ˌʌndərˈstænd/', masteryLevel: 65, timesReviewed: 12, isFavorite: true, nextReviewDate: '2025-09-20', cardSet: { name: '常用動詞', color: 'bg-yellow-500' }, difficultyLevel: 'B1', definition: 'To comprehend', example: 'I understand the concept', createdAt: '2025-09-15' },
{ id: 'mock4', word: 'elaborate', translation: '詳細說明', partOfSpeech: 'verb', pronunciation: '/ɪˈlæbərət/', masteryLevel: 45, timesReviewed: 5, isFavorite: false, nextReviewDate: '2025-09-19', cardSet: { name: '高級詞彙', color: 'bg-purple-500' }, difficultyLevel: 'B2', definition: 'To explain in detail', example: 'Please elaborate on your idea', createdAt: '2025-09-14' },
{ id: 'mock5', word: 'sophisticated', translation: '精密的', partOfSpeech: 'adjective', pronunciation: '/səˈfɪstɪkeɪtɪd/', masteryLevel: 30, timesReviewed: 3, isFavorite: true, nextReviewDate: '2025-09-18', cardSet: { name: '進階詞彙', color: 'bg-indigo-500' }, difficultyLevel: 'C1', definition: 'Highly developed', example: 'A sophisticated system', createdAt: '2025-09-13' },
{ id: 'mock6', word: 'ubiquitous', translation: '無處不在的', partOfSpeech: 'adjective', pronunciation: '/juːˈbɪkwɪtəs/', masteryLevel: 15, timesReviewed: 1, isFavorite: false, nextReviewDate: '2025-09-17', cardSet: { name: '學術詞彙', color: 'bg-red-500' }, difficultyLevel: 'C2', definition: 'Present everywhere', example: 'Smartphones are ubiquitous', createdAt: '2025-09-12' }
]
// Load data from API
@ -182,14 +205,82 @@ function FlashcardsContent() {
)
const allCards = [...flashcards, ...mockFlashcards] // 合併真實和假資料
// 進階搜尋邏輯
const filteredCards = allCards.filter(card => {
// 基本文字搜尋
if (searchTerm) {
return card.word?.toLowerCase().includes(searchTerm.toLowerCase()) ||
card.translation?.toLowerCase().includes(searchTerm.toLowerCase())
const searchLower = searchTerm.toLowerCase()
const matchesText =
card.word?.toLowerCase().includes(searchLower) ||
card.translation?.toLowerCase().includes(searchLower) ||
card.definition?.toLowerCase().includes(searchLower)
if (!matchesText) return false
}
// CEFR等級篩選
if (searchFilters.cefrLevel && (card as any).difficultyLevel !== searchFilters.cefrLevel) {
return false
}
// 詞性篩選
if (searchFilters.partOfSpeech && card.partOfSpeech !== searchFilters.partOfSpeech) {
return false
}
// 掌握度篩選
if (searchFilters.masteryLevel) {
const mastery = card.masteryLevel || 0
if (searchFilters.masteryLevel === 'high' && mastery < 80) return false
if (searchFilters.masteryLevel === 'medium' && (mastery < 60 || mastery >= 80)) return false
if (searchFilters.masteryLevel === 'low' && mastery >= 60) return false
}
// 收藏篩選
if (searchFilters.onlyFavorites && !card.isFavorite) {
return false
}
return true
})
// 清除所有篩選
const clearAllFilters = () => {
setSearchTerm('')
setSearchFilters({
cefrLevel: '',
partOfSpeech: '',
masteryLevel: '',
onlyFavorites: false
})
}
// 檢查是否有活動篩選
const hasActiveFilters = searchTerm ||
searchFilters.cefrLevel ||
searchFilters.partOfSpeech ||
searchFilters.masteryLevel ||
searchFilters.onlyFavorites
// 搜尋結果高亮函數
const highlightSearchTerm = (text: string, searchTerm: string) => {
if (!searchTerm || !text) return text
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
const parts = text.split(regex)
return parts.map((part, index) =>
regex.test(part) ? (
<mark key={index} className="bg-yellow-200 text-yellow-900 px-1 rounded">
{part}
</mark>
) : (
part
)
)
}
// Add loading and error states
if (loading) {
return (
@ -218,7 +309,6 @@ function FlashcardsContent() {
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold text-gray-900"></h1>
<p className="mt-1 text-sm text-gray-500"></p>
</div>
<div className="flex items-center space-x-4">
<button
@ -239,7 +329,10 @@ function FlashcardsContent() {
{/* Tabs */}
<div className="flex space-x-8 mb-6 border-b border-gray-200">
<button
onClick={() => setActiveTab('my-cards')}
onClick={() => {
setActiveTab('my-cards')
setSelectedSet(null) // 清除卡組選擇
}}
className={`pb-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'my-cards'
? 'border-primary text-primary'
@ -249,9 +342,12 @@ function FlashcardsContent() {
({filteredSets.length})
</button>
<button
onClick={() => setActiveTab('all-cards')}
onClick={() => {
setActiveTab('all-cards')
setSelectedSet(null) // 清除卡組選擇
}}
className={`pb-4 px-1 border-b-2 font-medium text-sm ${
activeTab === 'all-cards'
activeTab === 'all-cards' && !selectedSet
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
@ -259,7 +355,10 @@ function FlashcardsContent() {
({filteredCards.length})
</button>
<button
onClick={() => setActiveTab('favorites')}
onClick={() => {
setActiveTab('favorites')
setSelectedSet(null) // 清除卡組選擇
}}
className={`pb-4 px-1 border-b-2 font-medium text-sm flex items-center gap-1 ${
activeTab === 'favorites'
? 'border-primary text-primary'
@ -278,7 +377,7 @@ function FlashcardsContent() {
}
}}
className={`pb-4 px-1 border-b-2 font-medium text-sm ${
selectedSet && cardSets.find(set => set.id === selectedSet)?.isDefault
activeTab === 'all-cards' && selectedSet && cardSets.find(set => set.id === selectedSet)?.isDefault
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
@ -286,22 +385,188 @@ function FlashcardsContent() {
📂
</button>
</div>
{/* Search */}
<div className="mb-6">
<div className="relative">
{/* 進階搜尋區域 */}
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900"></h2>
<button
onClick={() => setShowAdvancedSearch(!showAdvancedSearch)}
className="text-sm text-blue-600 hover:text-blue-700 font-medium flex items-center gap-1"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
{showAdvancedSearch ? '收起篩選' : '進階篩選'}
</button>
</div>
{/* 主要搜尋框 */}
<div className="relative mb-4">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜尋詞卡或卡組..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="搜尋詞彙、翻譯或定義..."
className="w-full pl-12 pr-20 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent text-base"
onKeyDown={(e) => {
if (e.key === 'Escape') {
setSearchTerm('')
}
}}
/>
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
{(searchTerm || hasActiveFilters) && (
<div className="absolute inset-y-0 right-0 pr-3 flex items-center gap-2">
<span className="text-xs text-gray-500 bg-blue-100 px-2 py-1 rounded-full">
{filteredCards.length}
</span>
<button
onClick={clearAllFilters}
className="text-gray-400 hover:text-gray-600 p-1 rounded-full hover:bg-gray-100 transition-colors"
title="清除搜尋"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
)}
</div>
{/* 進階篩選選項 */}
{showAdvancedSearch && (
<div className="bg-gray-50 rounded-lg p-4 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* CEFR等級篩選 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">CEFR等級</label>
<select
value={searchFilters.cefrLevel}
onChange={(e) => setSearchFilters(prev => ({ ...prev, cefrLevel: e.target.value }))}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary focus:border-primary"
>
<option value=""></option>
<option value="A1">A1 - </option>
<option value="A2">A2 - </option>
<option value="B1">B1 - </option>
<option value="B2">B2 - </option>
<option value="C1">C1 - </option>
<option value="C2">C2 - </option>
</select>
</div>
{/* 詞性篩選 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<select
value={searchFilters.partOfSpeech}
onChange={(e) => setSearchFilters(prev => ({ ...prev, partOfSpeech: e.target.value }))}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary focus:border-primary"
>
<option value=""></option>
<option value="noun"> (noun)</option>
<option value="verb"> (verb)</option>
<option value="adjective"> (adjective)</option>
<option value="adverb"> (adverb)</option>
<option value="preposition"> (preposition)</option>
<option value="interjection"> (interjection)</option>
</select>
</div>
{/* 掌握度篩選 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<select
value={searchFilters.masteryLevel}
onChange={(e) => setSearchFilters(prev => ({ ...prev, masteryLevel: e.target.value }))}
className="w-full p-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-primary focus:border-primary"
>
<option value=""></option>
<option value="high"> (80%+)</option>
<option value="medium"> (60-79%)</option>
<option value="low"> (&lt;60%)</option>
</select>
</div>
{/* 收藏篩選 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<label className="flex items-center cursor-pointer">
<input
type="checkbox"
checked={searchFilters.onlyFavorites}
onChange={(e) => setSearchFilters(prev => ({ ...prev, onlyFavorites: e.target.checked }))}
className="w-4 h-4 text-yellow-600 bg-gray-100 border-gray-300 rounded focus:ring-yellow-500"
/>
<span className="ml-2 text-sm text-gray-700 flex items-center gap-1">
<span className="text-yellow-500"></span>
</span>
</label>
</div>
</div>
{/* 快速篩選按鈕 */}
<div className="flex items-center gap-2 pt-2 border-t border-gray-200">
<span className="text-sm text-gray-600">:</span>
<button
onClick={() => setSearchFilters(prev => ({ ...prev, masteryLevel: 'low' }))}
className="px-3 py-1 bg-red-100 text-red-700 rounded-full text-xs font-medium hover:bg-red-200 transition-colors"
>
</button>
<button
onClick={() => setSearchFilters(prev => ({ ...prev, onlyFavorites: true }))}
className="px-3 py-1 bg-yellow-100 text-yellow-700 rounded-full text-xs font-medium hover:bg-yellow-200 transition-colors"
>
</button>
<button
onClick={() => setSearchFilters(prev => ({ ...prev, cefrLevel: 'C1' }))}
className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-xs font-medium hover:bg-purple-200 transition-colors"
>
</button>
{hasActiveFilters && (
<button
onClick={clearAllFilters}
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-xs font-medium hover:bg-gray-200 transition-colors"
>
</button>
)}
</div>
</div>
)}
{/* 搜尋結果統計 */}
{(searchTerm || hasActiveFilters) && (
<div className="flex items-center justify-between text-sm text-gray-600 bg-blue-50 px-4 py-2 rounded-lg">
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<span>
<strong className="text-blue-700">{filteredCards.length}</strong>
{searchTerm && (
<span> "<strong className="text-blue-700">{searchTerm}</strong>"</span>
)}
</span>
</div>
{hasActiveFilters && (
<button
onClick={clearAllFilters}
className="text-blue-600 hover:text-blue-700 font-medium"
>
</button>
)}
</div>
)}
</div>
{/* Card Sets Tab */}
@ -376,18 +641,13 @@ function FlashcardsContent() {
) : (
<div className="space-y-2">
{allCards.filter(card => card.isFavorite).map(card => (
<div key={card.id} className="bg-white border border-yellow-200 rounded-lg hover:shadow-md transition-all duration-200 cursor-pointer relative ring-1 ring-yellow-100">
<div
className="p-4"
onClick={() => {
alert(`即將進入「${card.word}」的詳細頁面 (開發中)`)
}}
>
<div key={card.id} className="bg-white border border-yellow-200 rounded-lg hover:shadow-md transition-all duration-200 relative ring-1 ring-yellow-100">
<div className="p-4">
{/* 收藏詞卡內容 - 與普通詞卡相同的佈局 */}
<div className="flex items-center justify-between">
<div className="absolute top-3 right-3">
<span className={`text-xs px-2 py-1 rounded-full font-medium border ${getCEFRColor(card.difficultyLevel || 'A1')}`}>
{card.difficultyLevel || 'A1'}
<span className={`text-xs px-2 py-1 rounded-full font-medium border ${getCEFRColor((card as any).difficultyLevel || 'A1')}`}>
{(card as any).difficultyLevel || 'A1'}
</span>
</div>
@ -414,14 +674,18 @@ function FlashcardsContent() {
<div className="flex-1">
<div className="flex items-center gap-3">
<h3 className="text-xl font-bold text-gray-900">{card.word || '未設定'}</h3>
<h3 className="text-xl font-bold text-gray-900">
{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'}
</span>
</div>
<div className="flex items-center gap-4 mt-1">
<span className="text-lg text-gray-900 font-medium">{card.translation || '未設定'}</span>
<span className="text-lg text-gray-900 font-medium">
{searchTerm ? highlightSearchTerm(card.translation || '未設定', searchTerm) : (card.translation || '未設定')}
</span>
{card.pronunciation && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{card.pronunciation}</span>
@ -478,11 +742,19 @@ function FlashcardsContent() {
</button>
</div>
<div className="text-gray-400">
{/* 進入詳細頁面箭頭 - 僅此處可點擊導航 */}
<button
onClick={() => {
// TODO: 導航到詞卡詳細頁面
alert(`即將進入「${card.word}」的詳細頁面 (開發中)`)
}}
className="text-gray-400 hover:text-gray-600 p-2 rounded-lg hover:bg-gray-100 transition-colors"
title="查看詳細資訊"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
</div>
</div>
</div>
@ -541,19 +813,13 @@ function FlashcardsContent() {
) : (
<div className="space-y-2">
{filteredCards.map(card => (
<div key={card.id} className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-all duration-200 cursor-pointer relative">
<div
className="p-4"
onClick={() => {
// TODO: 導航到詞卡詳細頁面
alert(`即將進入「${card.word}」的詳細頁面 (開發中)`)
}}
>
<div key={card.id} className="bg-white border border-gray-200 rounded-lg hover:shadow-md transition-all duration-200 relative">
<div className="p-4">
<div className="flex items-center justify-between">
{/* 詞卡右上角CEFR標註 */}
<div className="absolute top-3 right-3">
<span className={`text-xs px-2 py-1 rounded-full font-medium border ${getCEFRColor(card.difficultyLevel || 'A1')}`}>
{card.difficultyLevel || 'A1'}
<span className={`text-xs px-2 py-1 rounded-full font-medium border ${getCEFRColor((card as any).difficultyLevel || 'A1')}`}>
{(card as any).difficultyLevel || 'A1'}
</span>
</div>
@ -583,14 +849,18 @@ function FlashcardsContent() {
<div className="flex-1">
<div className="flex items-center gap-3">
<h3 className="text-xl font-bold text-gray-900">{card.word || '未設定'}</h3>
<h3 className="text-xl font-bold text-gray-900">
{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'}
</span>
</div>
<div className="flex items-center gap-4 mt-1">
<span className="text-lg text-gray-900 font-medium">{card.translation || '未設定'}</span>
<span className="text-lg text-gray-900 font-medium">
{searchTerm ? highlightSearchTerm(card.translation || '未設定', searchTerm) : (card.translation || '未設定')}
</span>
{card.pronunciation && (
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{card.pronunciation}</span>
@ -621,46 +891,72 @@ function FlashcardsContent() {
{/* 右側:操作按鈕 */}
<div className="flex items-center gap-3">
{/* 快速操作按鈕 */}
<div className="flex gap-1" onClick={(e) => e.stopPropagation()}>
{/* 重新設計的操作按鈕區 */}
<div className="flex items-center gap-2">
{/* 收藏按鈕 */}
<button
onClick={() => handleToggleFavorite(card)}
className={`p-2 rounded-lg transition-colors ${
className={`px-3 py-2 rounded-lg font-medium transition-colors ${
card.isFavorite
? 'text-yellow-500 hover:text-yellow-600 hover:bg-yellow-50'
: 'text-gray-400 hover:text-yellow-500 hover:bg-yellow-50'
? 'bg-yellow-100 text-yellow-700 border border-yellow-300 hover:bg-yellow-200'
: 'bg-gray-100 text-gray-600 border border-gray-300 hover:bg-yellow-50 hover:text-yellow-600 hover:border-yellow-300'
}`}
title={card.isFavorite ? "取消收藏" : "加入收藏"}
>
<svg className="w-4 h-4" fill={card.isFavorite ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
<div className="flex items-center gap-1">
<svg className="w-4 h-4" fill={card.isFavorite ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
<span className="text-sm">
{card.isFavorite ? '已收藏' : '收藏'}
</span>
</div>
</button>
{/* 編輯按鈕 */}
<button
onClick={() => handleEdit(card)}
className="p-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors"
className="px-3 py-2 bg-blue-100 text-blue-700 border border-blue-300 rounded-lg font-medium hover:bg-blue-200 transition-colors"
title="編輯詞卡"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<div className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
<span className="text-sm"></span>
</div>
</button>
{/* 刪除按鈕 */}
<button
onClick={() => handleDelete(card)}
className="p-2 text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-colors"
className="px-3 py-2 bg-red-100 text-red-700 border border-red-300 rounded-lg font-medium hover:bg-red-200 transition-colors"
title="刪除詞卡"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<div className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span className="text-sm"></span>
</div>
</button>
</div>
{/* 進入詳細頁面箭頭 */}
<div className="text-gray-400">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
{/* 查看詳情按鈕 - 放大且更明顯 */}
<button
onClick={() => {
// TODO: 導航到詞卡詳細頁面
alert(`即將進入「${card.word}」的詳細頁面 (開發中)`)
}}
className="px-4 py-2 bg-gray-100 text-gray-700 border border-gray-300 rounded-lg font-medium hover:bg-gray-200 hover:text-gray-900 transition-colors"
title="查看詳細資訊"
>
<div className="flex items-center gap-1">
<span className="text-sm"></span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
</div>
</div>
</div>