feat: Add error reporting to all test modes and unify button position
- Added error report button to flip card mode - Unified error report button position across all modes (top-right) - Implemented error report modal with optional reason input - Removed example sentences from quiz mode results - Updated modal to display test mode and card information 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
96e205fd7f
commit
102643d96c
|
|
@ -8,6 +8,39 @@ export default function FlashcardsPage() {
|
|||
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 = [
|
||||
|
|
@ -64,11 +97,66 @@ export default function FlashcardsPage() {
|
|||
]
|
||||
|
||||
const flashcards = [
|
||||
{ id: 1, word: 'negotiate', translation: '協商', setId: 1, mastery: 80, nextReview: '明天' },
|
||||
{ id: 2, word: 'accomplish', translation: '完成', setId: 1, mastery: 60, nextReview: '今天' },
|
||||
{ id: 3, word: 'perspective', translation: '觀點', setId: 2, mastery: 90, nextReview: '3天後' },
|
||||
{ id: 4, word: 'substantial', translation: '大量的', setId: 2, mastery: 40, nextReview: '今天' },
|
||||
{ id: 5, word: 'implement', translation: '實施', setId: 3, mastery: 70, nextReview: '明天' },
|
||||
{
|
||||
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', '影視', '商務', '日常', '考試', '科技', '口語', '正式', '基礎', '學術', '專業']
|
||||
|
|
@ -184,6 +272,21 @@ export default function FlashcardsPage() {
|
|||
>
|
||||
收藏詞卡
|
||||
</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>
|
||||
|
||||
|
|
@ -282,17 +385,51 @@ export default function FlashcardsPage() {
|
|||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">共 {filteredCards.length} 個詞卡</h3>
|
||||
<button className="text-primary hover:text-primary-hover font-medium text-sm">
|
||||
批量操作
|
||||
</button>
|
||||
<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" className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded" />
|
||||
<div>
|
||||
<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>
|
||||
|
|
@ -336,6 +473,121 @@ export default function FlashcardsPage() {
|
|||
</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>
|
||||
|
||||
|
|
@ -398,6 +650,203 @@ export default function FlashcardsPage() {
|
|||
</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>
|
||||
)
|
||||
}
|
||||
|
|
@ -15,6 +15,9 @@ export default function LearnPage() {
|
|||
const [isRecording, setIsRecording] = useState(false)
|
||||
const [audioPlaying, setAudioPlaying] = useState(false)
|
||||
const [modalImage, setModalImage] = useState<string | null>(null)
|
||||
const [showReportModal, setShowReportModal] = useState(false)
|
||||
const [reportReason, setReportReason] = useState('')
|
||||
const [reportingCard, setReportingCard] = useState<any>(null)
|
||||
|
||||
// Mock data with real example images
|
||||
const cards = [
|
||||
|
|
@ -214,6 +217,23 @@ export default function LearnPage() {
|
|||
{mode === 'flip' ? (
|
||||
/* Flip Card Mode */
|
||||
<div className="relative">
|
||||
{/* Error Report Button for Flip Mode */}
|
||||
<div className="flex justify-end mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setReportingCard(currentCard)
|
||||
setShowReportModal(true)
|
||||
}}
|
||||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||||
title="回報錯誤"
|
||||
>
|
||||
<svg className="w-4 h-4" 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>
|
||||
回報錯誤
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="relative w-full h-96 cursor-pointer"
|
||||
onClick={handleFlip}
|
||||
|
|
@ -223,7 +243,7 @@ export default function LearnPage() {
|
|||
className={`absolute w-full h-full transition-transform duration-600 ${
|
||||
isFlipped ? 'rotate-y-180' : ''
|
||||
}`}
|
||||
style={{
|
||||
style={{
|
||||
transformStyle: 'preserve-3d',
|
||||
transform: isFlipped ? 'rotateY(180deg)' : 'rotateY(0deg)'
|
||||
}}
|
||||
|
|
@ -315,17 +335,35 @@ export default function LearnPage() {
|
|||
</div>
|
||||
) : mode === 'quiz' ? (
|
||||
/* Quiz Mode - 選擇題:英文定義選中文翻譯 */
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-2">根據定義選擇正確的中文翻譯</div>
|
||||
<div className="text-xl text-gray-800 leading-relaxed">
|
||||
{currentCard.definition}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
({currentCard.partOfSpeech})
|
||||
</div>
|
||||
<div className="relative">
|
||||
{/* Error Report Button for Quiz Mode */}
|
||||
<div className="flex justify-end mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setReportingCard(currentCard)
|
||||
setShowReportModal(true)
|
||||
}}
|
||||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||||
title="回報錯誤"
|
||||
>
|
||||
<svg className="w-4 h-4" 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>
|
||||
回報錯誤
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-2">根據定義選擇正確的中文翻譯</div>
|
||||
<div className="text-xl text-gray-800 leading-relaxed">
|
||||
{currentCard.definition}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-2">
|
||||
({currentCard.partOfSpeech})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{quizOptions.map((option, idx) => (
|
||||
<button
|
||||
|
|
@ -359,19 +397,31 @@ export default function LearnPage() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{showResult && (
|
||||
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-semibold text-gray-700 mb-2">例句</div>
|
||||
<div className="text-gray-600">{currentCard.example}</div>
|
||||
<div className="text-gray-500 text-sm mt-1">{currentCard.exampleTranslation}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : mode === 'fill' ? (
|
||||
/* Fill in the Blank Mode - 填空題 */
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-4">根據例句圖片和句子填入正確的詞彙</div>
|
||||
<div className="relative">
|
||||
{/* Error Report Button for Fill Mode */}
|
||||
<div className="flex justify-end mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setReportingCard(currentCard)
|
||||
setShowReportModal(true)
|
||||
}}
|
||||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||||
title="回報錯誤"
|
||||
>
|
||||
<svg className="w-4 h-4" 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>
|
||||
回報錯誤
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-4">根據例句圖片和句子填入正確的詞彙</div>
|
||||
|
||||
{/* Example Image */}
|
||||
{currentCard.exampleImage && (
|
||||
|
|
@ -477,12 +527,31 @@ export default function LearnPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : mode === 'listening' ? (
|
||||
/* Listening Test Mode - 聽力測試 */
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6 text-center">
|
||||
<div className="text-sm text-gray-600 mb-4">聽音頻,選擇正確的單字</div>
|
||||
<div className="relative">
|
||||
{/* Error Report Button for Listening Mode */}
|
||||
<div className="flex justify-end mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setReportingCard(currentCard)
|
||||
setShowReportModal(true)
|
||||
}}
|
||||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||||
title="回報錯誤"
|
||||
>
|
||||
<svg className="w-4 h-4" 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>
|
||||
回報錯誤
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6 text-center">
|
||||
<div className="text-sm text-gray-600 mb-4">聽音頻,選擇正確的單字</div>
|
||||
|
||||
{/* Audio Play Button */}
|
||||
<button
|
||||
|
|
@ -543,12 +612,31 @@ export default function LearnPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : mode === 'speaking' ? (
|
||||
/* Speaking Test Mode - 口說測試 */
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-4">念出以下例句</div>
|
||||
<div className="relative">
|
||||
{/* Error Report Button for Speaking Mode */}
|
||||
<div className="flex justify-end mb-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setReportingCard(currentCard)
|
||||
setShowReportModal(true)
|
||||
}}
|
||||
className="text-red-500 hover:text-red-600 text-sm flex items-center gap-1"
|
||||
title="回報錯誤"
|
||||
>
|
||||
<svg className="w-4 h-4" 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>
|
||||
回報錯誤
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-gray-600 mb-4">念出以下例句</div>
|
||||
|
||||
{/* Target Sentence */}
|
||||
<div className="p-6 bg-gray-50 rounded-lg mb-6">
|
||||
|
|
@ -622,6 +710,7 @@ export default function LearnPage() {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
|
|
@ -682,6 +771,84 @@ export default function LearnPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Report Modal */}
|
||||
{showReportModal && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 p-4"
|
||||
onClick={() => setShowReportModal(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-lg shadow-xl max-w-md w-full p-6"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">回報錯誤</h3>
|
||||
<button
|
||||
onClick={() => setShowReportModal(false)}
|
||||
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 className="mb-4">
|
||||
<div className="text-sm text-gray-600 mb-2">
|
||||
詞卡:<span className="font-medium">{reportingCard?.word}</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 mb-2">
|
||||
測驗模式:{mode === 'flip' ? '翻卡模式' : mode === 'quiz' ? '選擇題' : mode === 'fill' ? '填空題' : mode === 'listening' ? '聽力測試' : '口說測試'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
錯誤原因(選填)
|
||||
</label>
|
||||
<textarea
|
||||
value={reportReason}
|
||||
onChange={(e) => setReportReason(e.target.value)}
|
||||
placeholder="請描述錯誤內容..."
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-primary focus:border-primary"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
// Submit error report
|
||||
console.log('Error reported:', {
|
||||
card: reportingCard,
|
||||
mode,
|
||||
reason: reportReason
|
||||
})
|
||||
setShowReportModal(false)
|
||||
setReportReason('')
|
||||
setReportingCard(null)
|
||||
// Show success message (could add a toast notification here)
|
||||
alert('感謝您的回報,我們會盡快處理!')
|
||||
}}
|
||||
className="flex-1 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors"
|
||||
>
|
||||
送出回報
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowReportModal(false)
|
||||
setReportReason('')
|
||||
setReportingCard(null)
|
||||
}}
|
||||
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -147,6 +147,13 @@
|
|||
- 批量重設學習進度
|
||||
- 批量導出
|
||||
|
||||
- **智能檢測**
|
||||
- 單一詞彙檢測
|
||||
- 全面檢查指定詞彙所有內容是否有問題,並修正錯誤內容
|
||||
- 點擊指定內容進行檢查,並修正錯誤內容
|
||||
- 錯誤清單一鍵檢測
|
||||
- 點擊後及針對當前錯誤回報系統中清單進行檢測,一一修正錯誤內容
|
||||
|
||||
#### 1.3.3 組織功能
|
||||
- **標籤系統**
|
||||
- 預設標籤(動詞、名詞、片語、俚語等)
|
||||
|
|
@ -204,6 +211,10 @@
|
|||
- 聽力測試(聽音選詞)
|
||||
- 口說測試 (念例句)
|
||||
|
||||
- **錯誤回報**
|
||||
- 翻卡模式及所有測驗模式,都要設定錯誤回報
|
||||
- 點擊錯誤回報後,可以輸入錯誤原因,可以不填寫直接送出
|
||||
|
||||
- **沉浸模式**
|
||||
- 全螢幕學習
|
||||
- 自動播放(可調速度)
|
||||
|
|
|
|||
Loading…
Reference in New Issue