feat: 完成手動重構並建立階段4優化計劃
## 🎯 重構成果 - ✅ VocabChoiceTest: 149行→127行 (-15%, 使用ChoiceTestProps) - ✅ SentenceReorderTest: 220行→202行 (-8%, 使用ReorderTestProps) - ✅ review-design頁面: 更新支援新架構cardData傳遞 - ✅ 統一ErrorReportButton共用組件應用 ## 📝 計劃文檔 - 📋 更新現有優化計劃進度狀態 - 🚀 新增階段4詳細優化計劃 (效能/錯誤處理/UX) ## 🔧 技術成就 - 手動重構方法驗證成功 (避免全局替換風險) - 共用架構價值實現 (40行代碼減少) - TypeScript類型安全完整實現 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
eac856d07b
commit
35b3072852
|
|
@ -0,0 +1,375 @@
|
|||
# Review-Tests 組件階段4優化計劃
|
||||
|
||||
## 🎯 概述
|
||||
|
||||
基於前期重構成果,本階段專注於效能優化、錯誤處理改善和使用者體驗統一,將系統提升到產品級標準。
|
||||
|
||||
### **前期成果回顧**
|
||||
- ✅ **VocabChoiceTest**: 149行→127行 (-15%)
|
||||
- ✅ **SentenceReorderTest**: 220行→202行 (-8%)
|
||||
- ✅ **共用架構**: 成功建立並應用
|
||||
- ✅ **同義詞功能**: 全面整合
|
||||
|
||||
---
|
||||
|
||||
## 📈 階段4-1: 效能優化
|
||||
|
||||
### **🎯 目標**
|
||||
- 減少重複渲染 20-30%
|
||||
- 優化 bundle 大小
|
||||
- 改善初始載入速度
|
||||
|
||||
### **🔧 具體實施**
|
||||
|
||||
#### **1.1 React 效能優化**
|
||||
|
||||
**組件記憶化**
|
||||
```typescript
|
||||
// 對重構後的組件應用 React.memo
|
||||
export const VocabChoiceTest = React.memo<VocabChoiceTestProps>(({
|
||||
cardData,
|
||||
options,
|
||||
onAnswer,
|
||||
onReportError,
|
||||
disabled
|
||||
}) => {
|
||||
// ... 組件邏輯
|
||||
})
|
||||
```
|
||||
|
||||
**回調函數優化**
|
||||
```typescript
|
||||
// 使用 useCallback 優化事件處理函數
|
||||
const handleAnswerSelect = useCallback((answer: string) => {
|
||||
if (disabled || showResult) return
|
||||
setSelectedAnswer(answer)
|
||||
setShowResult(true)
|
||||
onAnswer(answer)
|
||||
}, [disabled, showResult, onAnswer])
|
||||
```
|
||||
|
||||
**計算結果記憶化**
|
||||
```typescript
|
||||
// 對複雜計算使用 useMemo
|
||||
const isCorrect = useMemo(() =>
|
||||
selectedAnswer === cardData.word
|
||||
, [selectedAnswer, cardData.word])
|
||||
```
|
||||
|
||||
#### **1.2 依賴優化**
|
||||
- 檢查並移除未使用的 imports
|
||||
- 優化 useEffect 依賴項
|
||||
- 確保共用組件正確樹搖
|
||||
|
||||
#### **1.3 效能監控**
|
||||
```typescript
|
||||
// 添加效能測量
|
||||
const startTime = performance.now()
|
||||
// 組件渲染
|
||||
const renderTime = performance.now() - startTime
|
||||
console.log(`組件渲染時間: ${renderTime}ms`)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 階段4-2: 錯誤處理改善
|
||||
|
||||
### **🎯 目標**
|
||||
- 統一錯誤處理機制
|
||||
- 改善錯誤報告UX
|
||||
- 增強系統穩定性
|
||||
|
||||
### **🔧 具體實施**
|
||||
|
||||
#### **2.1 統一錯誤邊界組件**
|
||||
|
||||
**創建 ReviewErrorBoundary**
|
||||
```typescript
|
||||
// frontend/components/review/shared/ReviewErrorBoundary.tsx
|
||||
interface ReviewErrorBoundaryProps {
|
||||
children: React.ReactNode
|
||||
fallback?: React.ComponentType<{ error: Error }>
|
||||
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
||||
}
|
||||
|
||||
export class ReviewErrorBoundary extends Component<ReviewErrorBoundaryProps> {
|
||||
// 錯誤捕獲和處理邏輯
|
||||
// 提供用戶友好的錯誤界面
|
||||
// 整合錯誤回報功能
|
||||
}
|
||||
```
|
||||
|
||||
**錯誤恢復機制**
|
||||
```typescript
|
||||
// 自動重試機制
|
||||
// 錯誤狀態重置
|
||||
// 用戶手動恢復選項
|
||||
```
|
||||
|
||||
#### **2.2 ErrorReportButton 增強**
|
||||
|
||||
**功能增強**
|
||||
```typescript
|
||||
// 添加 loading 狀態
|
||||
// 成功/失敗反饋
|
||||
// 錯誤詳細信息收集
|
||||
interface EnhancedErrorReportButtonProps {
|
||||
onClick: () => void
|
||||
loading?: boolean
|
||||
success?: boolean
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
**UX 改善**
|
||||
- 點擊後顯示提交狀態
|
||||
- 成功後顯示確認訊息
|
||||
- 失敗時提供重試選項
|
||||
|
||||
#### **2.3 類型安全強化**
|
||||
|
||||
**運行時驗證**
|
||||
```typescript
|
||||
// 添加 cardData 驗證函數
|
||||
const validateCardData = (data: unknown): data is ReviewCardData => {
|
||||
// 詳細的運行時類型檢查
|
||||
}
|
||||
```
|
||||
|
||||
**錯誤類型定義**
|
||||
```typescript
|
||||
// 統一錯誤類型
|
||||
interface ReviewError {
|
||||
type: 'validation' | 'network' | 'component'
|
||||
message: string
|
||||
componentName?: string
|
||||
timestamp: Date
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 階段4-3: 使用者體驗統一
|
||||
|
||||
### **🎯 目標**
|
||||
- 建立一致的視覺語言
|
||||
- 統一互動模式
|
||||
- 改善響應式體驗
|
||||
|
||||
### **🔧 具體實施**
|
||||
|
||||
#### **3.1 視覺一致性規範**
|
||||
|
||||
**設計系統建立**
|
||||
```typescript
|
||||
// frontend/styles/review-design-system.ts
|
||||
export const ReviewDesignSystem = {
|
||||
colors: {
|
||||
primary: '#3B82F6',
|
||||
success: '#10B981',
|
||||
error: '#EF4444',
|
||||
warning: '#F59E0B'
|
||||
},
|
||||
animations: {
|
||||
duration: {
|
||||
fast: '150ms',
|
||||
normal: '300ms',
|
||||
slow: '500ms'
|
||||
},
|
||||
easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
},
|
||||
spacing: {
|
||||
xs: '0.25rem',
|
||||
sm: '0.5rem',
|
||||
md: '1rem',
|
||||
lg: '1.5rem',
|
||||
xl: '2rem'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**統一動畫**
|
||||
```typescript
|
||||
// 所有按鈕使用相同的過渡效果
|
||||
const buttonTransition = 'transition-all duration-300 ease-in-out'
|
||||
|
||||
// 統一的懸停效果
|
||||
const hoverEffects = 'hover:scale-105 hover:shadow-lg'
|
||||
```
|
||||
|
||||
#### **3.2 互動體驗優化**
|
||||
|
||||
**載入狀態組件**
|
||||
```typescript
|
||||
// frontend/components/review/shared/LoadingSpinner.tsx
|
||||
interface LoadingSpinnerProps {
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
color?: 'primary' | 'secondary'
|
||||
text?: string
|
||||
}
|
||||
```
|
||||
|
||||
**按鈕反饋增強**
|
||||
```typescript
|
||||
// 添加 ripple 效果
|
||||
// 統一的點擊動畫
|
||||
// 禁用狀態視覺反饋
|
||||
```
|
||||
|
||||
#### **3.3 響應式設計改善**
|
||||
|
||||
**手機端優化**
|
||||
```css
|
||||
/* 觸控友好的按鈕大小 */
|
||||
@media (max-width: 768px) {
|
||||
.touch-button {
|
||||
min-height: 44px; /* Apple 建議的最小觸控目標 */
|
||||
min-width: 44px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**斷點標準化**
|
||||
```typescript
|
||||
// 統一的響應式斷點
|
||||
const breakpoints = {
|
||||
sm: '640px',
|
||||
md: '768px',
|
||||
lg: '1024px',
|
||||
xl: '1280px'
|
||||
}
|
||||
```
|
||||
|
||||
#### **3.4 無障礙功能增強**
|
||||
|
||||
**ARIA 標籤**
|
||||
```typescript
|
||||
// 為所有互動元素添加適當的 ARIA 標籤
|
||||
<button
|
||||
aria-label="選擇答案選項"
|
||||
aria-describedby="option-description"
|
||||
role="button"
|
||||
>
|
||||
```
|
||||
|
||||
**鍵盤導航**
|
||||
```typescript
|
||||
// 統一的鍵盤事件處理
|
||||
const useKeyboardNavigation = () => {
|
||||
// Tab 鍵導航
|
||||
// Enter/Space 鍵選擇
|
||||
// Escape 鍵取消
|
||||
}
|
||||
```
|
||||
|
||||
**螢幕閱讀器支援**
|
||||
```typescript
|
||||
// 添加 live regions 用於動態內容
|
||||
<div aria-live="polite" aria-atomic="true">
|
||||
{showResult && `答案${isCorrect ? '正確' : '錯誤'}`}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技術實施細節
|
||||
|
||||
### **新增共用組件清單**
|
||||
|
||||
```
|
||||
frontend/components/review/shared/
|
||||
├── LoadingSpinner.tsx // 統一載入指示器
|
||||
├── ReviewErrorBoundary.tsx // 錯誤邊界組件
|
||||
├── AnimatedContainer.tsx // 統一動畫容器
|
||||
├── TouchFriendlyButton.tsx // 觸控優化按鈕
|
||||
└── AccessibleContent.tsx // 無障礙內容包裝器
|
||||
```
|
||||
|
||||
### **增強現有組件**
|
||||
|
||||
**ErrorReportButton 增強版**
|
||||
```typescript
|
||||
interface EnhancedErrorReportButtonProps {
|
||||
onClick: () => Promise<void>
|
||||
className?: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
variant?: 'default' | 'minimal'
|
||||
}
|
||||
```
|
||||
|
||||
**ConfidenceButtons 優化版**
|
||||
```typescript
|
||||
// 添加觸控優化
|
||||
// 改善視覺反饋
|
||||
// 增強無障礙支援
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 實施順序和優先級
|
||||
|
||||
### **第1週: 效能優化 (高優先級)**
|
||||
1. 添加 React.memo 到重構組件
|
||||
2. 優化 useCallback/useMemo 使用
|
||||
3. 檢查並移除未使用代碼
|
||||
4. 效能測量和基準建立
|
||||
|
||||
### **第2週: 錯誤處理 (中優先級)**
|
||||
1. 創建 ReviewErrorBoundary 組件
|
||||
2. 增強 ErrorReportButton 功能
|
||||
3. 添加類型安全驗證
|
||||
4. 錯誤監控整合
|
||||
|
||||
### **第3週: 使用者體驗 (高優先級)**
|
||||
1. 建立設計系統規範
|
||||
2. 統一動畫和過渡效果
|
||||
3. 響應式設計改善
|
||||
4. 無障礙功能增強
|
||||
|
||||
### **第4週: 測試和調優**
|
||||
1. 效能測試和調優
|
||||
2. 用戶體驗測試
|
||||
3. 無障礙功能測試
|
||||
4. 文檔更新和總結
|
||||
|
||||
---
|
||||
|
||||
## 🎯 預期效果量化
|
||||
|
||||
### **效能提升目標**
|
||||
- **渲染效能**: 減少 20-30% 重複渲染
|
||||
- **Bundle 大小**: 減少 5-10% 未使用代碼
|
||||
- **初始載入**: 改善 15-20% 載入時間
|
||||
- **記憶體使用**: 優化 10-15% 記憶體佔用
|
||||
|
||||
### **用戶體驗改善**
|
||||
- **視覺一致性**: 100% 組件遵循設計系統
|
||||
- **互動流暢度**: 統一 300ms 動畫標準
|
||||
- **錯誤處理**: 95% 錯誤情況有適當處理
|
||||
- **無障礙支援**: 符合 WCAG 2.1 AA 標準
|
||||
|
||||
### **維護性提升**
|
||||
- **代碼複用**: 新增 5+ 共用組件
|
||||
- **錯誤監控**: 100% 組件有錯誤邊界保護
|
||||
- **類型安全**: 強化運行時驗證
|
||||
- **文檔完整性**: 完整的使用指南和範例
|
||||
|
||||
---
|
||||
|
||||
## ⚡ 成功指標
|
||||
|
||||
### **技術指標**
|
||||
- Lighthouse 效能分數 > 90
|
||||
- Bundle analyzer 顯示無重複依賴
|
||||
- TypeScript 編譯 0 錯誤 0 警告
|
||||
- 所有組件通過無障礙測試
|
||||
|
||||
### **業務指標**
|
||||
- 用戶操作流暢度提升
|
||||
- 錯誤報告減少
|
||||
- 開發效率提升
|
||||
- 維護成本降低
|
||||
|
||||
---
|
||||
|
||||
*此計劃將 Review-Tests 組件系統提升到產品級標準,確保優秀的效能、穩定性和使用者體驗。*
|
||||
|
|
@ -110,17 +110,17 @@ export const FlipMemoryTest: React.FC<BaseReviewProps> = ({ cardData, ...props }
|
|||
- [x] `ConfidenceButtons.tsx` - 信心度選擇按鈕
|
||||
- [x] `ErrorReportButton.tsx` - 錯誤回報按鈕
|
||||
|
||||
### **階段 2: 重構現有組件** 🚧 **部分完成**
|
||||
- [x] FlipMemoryTest 同義詞整合 + 架構應用 (270行→212行, -21%) ✅
|
||||
- [x] VocabChoiceTest 同義詞整合 (添加同義詞功能) ✅
|
||||
### **階段 2: 重構現有組件** ✅ **手動重構完成**
|
||||
- [x] FlipMemoryTest 同義詞整合 (添加同義詞功能) ✅
|
||||
- [x] VocabChoiceTest 同義詞整合 + 架構應用 (149行→127行, -15%) ✅
|
||||
- [x] SentenceFillTest 同義詞整合 (添加同義詞功能) ✅
|
||||
- [x] FlipMemoryTest 共用架構應用 (ErrorReportButton, SynonymsDisplay, ConfidenceButtons) ✅
|
||||
- [ ] 其他組件共用架構應用 (因全局替換問題暫停,需手動重構)
|
||||
- [x] SentenceReorderTest 架構應用 (220行→202行, -8%) ✅
|
||||
- [x] 安全手動重構方法驗證 (避免全局替換風險) ✅
|
||||
|
||||
### **階段 3: 統一整合** ⏳ **待執行**
|
||||
- [ ] 更新 review-design 頁面
|
||||
- [ ] 統一 props 傳遞
|
||||
- [ ] 測試所有功能
|
||||
### **階段 3: 統一整合** ✅ **已完成**
|
||||
- [x] 更新 review-design 頁面支援新架構 ✅
|
||||
- [x] 統一 props 傳遞結構 (cardData) ✅
|
||||
- [x] 測試編譯和類型安全 ✅
|
||||
|
||||
### **階段 4: 優化與測試** ⏳ **待執行**
|
||||
- [ ] 效能優化
|
||||
|
|
@ -154,12 +154,12 @@ frontend/
|
|||
- **SentenceFillTest**: 9513 bytes + synonyms (已添加同義詞功能 ✅)
|
||||
- **實際效果**: 所有組件已完成同義詞功能整合 ✅,架構優化未實際應用
|
||||
|
||||
### **✅ 實際完成成果** (2025-09-28 最新更新)
|
||||
### **✅ 實際完成成果** (2025-09-28 最終更新)
|
||||
1. **完整的基礎架構** - types, hooks, shared components 全部建立 ✅
|
||||
2. **FlipMemoryTest 同義詞整合** - 添加同義詞顯示功能 ✅
|
||||
3. **VocabChoiceTest 同義詞整合** - 已添加同義詞功能 ✅
|
||||
4. **SentenceFillTest 同義詞整合** - 已添加同義詞功能 ✅
|
||||
5. **FlipMemoryTest 架構應用** - 成功重構使用共用架構 (270行→212行, -21%) ✅
|
||||
2. **全面同義詞整合** - 所有組件已添加同義詞功能 ✅
|
||||
3. **VocabChoiceTest 架構重構** - 149行→127行 (-15%, 22行減少) ✅
|
||||
4. **SentenceReorderTest 架構重構** - 220行→202行 (-8%, 18行減少) ✅
|
||||
5. **review-design 頁面整合** - 支援新架構的 props 傳遞 ✅
|
||||
|
||||
### **🎯 實際可用優勢**
|
||||
- ✅ **完整基礎架構** - 為未來優化準備了完整的工具
|
||||
|
|
@ -168,29 +168,34 @@ frontend/
|
|||
- ✅ **FlipMemoryTest 優化** - 成功應用共用架構,減少21%程式碼
|
||||
- ⏳ **其他組件優化** - 架構已建立,可繼續應用於其他組件
|
||||
|
||||
### **🔄 當前實際狀態** (2025-09-28 18:55)
|
||||
### **🔄 最終實際狀態** (2025-09-28 19:10)
|
||||
|
||||
#### **✅ 已完成**
|
||||
- **FlipMemoryTest**: 成功應用共用架構,270行→212行 (-21%)
|
||||
- 使用 `ConfidenceTestProps` 介面
|
||||
- 應用 `ErrorReportButton`, `SynonymsDisplay`, `ConfidenceButtons`
|
||||
- 編譯正常,功能完整
|
||||
#### **✅ 成功完成的重構**
|
||||
1. **VocabChoiceTest**: 149行→127行 (-15%, -22行)
|
||||
- 使用 `ChoiceTestProps` 介面
|
||||
- 應用 `ErrorReportButton` 共用組件
|
||||
- 統一 `cardData` 參數結構
|
||||
|
||||
#### **⚠️ 暫停的重構**
|
||||
- **SentenceFillTest**, **SentenceReorderTest**, **VocabChoiceTest**
|
||||
- 全局字串替換導致語法錯誤
|
||||
- 已回滾到穩定狀態 (同義詞功能保留)
|
||||
- 需要更細緻的手動重構方法
|
||||
2. **SentenceReorderTest**: 220行→202行 (-8%, -18行)
|
||||
- 使用 `ReorderTestProps` 介面
|
||||
- 應用 `ErrorReportButton` 共用組件
|
||||
- 統一 `cardData` 參數結構
|
||||
|
||||
#### **📋 下一步選項**
|
||||
1. **繼續手動重構** - 逐步重構其他組件 (更安全但較慢)
|
||||
2. **保持現狀** - FlipMemoryTest 重構成功,其他組件保持原狀
|
||||
3. **測試當前狀態** - 驗證 FlipMemoryTest 重構效果
|
||||
3. **review-design 頁面整合**: 已更新支援新架構
|
||||
- VocabChoiceTest 和 SentenceReorderTest 使用新 props 結構
|
||||
- 正確的 `cardData` 傳遞和類型安全
|
||||
|
||||
#### **⚡ 技術學習**
|
||||
- ✅ **重構方法驗證** - 共用架構確實可行且有效
|
||||
- ⚠️ **全局替換風險** - 需要更精確的重構策略
|
||||
- 📝 **建議方法** - 分步驟、小範圍、頻繁測試
|
||||
#### **📊 總體效果**
|
||||
- **代碼減少**: 40行 (約3.3%優化)
|
||||
- **重構組件**: 2/7 (29% 完成率)
|
||||
- **架構驗證**: ✅ 手動重構方法安全有效
|
||||
- **類型安全**: ✅ 完整的 TypeScript 支援
|
||||
|
||||
#### **⚡ 技術成就**
|
||||
- ✅ **共用架構價值驗證** - 確實能簡化代碼並提升一致性
|
||||
- ✅ **安全重構方法** - 手動逐步重構避免語法錯誤
|
||||
- ✅ **統一介面設計** - `ReviewCardData` 和專用 Props 成功應用
|
||||
- 📝 **方法論建立** - 為後續組件重構提供了成功模式
|
||||
|
||||
## 🎯 預期效果
|
||||
|
||||
|
|
|
|||
|
|
@ -174,12 +174,11 @@ export default function ReviewTestsPage() {
|
|||
|
||||
{activeTab === 'VocabChoiceTest' && (
|
||||
<VocabChoiceTest
|
||||
word={mockCardData.word}
|
||||
definition={mockCardData.definition}
|
||||
example={mockCardData.example}
|
||||
exampleTranslation={mockCardData.exampleTranslation}
|
||||
pronunciation={mockCardData.pronunciation}
|
||||
difficultyLevel={mockCardData.difficultyLevel}
|
||||
cardData={{
|
||||
...mockCardData,
|
||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
||||
synonyms: mockCardData.synonyms || []
|
||||
}}
|
||||
options={vocabChoiceOptions}
|
||||
onAnswer={handleAnswer}
|
||||
onReportError={handleReportError}
|
||||
|
|
@ -204,11 +203,11 @@ export default function ReviewTestsPage() {
|
|||
|
||||
{activeTab === 'SentenceReorderTest' && (
|
||||
<SentenceReorderTest
|
||||
word={mockCardData.word}
|
||||
definition={mockCardData.definition}
|
||||
example={mockCardData.example}
|
||||
exampleTranslation={mockCardData.exampleTranslation}
|
||||
difficultyLevel={mockCardData.difficultyLevel}
|
||||
cardData={{
|
||||
...mockCardData,
|
||||
id: currentCard?.id || `card-${currentCardIndex}`,
|
||||
synonyms: mockCardData.synonyms || []
|
||||
}}
|
||||
exampleImage={mockCardData.exampleImage}
|
||||
onAnswer={handleAnswer}
|
||||
onReportError={handleReportError}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,15 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import AudioPlayer from '@/components/AudioPlayer'
|
||||
import { ReorderTestProps } from '@/types/review'
|
||||
import { ErrorReportButton } from '@/components/review/shared'
|
||||
|
||||
interface SentenceReorderTestProps {
|
||||
word: string
|
||||
definition: string
|
||||
example: string
|
||||
exampleTranslation: string
|
||||
difficultyLevel: string
|
||||
interface SentenceReorderTestProps extends ReorderTestProps {
|
||||
exampleImage?: string
|
||||
onAnswer: (answer: string) => void
|
||||
onReportError: () => void
|
||||
onImageClick?: (image: string) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const SentenceReorderTest: React.FC<SentenceReorderTestProps> = ({
|
||||
word,
|
||||
definition,
|
||||
example,
|
||||
exampleTranslation,
|
||||
difficultyLevel,
|
||||
cardData,
|
||||
exampleImage,
|
||||
onAnswer,
|
||||
onReportError,
|
||||
|
|
@ -33,11 +23,11 @@ export const SentenceReorderTest: React.FC<SentenceReorderTestProps> = ({
|
|||
|
||||
// 初始化單字順序
|
||||
useEffect(() => {
|
||||
const words = example.split(/\s+/).filter(word => word.length > 0)
|
||||
const words = cardData.example.split(/\s+/).filter(word => word.length > 0)
|
||||
const shuffled = [...words].sort(() => Math.random() - 0.5)
|
||||
setShuffledWords(shuffled)
|
||||
setArrangedWords([])
|
||||
}, [example])
|
||||
}, [cardData.example])
|
||||
|
||||
const handleWordClick = (word: string) => {
|
||||
if (disabled || showResult) return
|
||||
|
|
@ -54,7 +44,7 @@ export const SentenceReorderTest: React.FC<SentenceReorderTestProps> = ({
|
|||
const handleCheckAnswer = () => {
|
||||
if (disabled || showResult || arrangedWords.length === 0) return
|
||||
const userSentence = arrangedWords.join(' ')
|
||||
const isCorrect = userSentence.toLowerCase().trim() === example.toLowerCase().trim()
|
||||
const isCorrect = userSentence.toLowerCase().trim() === cardData.example.toLowerCase().trim()
|
||||
setReorderResult(isCorrect)
|
||||
setShowResult(true)
|
||||
onAnswer(userSentence)
|
||||
|
|
@ -62,7 +52,7 @@ export const SentenceReorderTest: React.FC<SentenceReorderTestProps> = ({
|
|||
|
||||
const handleReset = () => {
|
||||
if (disabled || showResult) return
|
||||
const words = example.split(/\s+/).filter(word => word.length > 0)
|
||||
const words = cardData.example.split(/\s+/).filter(word => word.length > 0)
|
||||
const shuffled = [...words].sort(() => Math.random() - 0.5)
|
||||
setShuffledWords(shuffled)
|
||||
setArrangedWords([])
|
||||
|
|
@ -71,22 +61,14 @@ export const SentenceReorderTest: React.FC<SentenceReorderTestProps> = ({
|
|||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* 錯誤回報按鈕 */}
|
||||
<div className="flex justify-end mb-2">
|
||||
<button
|
||||
onClick={onReportError}
|
||||
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
🚩 回報錯誤
|
||||
</button>
|
||||
</div>
|
||||
<ErrorReportButton onClick={onReportError} />
|
||||
|
||||
<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}
|
||||
{cardData.difficultyLevel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -195,7 +177,7 @@ export const SentenceReorderTest: React.FC<SentenceReorderTestProps> = ({
|
|||
{!reorderResult && (
|
||||
<div className="mb-4">
|
||||
<p className="text-gray-700 text-left">
|
||||
正確答案是:<strong className="text-lg">{example}</strong>
|
||||
正確答案是:<strong className="text-lg">{cardData.example}</strong>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -203,13 +185,13 @@ export const SentenceReorderTest: React.FC<SentenceReorderTestProps> = ({
|
|||
<div className="space-y-3">
|
||||
<div className="text-left">
|
||||
<div className="flex items-center text-gray-600">
|
||||
<AudioPlayer text={example} />
|
||||
<AudioPlayer text={cardData.example} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-left">
|
||||
<p className="text-gray-600">
|
||||
{exampleTranslation}
|
||||
{cardData.exampleTranslation}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,14 @@
|
|||
import { useState } from 'react'
|
||||
import AudioPlayer from '@/components/AudioPlayer'
|
||||
import { ChoiceTestProps } from '@/types/review'
|
||||
import { ErrorReportButton } from '@/components/review/shared'
|
||||
|
||||
interface VocabChoiceTestProps {
|
||||
word: string
|
||||
definition: string
|
||||
example: string
|
||||
exampleTranslation: string
|
||||
pronunciation?: string
|
||||
synonyms?: string[]
|
||||
difficultyLevel: string
|
||||
options: string[]
|
||||
onAnswer: (answer: string) => void
|
||||
onReportError: () => void
|
||||
disabled?: boolean
|
||||
interface VocabChoiceTestProps extends ChoiceTestProps {
|
||||
// VocabChoiceTest specific props (if any)
|
||||
}
|
||||
|
||||
export const VocabChoiceTest: React.FC<VocabChoiceTestProps> = ({
|
||||
word,
|
||||
definition,
|
||||
example,
|
||||
exampleTranslation,
|
||||
pronunciation,
|
||||
synonyms = [],
|
||||
difficultyLevel,
|
||||
cardData,
|
||||
options,
|
||||
onAnswer,
|
||||
onReportError,
|
||||
|
|
@ -38,26 +24,18 @@ export const VocabChoiceTest: React.FC<VocabChoiceTestProps> = ({
|
|||
onAnswer(answer)
|
||||
}
|
||||
|
||||
const isCorrect = selectedAnswer === word
|
||||
const isCorrect = selectedAnswer === cardData.word
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* 錯誤回報按鈕 */}
|
||||
<div className="flex justify-end mb-2">
|
||||
<button
|
||||
onClick={onReportError}
|
||||
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
🚩 回報錯誤
|
||||
</button>
|
||||
</div>
|
||||
<ErrorReportButton onClick={onReportError} />
|
||||
|
||||
<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}
|
||||
{cardData.difficultyLevel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -70,15 +48,15 @@ export const VocabChoiceTest: React.FC<VocabChoiceTestProps> = ({
|
|||
<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>
|
||||
<p className="text-gray-700 text-left">{cardData.definition}</p>
|
||||
</div>
|
||||
|
||||
{/* 同義詞顯示 */}
|
||||
{synonyms && synonyms.length > 0 && (
|
||||
{cardData.synonyms && cardData.synonyms.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-lg p-4 mb-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-2 text-left">同義詞提示</h3>
|
||||
<div className="flex flex-wrap gap-2 justify-start">
|
||||
{synonyms.map((synonym, index) => (
|
||||
{cardData.synonyms.map((synonym, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="px-3 py-1 bg-blue-100 text-blue-700 text-sm rounded-full font-medium"
|
||||
|
|
@ -100,7 +78,7 @@ export const VocabChoiceTest: React.FC<VocabChoiceTestProps> = ({
|
|||
disabled={disabled || showResult}
|
||||
className={`p-4 text-center rounded-lg border-2 transition-all ${
|
||||
showResult
|
||||
? option === word
|
||||
? option === cardData.word
|
||||
? 'border-green-500 bg-green-50 text-green-700'
|
||||
: option === selectedAnswer
|
||||
? 'border-red-500 bg-red-50 text-red-700'
|
||||
|
|
@ -129,7 +107,7 @@ export const VocabChoiceTest: React.FC<VocabChoiceTestProps> = ({
|
|||
{!isCorrect && (
|
||||
<div className="mb-4">
|
||||
<p className="text-gray-700 text-left">
|
||||
正確答案是:<strong className="text-lg">{word}</strong>
|
||||
正確答案是:<strong className="text-lg">{cardData.word}</strong>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -137,8 +115,8 @@ export const VocabChoiceTest: React.FC<VocabChoiceTestProps> = ({
|
|||
<div className="space-y-3">
|
||||
<div className="text-left">
|
||||
<div className="flex items-center text-gray-600">
|
||||
{pronunciation && <span className="mx-2">{pronunciation}</span>}
|
||||
<AudioPlayer text={word} />
|
||||
{cardData.pronunciation && <span className="mx-2">{cardData.pronunciation}</span>}
|
||||
<AudioPlayer text={cardData.word} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue