852 lines
40 KiB
TypeScript
852 lines
40 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import Link from 'next/link'
|
||
|
||
export default function FlashcardsPage() {
|
||
const [activeTab, setActiveTab] = useState('my-cards')
|
||
const [selectedSet, setSelectedSet] = useState<number | null>(null)
|
||
const [searchTerm, setSearchTerm] = useState('')
|
||
const [filterTag, setFilterTag] = useState('all')
|
||
const [selectedCards, setSelectedCards] = useState<number[]>([])
|
||
const [showCheckModal, setShowCheckModal] = useState(false)
|
||
const [checkingCard, setCheckingCard] = useState<any>(null)
|
||
const [checkResults, setCheckResults] = useState<any>(null)
|
||
const [errorReports, setErrorReports] = useState<any[]>([
|
||
{
|
||
id: 1,
|
||
cardId: 1,
|
||
word: 'negotiate',
|
||
reportType: '發音錯誤',
|
||
description: '美式發音標記有誤',
|
||
reportedAt: '2小時前',
|
||
status: 'pending'
|
||
},
|
||
{
|
||
id: 2,
|
||
cardId: 3,
|
||
word: 'perspective',
|
||
reportType: '翻譯不準確',
|
||
description: '翻譯缺少其他常用含義',
|
||
reportedAt: '1天前',
|
||
status: 'pending'
|
||
},
|
||
{
|
||
id: 3,
|
||
cardId: 5,
|
||
word: 'implement',
|
||
reportType: '例句錯誤',
|
||
description: '例句時態使用不當',
|
||
reportedAt: '3天前',
|
||
status: 'pending'
|
||
}
|
||
])
|
||
|
||
// Mock data
|
||
const cardSets = [
|
||
{
|
||
id: 1,
|
||
name: '美劇經典台詞',
|
||
description: '從熱門美劇中精選的實用對話',
|
||
cardCount: 45,
|
||
progress: 60,
|
||
lastStudied: '2 小時前',
|
||
tags: ['影視', '口語'],
|
||
color: 'bg-blue-500'
|
||
},
|
||
{
|
||
id: 2,
|
||
name: '商務英文必備',
|
||
description: '職場溝通和商業會議常用詞彙',
|
||
cardCount: 30,
|
||
progress: 30,
|
||
lastStudied: '昨天',
|
||
tags: ['商務', '正式'],
|
||
color: 'bg-purple-500'
|
||
},
|
||
{
|
||
id: 3,
|
||
name: '日常對話',
|
||
description: '生活中最常用的英文表達',
|
||
cardCount: 25,
|
||
progress: 80,
|
||
lastStudied: '3 天前',
|
||
tags: ['日常', '基礎'],
|
||
color: 'bg-green-500'
|
||
},
|
||
{
|
||
id: 4,
|
||
name: 'TOEFL 核心詞彙',
|
||
description: '托福考試高頻詞彙整理',
|
||
cardCount: 100,
|
||
progress: 15,
|
||
lastStudied: '1 週前',
|
||
tags: ['考試', '學術'],
|
||
color: 'bg-orange-500'
|
||
},
|
||
{
|
||
id: 5,
|
||
name: '科技新聞詞彙',
|
||
description: '科技領域專業術語和流行用語',
|
||
cardCount: 35,
|
||
progress: 45,
|
||
lastStudied: '5 天前',
|
||
tags: ['科技', '專業'],
|
||
color: 'bg-indigo-500'
|
||
}
|
||
]
|
||
|
||
const flashcards = [
|
||
{
|
||
id: 1,
|
||
word: 'negotiate',
|
||
translation: '協商',
|
||
definition: 'To discuss something with someone in order to reach an agreement',
|
||
partOfSpeech: 'verb',
|
||
pronunciation: '/nɪˈɡoʊʃieɪt/',
|
||
example: 'We need to negotiate a better deal with our suppliers.',
|
||
setId: 1,
|
||
mastery: 80,
|
||
nextReview: '明天'
|
||
},
|
||
{
|
||
id: 2,
|
||
word: 'accomplish',
|
||
translation: '完成',
|
||
definition: 'To finish something successfully or to achieve something',
|
||
partOfSpeech: 'verb',
|
||
pronunciation: '/əˈkɒmplɪʃ/',
|
||
example: 'She accomplished her goal of running a marathon.',
|
||
setId: 1,
|
||
mastery: 60,
|
||
nextReview: '今天'
|
||
},
|
||
{
|
||
id: 3,
|
||
word: 'perspective',
|
||
translation: '觀點',
|
||
definition: 'A particular way of considering something',
|
||
partOfSpeech: 'noun',
|
||
pronunciation: '/pərˈspektɪv/',
|
||
example: 'From my perspective, this is the best solution.',
|
||
setId: 2,
|
||
mastery: 90,
|
||
nextReview: '3天後'
|
||
},
|
||
{
|
||
id: 4,
|
||
word: 'substantial',
|
||
translation: '大量的',
|
||
definition: 'Large in size, value, or importance',
|
||
partOfSpeech: 'adjective',
|
||
pronunciation: '/səbˈstænʃəl/',
|
||
example: 'The company made a substantial profit last year.',
|
||
setId: 2,
|
||
mastery: 40,
|
||
nextReview: '今天'
|
||
},
|
||
{
|
||
id: 5,
|
||
word: 'implement',
|
||
translation: '實施',
|
||
definition: 'To put a decision, plan, or agreement into effect',
|
||
partOfSpeech: 'verb',
|
||
pronunciation: '/ˈɪmplɪment/',
|
||
example: 'We need to implement new safety measures.',
|
||
setId: 3,
|
||
mastery: 70,
|
||
nextReview: '明天'
|
||
},
|
||
]
|
||
|
||
const tags = ['all', '影視', '商務', '日常', '考試', '科技', '口語', '正式', '基礎', '學術', '專業']
|
||
|
||
const filteredSets = cardSets.filter(set =>
|
||
set.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
set.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||
)
|
||
|
||
const filteredCards = flashcards.filter(card =>
|
||
card.word.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||
card.translation.includes(searchTerm)
|
||
)
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
{/* Navigation */}
|
||
<nav className="bg-white shadow-sm border-b">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div className="flex justify-between h-16">
|
||
<div className="flex items-center space-x-8">
|
||
<Link href="/dashboard" className="text-2xl font-bold text-primary">DramaLing</Link>
|
||
<div className="hidden md:flex space-x-6">
|
||
<Link href="/dashboard" className="text-gray-600 hover:text-gray-900">儀表板</Link>
|
||
<Link href="/flashcards" className="text-gray-900 font-medium">詞卡</Link>
|
||
<Link href="/learn" className="text-gray-600 hover:text-gray-900">學習</Link>
|
||
<Link href="/generate" className="text-gray-600 hover:text-gray-900">AI 生成</Link>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center space-x-4">
|
||
<Link
|
||
href="/generate"
|
||
className="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-hover transition-colors text-sm font-medium"
|
||
>
|
||
+ 新增詞卡
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
{/* Header */}
|
||
<div className="mb-8">
|
||
<h1 className="text-3xl font-bold text-gray-900 mb-2">我的詞卡庫</h1>
|
||
<p className="text-gray-600">管理和組織您的學習詞卡</p>
|
||
</div>
|
||
|
||
{/* Search and Filter */}
|
||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||
<div className="flex flex-col md:flex-row gap-4">
|
||
<div className="flex-1">
|
||
<input
|
||
type="text"
|
||
value={searchTerm}
|
||
onChange={(e) => setSearchTerm(e.target.value)}
|
||
placeholder="搜尋詞卡或卡組..."
|
||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent outline-none"
|
||
/>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<select
|
||
value={filterTag}
|
||
onChange={(e) => setFilterTag(e.target.value)}
|
||
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent outline-none"
|
||
>
|
||
{tags.map(tag => (
|
||
<option key={tag} value={tag}>
|
||
{tag === 'all' ? '所有標籤' : tag}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<button className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div className="bg-white rounded-lg shadow-sm mb-6">
|
||
<div className="border-b border-gray-200">
|
||
<nav className="-mb-px flex space-x-8 px-6" aria-label="Tabs">
|
||
<button
|
||
onClick={() => setActiveTab('my-cards')}
|
||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||
activeTab === 'my-cards'
|
||
? 'border-primary text-primary'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
我的卡組
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('all-cards')}
|
||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||
activeTab === 'all-cards'
|
||
? 'border-primary text-primary'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
所有詞卡
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('favorites')}
|
||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||
activeTab === 'favorites'
|
||
? 'border-primary text-primary'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
收藏詞卡
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('error-reports')}
|
||
className={`py-4 px-1 border-b-2 font-medium text-sm relative ${
|
||
activeTab === 'error-reports'
|
||
? 'border-primary text-primary'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
錯誤回報
|
||
{errorReports.filter(r => r.status === 'pending').length > 0 && (
|
||
<span className="absolute -top-1 -right-2 bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">
|
||
{errorReports.filter(r => r.status === 'pending').length}
|
||
</span>
|
||
)}
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
|
||
<div className="p-6">
|
||
{activeTab === 'my-cards' && (
|
||
<div>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-semibold">共 {filteredSets.length} 個卡組</h3>
|
||
<div className="flex gap-2">
|
||
<button className="p-2 text-gray-600 hover:text-gray-900">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||
</svg>
|
||
</button>
|
||
<button className="p-2 text-gray-600 hover:text-gray-900">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{filteredSets.map(set => (
|
||
<div
|
||
key={set.id}
|
||
className="border rounded-lg hover:shadow-lg transition-shadow cursor-pointer"
|
||
onClick={() => setSelectedSet(set.id)}
|
||
>
|
||
<div className={`h-2 ${set.color} rounded-t-lg`}></div>
|
||
<div className="p-4">
|
||
<div className="flex justify-between items-start mb-2">
|
||
<h4 className="font-semibold text-lg">{set.name}</h4>
|
||
<button className="text-gray-400 hover:text-gray-600">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<p className="text-sm text-gray-600 mb-3">{set.description}</p>
|
||
<div className="flex flex-wrap gap-1 mb-3">
|
||
{set.tags.map(tag => (
|
||
<span key={tag} className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full">
|
||
{tag}
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-600">進度</span>
|
||
<span className="font-medium">{set.progress}%</span>
|
||
</div>
|
||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||
<div
|
||
className="bg-primary h-2 rounded-full transition-all"
|
||
style={{ width: `${set.progress}%` }}
|
||
></div>
|
||
</div>
|
||
<div className="flex justify-between text-sm text-gray-600">
|
||
<span>{set.cardCount} 個詞卡</span>
|
||
<span>{set.lastStudied}</span>
|
||
</div>
|
||
</div>
|
||
<div className="mt-4 flex gap-2">
|
||
<Link
|
||
href={`/learn?set=${set.id}`}
|
||
className="flex-1 bg-primary text-white text-center py-2 rounded-lg hover:bg-primary-hover transition-colors text-sm font-medium"
|
||
>
|
||
開始學習
|
||
</Link>
|
||
<button className="flex-1 border border-gray-300 py-2 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
|
||
管理詞卡
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{/* Add New Set Card */}
|
||
<div className="border-2 border-dashed border-gray-300 rounded-lg hover:border-gray-400 transition-colors cursor-pointer flex items-center justify-center min-h-[280px]">
|
||
<div className="text-center">
|
||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</div>
|
||
<p className="text-gray-600 font-medium">創建新卡組</p>
|
||
<p className="text-sm text-gray-500 mt-1">組織您的學習內容</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'all-cards' && (
|
||
<div>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-semibold">共 {filteredCards.length} 個詞卡</h3>
|
||
<div className="flex gap-2">
|
||
{selectedCards.length > 0 && (
|
||
<button
|
||
onClick={() => {
|
||
setCheckResults(null)
|
||
setShowCheckModal(true)
|
||
}}
|
||
className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 transition-colors text-sm font-medium flex items-center gap-2"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
智能檢測 ({selectedCards.length})
|
||
</button>
|
||
)}
|
||
<button className="text-primary hover:text-primary-hover font-medium text-sm">
|
||
批量操作
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-3">
|
||
{filteredCards.map(card => (
|
||
<div key={card.id} className="bg-white border rounded-lg p-4 hover:shadow-md transition-shadow">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center space-x-4">
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedCards.includes(card.id)}
|
||
onChange={(e) => {
|
||
if (e.target.checked) {
|
||
setSelectedCards([...selectedCards, card.id])
|
||
} else {
|
||
setSelectedCards(selectedCards.filter(id => id !== card.id))
|
||
}
|
||
}}
|
||
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
|
||
/>
|
||
<div
|
||
onClick={() => {
|
||
setCheckingCard(card)
|
||
setCheckResults(null)
|
||
setShowCheckModal(true)
|
||
}}
|
||
className="cursor-pointer hover:text-primary transition-colors"
|
||
>
|
||
<div className="font-semibold">{card.word}</div>
|
||
<div className="text-sm text-gray-600">{card.translation}</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center space-x-4">
|
||
<div className="text-right">
|
||
<div className="text-sm text-gray-600">掌握度</div>
|
||
<div className="font-medium">{card.mastery}%</div>
|
||
</div>
|
||
<div className="text-right">
|
||
<div className="text-sm text-gray-600">下次複習</div>
|
||
<div className="text-sm font-medium text-primary">{card.nextReview}</div>
|
||
</div>
|
||
<button className="text-gray-400 hover:text-gray-600">
|
||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'favorites' && (
|
||
<div className="text-center py-12">
|
||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||
</svg>
|
||
</div>
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-2">還沒有收藏的詞卡</h3>
|
||
<p className="text-gray-600 mb-4">在學習時點擊愛心圖標來收藏詞卡</p>
|
||
<Link
|
||
href="/learn"
|
||
className="inline-block bg-primary text-white px-6 py-2 rounded-lg hover:bg-primary-hover transition-colors"
|
||
>
|
||
開始學習
|
||
</Link>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'error-reports' && (
|
||
<div>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-semibold">
|
||
錯誤回報清單 ({errorReports.filter(r => r.status === 'pending').length} 個待處理)
|
||
</h3>
|
||
{errorReports.filter(r => r.status === 'pending').length > 0 && (
|
||
<button
|
||
onClick={() => {
|
||
// 一鍵檢測所有錯誤回報
|
||
const pendingReports = errorReports.filter(r => r.status === 'pending')
|
||
setCheckingCard({
|
||
isErrorList: true,
|
||
reports: pendingReports
|
||
})
|
||
setCheckResults(null)
|
||
setShowCheckModal(true)
|
||
}}
|
||
className="bg-green-500 text-white px-4 py-2 rounded-lg hover:bg-green-600 transition-colors text-sm font-medium flex items-center gap-2"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
一鍵檢測所有錯誤
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{errorReports.length === 0 ? (
|
||
<div className="text-center py-12">
|
||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
</div>
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-2">沒有錯誤回報</h3>
|
||
<p className="text-gray-600">所有詞卡內容都正確無誤!</p>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{errorReports.map(report => (
|
||
<div
|
||
key={report.id}
|
||
className={`bg-white border rounded-lg p-4 hover:shadow-md transition-shadow ${
|
||
report.status === 'resolved' ? 'opacity-60' : ''
|
||
}`}
|
||
>
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-3 mb-2">
|
||
<h4 className="font-semibold text-lg">{report.word}</h4>
|
||
<span className={`px-2 py-1 rounded-full text-xs ${
|
||
report.status === 'pending'
|
||
? 'bg-yellow-100 text-yellow-700'
|
||
: 'bg-green-100 text-green-700'
|
||
}`}>
|
||
{report.status === 'pending' ? '待處理' : '已解決'}
|
||
</span>
|
||
<span className="px-2 py-1 bg-red-100 text-red-700 rounded-full text-xs">
|
||
{report.reportType}
|
||
</span>
|
||
</div>
|
||
<p className="text-gray-600 mb-2">{report.description}</p>
|
||
<div className="text-sm text-gray-500">
|
||
回報時間:{report.reportedAt}
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
{report.status === 'pending' && (
|
||
<>
|
||
<button
|
||
onClick={() => {
|
||
// 檢測單個錯誤
|
||
const card = flashcards.find(c => c.id === report.cardId)
|
||
if (card) {
|
||
setCheckingCard({
|
||
...card,
|
||
errorReport: report
|
||
})
|
||
setCheckResults(null)
|
||
setShowCheckModal(true)
|
||
}
|
||
}}
|
||
className="text-green-600 hover:text-green-700 p-2"
|
||
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 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
// 標記為已解決
|
||
setErrorReports(errorReports.map(r =>
|
||
r.id === report.id ? { ...r, status: 'resolved' } : r
|
||
))
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600 p-2"
|
||
title="忽略此錯誤"
|
||
>
|
||
<svg className="w-5 h-5" 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>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Stats Summary */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||
<div className="bg-white rounded-lg shadow-sm p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="text-2xl font-bold">234</div>
|
||
<div className="text-sm text-gray-600">總詞卡數</div>
|
||
</div>
|
||
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
|
||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-lg shadow-sm p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="text-2xl font-bold">156</div>
|
||
<div className="text-sm text-gray-600">已掌握</div>
|
||
</div>
|
||
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
|
||
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-lg shadow-sm p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="text-2xl font-bold">23</div>
|
||
<div className="text-sm text-gray-600">待複習</div>
|
||
</div>
|
||
<div className="w-12 h-12 bg-yellow-100 rounded-full flex items-center justify-center">
|
||
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-lg shadow-sm p-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="text-2xl font-bold">67%</div>
|
||
<div className="text-sm text-gray-600">總體掌握</div>
|
||
</div>
|
||
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
|
||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Smart Check Modal */}
|
||
{showCheckModal && (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4">
|
||
<div className="bg-white rounded-xl max-w-2xl w-full max-h-[80vh] overflow-y-auto">
|
||
<div className="p-6 border-b">
|
||
<div className="flex items-center justify-between">
|
||
<h2 className="text-xl font-bold">🤖 智能檢測</h2>
|
||
<button
|
||
onClick={() => {
|
||
setShowCheckModal(false)
|
||
setCheckingCard(null)
|
||
setCheckResults(null)
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
<svg className="w-6 h-6" 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>
|
||
|
||
<div className="p-6">
|
||
{!checkResults ? (
|
||
// 檢測前
|
||
<div>
|
||
<div className="mb-4">
|
||
<p className="text-gray-600 mb-4">
|
||
{checkingCard?.isErrorList
|
||
? `即將檢測 ${checkingCard.reports.length} 個錯誤回報`
|
||
: checkingCard?.errorReport
|
||
? `正在檢測錯誤回報:${checkingCard.errorReport.reportType}`
|
||
: checkingCard
|
||
? `正在檢測詞卡「${checkingCard.word}」的內容...`
|
||
: `即將檢測 ${selectedCards.length} 個詞卡的內容正確性`}
|
||
</p>
|
||
</div>
|
||
|
||
{checkingCard && !checkingCard.isErrorList && (
|
||
<div className="bg-gray-50 rounded-lg p-4 mb-4">
|
||
{checkingCard.errorReport && (
|
||
<div className="mb-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
|
||
<div className="text-sm font-medium text-yellow-800 mb-1">
|
||
錯誤回報:{checkingCard.errorReport.reportType}
|
||
</div>
|
||
<div className="text-sm text-yellow-700">
|
||
{checkingCard.errorReport.description}
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className="space-y-2">
|
||
<div><strong>單字:</strong>{checkingCard.word}</div>
|
||
<div><strong>翻譯:</strong>{checkingCard.translation}</div>
|
||
<div><strong>定義:</strong>{checkingCard.definition}</div>
|
||
<div><strong>詞性:</strong>{checkingCard.partOfSpeech}</div>
|
||
<div><strong>發音:</strong>{checkingCard.pronunciation}</div>
|
||
<div><strong>例句:</strong>{checkingCard.example}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{checkingCard?.isErrorList && (
|
||
<div className="bg-gray-50 rounded-lg p-4 mb-4 max-h-60 overflow-y-auto">
|
||
<div className="text-sm font-medium text-gray-700 mb-2">錯誤清單:</div>
|
||
<div className="space-y-2">
|
||
{checkingCard.reports.map((report, idx) => (
|
||
<div key={idx} className="bg-white p-2 rounded border border-gray-200">
|
||
<div className="font-medium">{report.word}</div>
|
||
<div className="text-xs text-gray-600">
|
||
{report.reportType}: {report.description}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
onClick={() => {
|
||
// 模擬檢測過程
|
||
setTimeout(() => {
|
||
const isErrorList = checkingCard?.isErrorList
|
||
const totalCount = isErrorList
|
||
? checkingCard.reports.length
|
||
: checkingCard
|
||
? 1
|
||
: selectedCards.length
|
||
|
||
setCheckResults({
|
||
totalChecked: totalCount,
|
||
isErrorList: isErrorList,
|
||
errors: [
|
||
{
|
||
field: '發音',
|
||
original: '/nɪˈɡoʊʃieɪt/',
|
||
corrected: '/nɪˈɡəʊʃieɪt/',
|
||
reason: '英式發音標記錯誤'
|
||
},
|
||
{
|
||
field: '例句',
|
||
original: 'We need to negotiate a better deal with our suppliers.',
|
||
corrected: 'We need to negotiate a better deal with our suppliers.',
|
||
reason: '例句文法正確,但可加入更多上下文'
|
||
}
|
||
],
|
||
suggestions: [
|
||
'建議添加同義詞:bargain, discuss',
|
||
'建議添加反義詞:refuse, reject',
|
||
'可補充詞根詞綴說明'
|
||
]
|
||
})
|
||
}, 2000)
|
||
}}
|
||
className="w-full bg-green-500 text-white py-3 rounded-lg hover:bg-green-600 transition-colors font-medium"
|
||
>
|
||
開始智能檢測
|
||
</button>
|
||
</div>
|
||
) : (
|
||
// 檢測結果
|
||
<div>
|
||
<div className="mb-4">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<svg className="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
<h3 className="text-lg font-semibold">檢測完成</h3>
|
||
</div>
|
||
<p className="text-gray-600">
|
||
{checkResults.isErrorList
|
||
? `已檢測 ${checkResults.totalChecked} 個錯誤回報,發現 ${checkResults.errors.length} 個問題`
|
||
: `已檢測 ${checkResults.totalChecked} 個詞卡,發現 ${checkResults.errors.length} 個問題`}
|
||
</p>
|
||
</div>
|
||
|
||
{checkResults.errors.length > 0 && (
|
||
<div className="mb-4">
|
||
<h4 className="font-semibold mb-2">📝 發現的問題:</h4>
|
||
<div className="space-y-3">
|
||
{checkResults.errors.map((error, idx) => (
|
||
<div key={idx} className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||
<div className="font-medium text-yellow-800 mb-1">{error.field}</div>
|
||
<div className="text-sm space-y-1">
|
||
<div><span className="text-gray-600">原始:</span> {error.original}</div>
|
||
<div><span className="text-gray-600">建議:</span> <span className="text-green-600">{error.corrected}</span></div>
|
||
<div className="text-xs text-gray-500">{error.reason}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{checkResults.suggestions.length > 0 && (
|
||
<div className="mb-4">
|
||
<h4 className="font-semibold mb-2">💡 改進建議:</h4>
|
||
<ul className="space-y-1">
|
||
{checkResults.suggestions.map((suggestion, idx) => (
|
||
<li key={idx} className="flex items-start gap-2 text-sm text-gray-600">
|
||
<span className="text-primary">•</span>
|
||
{suggestion}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => {
|
||
alert('已自動修正所有問題!')
|
||
setShowCheckModal(false)
|
||
setCheckResults(null)
|
||
setCheckingCard(null)
|
||
}}
|
||
className="flex-1 bg-primary text-white py-2 rounded-lg hover:bg-primary-hover transition-colors font-medium"
|
||
>
|
||
接受修正
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
setShowCheckModal(false)
|
||
setCheckResults(null)
|
||
setCheckingCard(null)
|
||
}}
|
||
className="flex-1 border border-gray-300 py-2 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
||
>
|
||
稍後處理
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
} |