1096 lines
35 KiB
Markdown
1096 lines
35 KiB
Markdown
# 詞彙學習 - UI設計規範
|
||
|
||
**文件版本**: 1.0
|
||
**建立日期**: 2025-09-27
|
||
**目標讀者**: 前端實作工程師、UI設計師
|
||
**用途**: HTML/CSS實作、視覺設計、組件規範
|
||
**配合文檔**: [智能複習系統-開發指南.md](/note/智能複習/智能複習系統-開發指南.md) - 系統架構和業務邏輯
|
||
**來源**: 從備份檔案 `page-v1-original.tsx` 提取的完整UI設計規格
|
||
|
||
---
|
||
|
||
## 📋 總體設計架構
|
||
|
||
### 核心測驗類型
|
||
該系統包含6種主要測驗類型:
|
||
1. **翻卡記憶** (flip-memory)
|
||
2. **詞彙選擇** (vocab-choice)
|
||
3. **例句填空** (sentence-fill)
|
||
4. **例句重組** (sentence-reorder)
|
||
5. **聽力測驗** (listening - vocab-listening, sentence-listening)
|
||
6. **口說測驗** (speaking - sentence-speaking)
|
||
|
||
### 共同設計元素
|
||
|
||
#### 通用容器結構
|
||
```jsx
|
||
<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>
|
||
```
|
||
|
||
#### 通用標題區
|
||
```jsx
|
||
<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結構
|
||
```jsx
|
||
<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動畫樣式
|
||
```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結構
|
||
```jsx
|
||
<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結構
|
||
```jsx
|
||
<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結構
|
||
```jsx
|
||
<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)
|
||
```jsx
|
||
<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)
|
||
```jsx
|
||
<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結構
|
||
```jsx
|
||
<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>
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 進度條系統設計
|
||
|
||
### 雙層進度條
|
||
```jsx
|
||
<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
|
||
```jsx
|
||
<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
|
||
```jsx
|
||
<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>
|
||
```
|
||
|
||
---
|
||
|
||
## 🎮 導航控制設計
|
||
|
||
### 智能導航按鈕
|
||
```jsx
|
||
<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>
|
||
```
|
||
|
||
### 跳過功能按鈕
|
||
```jsx
|
||
{/* 跳過按鈕(答題前狀態) */}
|
||
{!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. **無障礙設計**
|
||
- 鍵盤導航支援 (`onKeyDown` Enter 鍵提交)
|
||
- 清晰的狀態反饋 (`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. **即時反饋**
|
||
- 答題後的即時視覺反饋(綠色成功,紅色錯誤)
|
||
- 動態進度條更新
|
||
- 狀態圖標顯示 (`✅`, `❌`, `⏳`, `⚪`)
|
||
|
||
---
|
||
|
||
## 📝 實作注意事項
|
||
|
||
### 必需依賴組件
|
||
1. **AudioPlayer** - 音頻播放功能
|
||
2. **VoiceRecorder** - 語音錄製功能
|
||
3. **MasteryIndicator** - 熟悉度顯示
|
||
4. **ReviewTypeIndicator** - 測驗類型指示器
|
||
|
||
### 關鍵狀態管理
|
||
- `isFlipped` - 翻卡狀態
|
||
- `showResult` - 顯示答題結果
|
||
- `selectedAnswer` - 用戶選擇的答案
|
||
- `fillAnswer` - 填空題答案
|
||
- `arrangedWords` / `shuffledWords` - 重組題狀態
|
||
|
||
### 動畫效果實作
|
||
- 使用 CSS `transition-all` 實現平滑過渡
|
||
- 翻卡動畫需要 `transform-style: preserve-3d`
|
||
- 進度條動畫使用 `transition-all` 配合動態 `width` 樣式
|
||
|
||
這份文件保留了原始設計的所有精華和用戶體驗細節,可作為重新實作的完整參考。 |