35 KiB
35 KiB
詞彙學習 - UI設計規範
文件版本: 1.0
建立日期: 2025-09-27
目標讀者: 前端實作工程師、UI設計師
用途: HTML/CSS實作、視覺設計、組件規範
配合文檔: 智能複習系統-開發指南.md - 系統架構和業務邏輯
來源: 從備份檔案 page-v1-original.tsx 提取的完整UI設計規格
📋 總體設計架構
核心測驗類型
該系統包含6種主要測驗類型:
- 翻卡記憶 (flip-memory)
- 詞彙選擇 (vocab-choice)
- 例句填空 (sentence-fill)
- 例句重組 (sentence-reorder)
- 聽力測驗 (listening - vocab-listening, sentence-listening)
- 口說測驗 (speaking - sentence-speaking)
共同設計元素
通用容器結構
<div className="relative">
{/* 錯誤回報按鈕 */}
<div className="flex justify-end mb-2">
<button className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900">
🚩 回報錯誤
</button>
</div>
{/* 主要內容區 */}
<div className="bg-white rounded-xl shadow-lg p-8">
{/* 測驗內容 */}
</div>
{/* 導航按鈕 */}
<div className="flex gap-4 mt-6">
<button className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium">
上一張
</button>
<button className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium">
下一張/完成
</button>
</div>
</div>
通用標題區
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">
{測驗名稱}
</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{currentCard.difficultyLevel}
</span>
</div>
配色方案
- 主色調:
bg-primary,text-white,hover:bg-primary-dark - 次要色調:
bg-gray-500,hover:bg-gray-600 - 成功狀態:
bg-green-50,border-green-200,text-green-700 - 錯誤狀態:
bg-red-50,border-red-200,text-red-700 - 提示狀態:
bg-yellow-50,border-yellow-200,text-yellow-800 - 信息狀態:
bg-blue-50,border-blue-200,text-blue-700
🔄 1. 翻卡記憶 (flip-memory)
核心特色
- 3D翻卡動畫效果
- 正面顯示單字,背面顯示詳細資訊
- 信心等級評估系統
HTML結構
<div
className="card-container"
onClick={handleFlip}
style={{ height: `${cardHeight}px` }}
>
<div className={`card ${isFlipped ? 'flipped' : ''}`}>
{/* 正面 */}
<div className="card-front">
<div className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow p-8">
{/* 標題區 */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">翻卡記憶</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{difficultyLevel}
</span>
</div>
{/* 指示文字 */}
<p className="text-lg text-gray-700 mb-6 text-left">
點擊卡片翻面,根據你對單字的熟悉程度進行自我評估:
</p>
{/* 單字顯示區 */}
<div className="flex-1 flex items-center justify-center mt-6">
<div className="bg-gray-50 rounded-lg p-8 w-full text-center">
<h3 className="text-4xl font-bold text-gray-900 mb-6">{word}</h3>
<div className="flex items-center justify-center gap-3">
<span className="text-lg text-gray-500">{pronunciation}</span>
<AudioPlayer text={word} />
</div>
</div>
</div>
</div>
</div>
{/* 背面 */}
<div className="card-back">
<div className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow">
<div className="space-y-4">
{/* 定義區塊 */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left">定義</h3>
<p className="text-gray-700 text-left">{definition}</p>
</div>
{/* 例句區塊 */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left">例句</h3>
<div className="relative">
<p className="text-gray-700 italic mb-2 text-left pr-12">{example}</p>
<div className="absolute bottom-0 right-0">
<AudioPlayer text={example} />
</div>
</div>
<p className="text-gray-600 text-sm text-left">{exampleTranslation}</p>
</div>
{/* 同義詞區塊 */}
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left">同義詞</h3>
<div className="flex flex-wrap gap-2">
{synonyms.map(synonym => (
<span className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200">
{synonym}
</span>
))}
</div>
</div>
{/* 信心等級評估區 */}
<div className="bg-blue-50 rounded-lg p-4">
<h3 className="font-semibold text-blue-900 mb-3 text-left">你對這個單字的熟悉程度:</h3>
<div className="grid grid-cols-5 gap-2">
{[1, 2, 3, 4, 5].map(level => (
<button
key={level}
onClick={() => handleConfidenceLevel(level)}
className="py-3 px-4 bg-white border border-blue-200 text-blue-700 rounded-lg hover:bg-blue-100 hover:border-blue-300 transition-all text-center"
>
<div className="font-bold text-lg">{level}</div>
<div className="text-xs">
{level === 1 ? '完全不懂' :
level === 2 ? '模糊' :
level === 3 ? '一般' :
level === 4 ? '熟悉' : '非常熟悉'}
</div>
</button>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
CSS動畫樣式
.card-container {
perspective: 1000px;
transition: height 0.3s ease;
overflow: visible;
position: relative;
}
.card {
position: relative;
width: 100%;
height: 100%;
text-align: center;
transition: transform 0.6s ease;
transform-style: preserve-3d;
}
.card.flipped {
transform: rotateY(180deg);
}
.card-front, .card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: stretch;
justify-content: center;
top: 0;
left: 0;
}
.card-back {
transform: rotateY(180deg);
}
🎯 2. 詞彙選擇 (vocab-choice)
核心特色
- 四選一選擇題
- 顯示詞彙定義,選擇對應單字
- 即時反饋系統
HTML結構
<div className="bg-white rounded-xl shadow-lg p-8">
{/* 標題區 */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">詞彙選擇</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{difficultyLevel}
</span>
</div>
{/* 指示文字 */}
<p className="text-lg text-gray-700 mb-6 text-left">
請選擇符合上述定義的英文詞彙:
</p>
{/* 定義顯示區 */}
<div className="text-center mb-8">
<div className="bg-gray-50 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-gray-900 mb-2 text-left">定義</h3>
<p className="text-gray-700 text-left">{definition}</p>
</div>
</div>
{/* 選項區域 */}
<div className="space-y-3 mb-6">
{quizOptions.map((option, idx) => (
<button
key={idx}
onClick={() => !showResult && handleQuizAnswer(option)}
disabled={showResult}
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
showResult
? option === correctAnswer
? 'border-green-500 bg-green-50 text-green-700'
: option === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
{option}
</button>
))}
</div>
{/* 結果反饋區 */}
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === correctAnswer
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === correctAnswer ? 'text-green-700' : 'text-red-700'
}`}>
{selectedAnswer === correctAnswer ? '正確!' : '錯誤!'}
</p>
{selectedAnswer !== correctAnswer && (
<div className="mb-4">
<p className="text-gray-700 text-left">
正確答案是:<strong className="text-lg">{correctAnswer}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<div className="flex items-center text-gray-600">
<span className="mx-2">{pronunciation}</span>
<AudioPlayer text={correctAnswer} />
</div>
</div>
<div className="text-left">
<p className="text-gray-600">
<strong>例句:</strong>{example}
</p>
<p className="text-gray-500 text-sm">
<strong>翻譯:</strong>{exampleTranslation}
</p>
</div>
</div>
</div>
)}
</div>
✏️ 3. 例句填空 (sentence-fill)
核心特色
- 內嵌式輸入框設計
- 動態寬度調整
- 提示系統
- 支援圖片顯示
HTML結構
<div className="bg-white rounded-xl shadow-lg p-8">
{/* 標題區 */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">例句填空</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{difficultyLevel}
</span>
</div>
{/* 圖片區(如果有) */}
{exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left">圖片提示</h3>
<img
src={exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => setModalImage(exampleImage)}
/>
</div>
</div>
)}
{/* 指示文字 */}
<p className="text-lg text-gray-700 mb-6 text-left">
請點擊例句中的空白處輸入正確的單字:
</p>
{/* 填空句子區域 */}
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-6">
<div className="text-lg text-gray-700 leading-relaxed">
{/* 動態生成填空句子 */}
{sentence.split(targetWord).map((part, index) => {
const isTargetWord = index > 0;
return (
<React.Fragment key={index}>
<span>{part}</span>
{isTargetWord && (
<span className="relative inline-block mx-1">
<input
type="text"
value={fillAnswer}
onChange={(e) => setFillAnswer(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !showResult && fillAnswer.trim()) {
handleFillAnswer()
}
}}
placeholder=""
disabled={showResult}
className={`inline-block px-2 py-1 text-center bg-transparent focus:outline-none disabled:bg-gray-100 ${
fillAnswer
? 'border-b-2 border-blue-500'
: 'border-b-2 border-dashed border-gray-400 focus:border-blue-400 focus:border-solid'
}`}
style={{
width: `${Math.max(100, Math.max(targetWord.length * 12, fillAnswer.length * 12 + 20))}px`
}}
/>
{!fillAnswer && (
<span
className="absolute inset-0 flex items-center justify-center text-gray-400 pointer-events-none"
style={{ paddingBottom: '8px' }}
>
____
</span>
)}
</span>
)}
</React.Fragment>
)
})}
</div>
</div>
</div>
{/* 操作按鈕 */}
<div className="flex gap-3 mb-4">
{!showResult && fillAnswer.trim() && (
<button
onClick={handleFillAnswer}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
確認答案
</button>
)}
<button
onClick={() => setShowHint(!showHint)}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
{showHint ? '隱藏提示' : '顯示提示'}
</button>
</div>
{/* 提示區域 */}
{showHint && (
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-semibold text-yellow-800 mb-2">詞彙定義:</h4>
<p className="text-yellow-800">{definition}</p>
</div>
)}
{/* 結果反饋區 */}
{showResult && (
<div className={`mt-6 p-6 rounded-lg w-full ${
fillAnswer.toLowerCase().trim() === targetWord.toLowerCase()
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
fillAnswer.toLowerCase().trim() === targetWord.toLowerCase()
? 'text-green-700'
: 'text-red-700'
}`}>
{fillAnswer.toLowerCase().trim() === targetWord.toLowerCase() ? '正確!' : '錯誤!'}
</p>
{fillAnswer.toLowerCase().trim() !== targetWord.toLowerCase() && (
<div className="mb-4">
<p className="text-gray-700 text-left">
正確答案是:<strong className="text-lg">{targetWord}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-600">
<strong>發音:</strong>
<span className="mx-2">{pronunciation}</span>
<AudioPlayer text={targetWord} />
</p>
</div>
<div className="text-left">
<p className="text-gray-600">
<strong>完整例句:</strong>"{completeExample}"
</p>
<p className="text-gray-500 text-sm">
<strong>翻譯:</strong>"{exampleTranslation}"
</p>
</div>
</div>
</div>
)}
</div>
🧩 4. 例句重組 (sentence-reorder)
核心特色
- 拖拽式單字重組
- 雙區域設計(可用單字區 + 重組區域)
- 即時狀態反饋
HTML結構
<div className="bg-white rounded-xl shadow-lg p-8">
{/* 標題區 */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">例句重組</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{difficultyLevel}
</span>
</div>
{/* 圖片區(如果有) */}
{exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left">圖片提示</h3>
<img
src={exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => setModalImage(exampleImage)}
/>
</div>
</div>
)}
{/* 重組區域 */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left">重組區域:</h3>
<div className="relative min-h-[120px] bg-gray-50 rounded-lg p-4 border-2 border-dashed border-gray-300">
{arrangedWords.length === 0 ? (
<div className="absolute inset-0 flex items-center justify-center text-gray-400 text-lg">
將下方單字拖拽到這裡組成完整句子
</div>
) : (
<div className="flex flex-wrap gap-2">
{arrangedWords.map((word, index) => (
<div
key={`arranged-${index}`}
className="inline-flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-blue-200 transition-colors"
onClick={() => handleRemoveFromArranged(word)}
>
{word}
<span className="ml-2 text-blue-600 hover:text-blue-800">×</span>
</div>
))}
</div>
)}
</div>
</div>
{/* 指示文字 */}
<p className="text-lg text-gray-700 mb-6 text-left">
點擊下方單字,依序重組成正確的句子:
</p>
{/* 可用單字區域 */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3 text-left">可用單字:</h3>
<div className="bg-white border border-gray-200 rounded-lg p-4 min-h-[80px]">
{shuffledWords.length === 0 ? (
<div className="text-center text-gray-400">
所有單字都已使用
</div>
) : (
<div className="flex flex-wrap gap-2">
{shuffledWords.map((word, index) => (
<button
key={`shuffled-${index}`}
onClick={() => handleWordClick(word)}
className="bg-gray-100 text-gray-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-gray-200 active:bg-gray-300 transition-colors select-none"
>
{word}
</button>
))}
</div>
)}
</div>
</div>
{/* 控制按鈕 */}
<div className="flex gap-3 mb-6">
{arrangedWords.length > 0 && (
<button
onClick={handleCheckReorderAnswer}
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
>
檢查答案
</button>
)}
<button
onClick={handleResetReorder}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
重新開始
</button>
</div>
{/* 結果反饋區 */}
{reorderResult !== null && (
<div className={`p-6 rounded-lg w-full mb-6 ${
reorderResult
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
reorderResult ? 'text-green-700' : 'text-red-700'
}`}>
{reorderResult ? '正確!' : '錯誤!'}
</p>
{!reorderResult && (
<div className="mb-4">
<p className="text-gray-700 text-left">
正確答案是:<strong className="text-lg">{correctSentence}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<div className="flex items-center text-gray-600">
<AudioPlayer text={correctSentence} />
</div>
</div>
<div className="text-left">
<p className="text-gray-600">
{translation}
</p>
</div>
</div>
</div>
)}
</div>
🎧 5. 聽力測驗 (listening)
5.1 詞彙聽力 (vocab-listening)
<div className="bg-white rounded-xl shadow-lg p-8">
{/* 標題區 */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">詞彙聽力</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{difficultyLevel}
</span>
</div>
{/* 指示文字 */}
<p className="text-lg text-gray-700 mb-6 text-left">
請聽發音並選擇正確的英文單字:
</p>
{/* 音頻播放區 */}
<div className="space-y-4 mb-8">
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-semibold text-gray-900 mb-2 text-left">發音</h3>
<div className="flex items-center gap-3">
<span className="text-gray-700">{pronunciation}</span>
<AudioPlayer text={word} />
</div>
</div>
</div>
{/* 選項區域 - 2x2網格布局 */}
<div className="grid grid-cols-2 gap-3 mb-6">
{options.map((word) => (
<button
key={word}
onClick={() => !showResult && handleListeningAnswer(word)}
disabled={showResult}
className={`p-4 text-center rounded-lg border-2 transition-all ${
showResult
? word === correctAnswer
? 'border-green-500 bg-green-50 text-green-700'
: word === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
<div className="text-lg font-medium">{word}</div>
<div className="text-sm text-gray-500 mt-1">{pronunciation}</div>
</button>
))}
</div>
{/* 結果反饋區 */}
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === correctAnswer
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === correctAnswer ? 'text-green-700' : 'text-red-700'
}`}>
{selectedAnswer === correctAnswer ? '正確!' : '錯誤!'}
</p>
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-700">
<strong>正確單字:</strong>{correctAnswer}
</p>
<p className="text-gray-600">
<strong>定義:</strong>{definition}
</p>
</div>
</div>
</div>
)}
</div>
5.2 例句聽力 (sentence-listening)
<div className="bg-white rounded-xl shadow-lg p-8">
{/* 標題區 */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">例句聽力</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{difficultyLevel}
</span>
</div>
{/* 指示文字 */}
<p className="text-lg text-gray-700 mb-6 text-left">
請聽例句並選擇正確的選項:
</p>
{/* 音頻播放區 */}
<div className="text-center mb-8">
<div className="mb-6">
<AudioPlayer text={example} />
<p className="text-sm text-gray-500 mt-2">
點擊播放聽例句
</p>
</div>
</div>
{/* 選項區域 - 垂直列表布局 */}
<div className="grid grid-cols-1 gap-3 mb-6">
{sentenceOptions.map((sentence, idx) => (
<button
key={idx}
onClick={() => !showResult && handleSentenceListeningAnswer(sentence)}
disabled={showResult}
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
showResult
? sentence === correctSentence
? 'border-green-500 bg-green-50 text-green-700'
: sentence === selectedAnswer
? 'border-red-500 bg-red-50 text-red-700'
: 'border-gray-200 bg-gray-50 text-gray-500'
: 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}`}
>
<div className="text-sm text-gray-600 mb-1">選項 {String.fromCharCode(65 + idx)}:</div>
<div className="text-base">{sentence}</div>
</button>
))}
</div>
{/* 結果反饋區 */}
{showResult && (
<div className={`p-6 rounded-lg w-full mb-6 ${
selectedAnswer === correctSentence
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
selectedAnswer === correctSentence ? 'text-green-700' : 'text-red-700'
}`}>
{selectedAnswer === correctSentence ? '正確!' : '錯誤!'}
</p>
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-700">
{correctSentence}
</p>
<p className="text-gray-600">
{translation}
</p>
</div>
</div>
</div>
)}
</div>
🎤 6. 口說測驗 (sentence-speaking)
核心特色
- 整合 VoiceRecorder 組件
- 語音識別評估
- 即時反饋系統
HTML結構
<div className="bg-white rounded-xl shadow-lg p-8">
{/* 標題區 */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900">例句口說</h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{difficultyLevel}
</span>
</div>
{/* VoiceRecorder 組件區域 */}
<div className="w-full">
<VoiceRecorder
targetText={example}
targetTranslation={exampleTranslation}
exampleImage={exampleImage}
instructionText="請看例句圖片並大聲說出完整的例句:"
onRecordingComplete={() => {
handleSpeakingAnswer(example)
}}
/>
</div>
{/* 結果反饋區 */}
{showResult && (
<div className="mt-6 p-6 rounded-lg bg-blue-50 border border-blue-200 w-full">
<p className="text-blue-700 text-left text-xl font-semibold mb-2">
錄音完成!
</p>
<p className="text-gray-600 text-left">
系統正在評估你的發音...
</p>
</div>
)}
</div>
📊 進度條系統設計
雙層進度條
<div className="mb-8">
<div className="flex justify-between items-center mb-3">
<span className="text-sm font-medium text-gray-900">學習進度</span>
<div className="flex items-center gap-6">
{/* 詞卡進度 */}
<div className="flex items-center gap-2 text-sm text-gray-600">
<span>詞卡: {completedCards}/{totalCards}</span>
</div>
{/* 測驗進度 */}
<button
onClick={() => setShowTaskListModal(true)}
className="text-sm text-gray-600 hover:text-blue-600 transition-colors cursor-pointer flex items-center gap-1"
title="點擊查看詳細進度"
>
測驗: {completedTests}/{totalTests}
<span className="text-xs ml-1">📋</span>
</button>
</div>
</div>
{/* 詞卡進度條 */}
<div className="flex items-center gap-2 mb-2">
<span className="text-xs text-gray-500 w-12">詞卡</span>
<div className="flex-1 bg-gray-200 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${(completedCards / totalCards) * 100}%` }}
/>
</div>
<span className="text-xs text-gray-500 w-8">{completedCards}/{totalCards}</span>
</div>
{/* 測驗進度條 */}
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 w-12">測驗</span>
<div
className="flex-1 bg-gray-200 rounded-full h-2 cursor-pointer hover:bg-gray-300 transition-colors"
onClick={() => setShowTaskListModal(true)}
title="點擊查看詳細進度"
>
<div
className="bg-blue-500 h-2 rounded-full transition-all hover:bg-blue-600"
style={{ width: `${(completedTests / totalTests) * 100}%` }}
/>
</div>
<span className="text-xs text-gray-500 w-8">{completedTests}/{totalTests}</span>
</div>
</div>
🎨 通用Modal設計
圖片放大Modal
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50" onClick={() => setModalImage(null)}>
<div className="relative max-w-4xl max-h-[90vh] mx-4">
<img
src={modalImage}
alt="放大圖片"
className="max-w-full max-h-full rounded-lg"
/>
<button
onClick={() => setModalImage(null)}
className="absolute top-4 right-4 bg-black bg-opacity-50 text-white p-2 rounded-full hover:bg-opacity-75"
>
✕
</button>
</div>
</div>
任務清單Modal
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-2xl max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
📚 學習任務清單
</h2>
<button className="text-gray-400 hover:text-gray-600 text-2xl">✕</button>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto max-h-[60vh]">
{/* 進度統計 */}
<div className="mb-6 bg-blue-50 rounded-lg p-4">
<div className="flex items-center justify-between text-sm">
<span className="text-blue-900 font-medium">
測驗進度: {completedTests} / {totalTests} ({progressPercentage}%)
</span>
<div className="flex items-center gap-4 text-blue-800">
<span>✅ 已完成: {completedCount}</span>
<span>⏳ 進行中: {currentCount}</span>
<span>⚪ 待完成: {pendingCount}</span>
</div>
</div>
<div className="mt-3 w-full bg-blue-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${progressPercentage}%` }}
/>
</div>
</div>
{/* 測驗清單 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{testItems.map((item) => (
<div
key={item.id}
className={`flex items-center gap-3 p-3 rounded-lg transition-all ${
item.isCompleted
? 'bg-green-50 border border-green-200'
: item.isCurrent
? 'bg-blue-50 border border-blue-300 shadow-sm'
: 'bg-gray-50 border border-gray-200'
}`}
>
<span className="text-lg">
{item.isCompleted ? '✅' : item.isCurrent ? '⏳' : '⚪'}
</span>
<div className="flex-1">
<div className="font-medium text-sm">
{item.order}. {item.word} - {item.testName}
</div>
<div className={`text-xs ${
item.isCompleted ? 'text-green-600' :
item.isCurrent ? 'text-blue-600' : 'text-gray-500'
}`}>
{item.isCompleted ? '已完成' :
item.isCurrent ? '進行中' : '待完成'}
</div>
</div>
</div>
))}
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
<button className="w-full py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors">
關閉
</button>
</div>
</div>
</div>
🎮 導航控制設計
智能導航按鈕
<div className="flex gap-4 mt-6">
{/* 上一張按鈕 */}
<button
onClick={handlePrevious}
disabled={currentCardIndex === 0}
className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
>
上一張
</button>
{/* 動態下一張按鈕 */}
<button
onClick={handleNext}
className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
>
{currentCardIndex < totalCards - 1 ? '下一張' : '完成學習'}
</button>
</div>
跳過功能按鈕
{/* 跳過按鈕(答題前狀態) */}
{!showResult && (
<div className="flex justify-center mb-4">
<button
onClick={handleSkip}
className="px-6 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition-colors"
>
⏭️ 暫時跳過
</button>
</div>
)}
{/* 繼續按鈕(答題後狀態) */}
{showResult && (
<div className="flex justify-center mt-6">
<button
onClick={handleNext}
className="px-8 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors font-medium"
>
繼續下一個 →
</button>
</div>
)}
🎨 設計特色總結
1. 一致性設計
- 所有測驗類型都採用相同的容器結構 (
bg-white rounded-xl shadow-lg p-8) - 統一的配色方案和視覺層次
- 標準化的按鈕樣式和hover效果
2. 響應式布局
- 使用 Grid 和 Flexbox 布局
- 適配不同螢幕尺寸 (
md:grid-cols-2 lg:grid-cols-3) - 移動端友好的觸控設計
3. 互動動畫
- 翻卡3D效果 (
transform: rotateY(180deg)) - Hover狀態轉換 (
transition-all,hover:bg-blue-50) - 按鈕點擊反饋 (
active:bg-gray-300)
4. 無障礙設計
- 鍵盤導航支援 (
onKeyDownEnter 鍵提交) - 清晰的狀態反饋 (
disabled狀態顯示) - 適當的對比度和字體大小
5. 視覺層次
- 合理的間距系統 (
mb-6,p-4,gap-3) - 字體大小層次 (
text-4xl,text-2xl,text-lg,text-sm) - 顏色對比設計 (
text-gray-900,text-gray-700,text-gray-500)
6. 即時反饋
- 答題後的即時視覺反饋(綠色成功,紅色錯誤)
- 動態進度條更新
- 狀態圖標顯示 (
✅,❌,⏳,⚪)
📝 實作注意事項
必需依賴組件
- AudioPlayer - 音頻播放功能
- VoiceRecorder - 語音錄製功能
- MasteryIndicator - 熟悉度顯示
- ReviewTypeIndicator - 測驗類型指示器
關鍵狀態管理
isFlipped- 翻卡狀態showResult- 顯示答題結果selectedAnswer- 用戶選擇的答案fillAnswer- 填空題答案arrangedWords/shuffledWords- 重組題狀態
動畫效果實作
- 使用 CSS
transition-all實現平滑過渡 - 翻卡動畫需要
transform-style: preserve-3d - 進度條動畫使用
transition-all配合動態width樣式
這份文件保留了原始設計的所有精華和用戶體驗細節,可作為重新實作的完整參考。