dramaling-vocab-learning/docs/04_technical/frontend-architecture.md

693 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# DramaLing 前端架構詳細說明
## 1. 技術棧概覽
### 1.1 核心技術
- **框架**: Next.js 15 (App Router)
- **語言**: TypeScript 5.x
- **樣式框架**: Tailwind CSS 3.x
- **UI 組件**: React 18.x + 自定義組件
- **狀態管理**: React useState/useEffect hooks
- **API 通信**: Fetch API + 自定義 Service 層
### 1.2 開發工具
- **套件管理**: npm
- **建置工具**: Next.js 內建 (Webpack + SWC)
- **型別檢查**: TypeScript Compiler
- **代碼格式化**: Prettier (如果配置)
- **代碼檢查**: ESLint (如果配置)
## 2. 專案結構
### 2.1 目錄架構
```
frontend/
├── app/ # Next.js App Router 頁面
│ ├── flashcards/ # 詞卡管理頁面
│ │ ├── page.tsx # 詞卡列表頁面
│ │ └── [id]/ # 動態詞卡詳細頁面
│ │ └── page.tsx
│ ├── generate/ # AI 生成詞卡頁面
│ │ └── page.tsx
│ ├── settings/ # 設定頁面
│ │ └── page.tsx
│ ├── layout.tsx # 根佈局
│ ├── page.tsx # 首頁
│ └── globals.css # 全域樣式
├── components/ # 可重用組件
│ ├── ClickableTextV2.tsx # 可點擊文字組件
│ ├── FlashcardForm.tsx # 詞卡表單組件
│ ├── Navigation.tsx # 導航組件
│ ├── ProtectedRoute.tsx # 路由保護組件
│ └── AudioPlayer.tsx # 音頻播放組件
├── lib/ # 工具函數和服務
│ ├── services/ # API 服務層
│ │ ├── flashcards.ts # 詞卡 API 服務
│ │ └── auth.ts # 認證 API 服務
│ └── utils/ # 工具函數
├── public/ # 靜態資源
│ └── images/ # 圖片資源
├── package.json # 專案配置
├── tailwind.config.js # Tailwind 配置
├── tsconfig.json # TypeScript 配置
└── next.config.js # Next.js 配置
```
## 3. 頁面架構設計
### 3.1 詞卡管理頁面 (app/flashcards/page.tsx)
#### 組件層級結構
```
FlashcardsPage (Protected Route Wrapper)
└── FlashcardsContent (Main Logic Component)
├── Navigation (Top Navigation Bar)
├── Page Header (Title + Action Buttons)
├── Tab System (All Cards / Favorites)
├── Search & Filter Section
│ ├── Main Search Input
│ ├── Advanced Filters (Collapsible)
│ └── Quick Filter Buttons
├── Flashcard List Display
│ └── Flashcard Card Components (Repeated)
└── FlashcardForm Modal (When Editing)
```
#### 狀態管理架構
```typescript
// 主要狀態
const [activeTab, setActiveTab] = useState<'all-cards' | 'favorites'>('all-cards')
const [searchTerm, setSearchTerm] = useState<string>('')
const [searchFilters, setSearchFilters] = useState<SearchFilters>({
cefrLevel: '',
partOfSpeech: '',
masteryLevel: '',
onlyFavorites: false
})
// 資料狀態
const [flashcards, setFlashcards] = useState<Flashcard[]>([])
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
// 表單狀態
const [showForm, setShowForm] = useState<boolean>(false)
const [editingCard, setEditingCard] = useState<Flashcard | null>(null)
```
### 3.2 AI 分析頁面 (app/generate/page.tsx)
#### 組件結構
```
GeneratePage (Protected Route Wrapper)
└── GenerateContent (Main Logic Component)
├── Navigation
├── Input Section (Conditional Rendering)
│ ├── Text Input Area
│ ├── Character Counter
│ └── Analysis Button
└── Analysis Results Section (Conditional Rendering)
├── Star Explanation Banner
├── Grammar Correction Panel (If Needed)
├── Main Sentence Display
│ ├── Vocabulary Statistics Cards
│ ├── ClickableTextV2 Component
│ ├── Translation Section
│ └── Idioms Display Section
└── Action Buttons
```
#### 分析結果資料流
```typescript
// AI 分析請求
handleAnalyzeSentence()
fetch('/api/ai/analyze-sentence')
setSentenceAnalysis(apiData)
setShowAnalysisView(true)
// 詞卡保存流程
handleSaveWord()
flashcardsService.createFlashcard()
alert(success/failure message)
```
## 4. 組件設計模式
### 4.1 可重用組件設計
#### ClickableTextV2 組件
```typescript
interface ClickableTextProps {
text: string; // 顯示文字
analysis?: Record<string, WordAnalysis>; // 詞彙分析資料
onWordClick?: (word: string, analysis: WordAnalysis) => void;
onSaveWord?: (word: string, analysis: WordAnalysis) => Promise<{success: boolean}>;
remainingUsage?: number; // 剩餘使用次數
showIdiomsInline?: boolean; // 是否內嵌顯示慣用語
}
// 設計特色:
// - 詞彙點擊互動
// - CEFR 等級顏色編碼
// - 星星標記 (高頻詞彙)
// - 彈出式詞彙詳情
// - Portal 渲染優化
```
#### FlashcardForm 組件
```typescript
interface FlashcardFormProps {
initialData?: Partial<Flashcard>; // 編輯時的初始資料
isEdit?: boolean; // 是否為編輯模式
onSuccess: () => void; // 成功回調
onCancel: () => void; // 取消回調
}
// 表單欄位:
// - word (必填)
// - translation (必填)
// - definition (必填)
// - pronunciation (選填)
// - partOfSpeech (選填,下拉選單)
// - example (必填)
// - exampleTranslation (選填)
```
### 4.2 路由保護模式
#### ProtectedRoute 組件
```typescript
// 用途:保護需要認證的頁面
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
// 檢查認證狀態
// 未登入時導向登入頁面
// 已登入時顯示子組件
return (
<div>
{/* 認證檢查邏輯 */}
{children}
</div>
)
}
// 使用方式:
<ProtectedRoute>
<FlashcardsContent />
</ProtectedRoute>
```
## 5. API 服務層設計
### 5.1 服務類別架構
#### FlashcardsService 類別
```typescript
class FlashcardsService {
private readonly baseURL = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5008'}/api`;
// 統一的請求處理方法
private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T>
// CRUD 操作方法
async getFlashcards(search?: string, favoritesOnly?: boolean): Promise<ApiResponse<{flashcards: Flashcard[], count: number}>>
async getFlashcard(id: string): Promise<ApiResponse<Flashcard>>
async createFlashcard(data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>>
async updateFlashcard(id: string, data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>>
async deleteFlashcard(id: string): Promise<ApiResponse<void>>
async toggleFavorite(id: string): Promise<ApiResponse<void>>
}
// 單例模式匯出
export const flashcardsService = new FlashcardsService();
```
### 5.2 型別定義標準化
#### 核心型別定義
```typescript
// 詞卡介面定義
export interface Flashcard {
id: string;
word: string;
translation: string;
definition: string;
partOfSpeech: string;
pronunciation: string;
example: string;
exampleTranslation?: string;
masteryLevel: number;
timesReviewed: number;
isFavorite: boolean;
nextReviewDate: string;
difficultyLevel: string;
createdAt: string;
updatedAt?: string;
}
// API 回應格式
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
// 創建請求格式
export interface CreateFlashcardRequest {
word: string;
translation: string;
definition: string;
pronunciation: string;
partOfSpeech: string;
example: string;
exampleTranslation?: string;
}
```
## 6. 樣式系統架構
### 6.1 Tailwind CSS 配置
#### 主要設計系統
```javascript
// tailwind.config.js
module.exports = {
content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#3B82F6', // 主色調藍色
hover: '#2563EB' // 懸停狀態
}
}
}
}
}
```
#### CEFR 等級顏色系統
```typescript
const getCEFRColor = (level: string) => {
switch (level) {
case 'A1': return 'bg-green-100 text-green-700 border-green-200'
case 'A2': return 'bg-blue-100 text-blue-700 border-blue-200'
case 'B1': return 'bg-yellow-100 text-yellow-700 border-yellow-200'
case 'B2': return 'bg-orange-100 text-orange-700 border-orange-200'
case 'C1': return 'bg-red-100 text-red-700 border-red-200'
case 'C2': return 'bg-purple-100 text-purple-700 border-purple-200'
default: return 'bg-gray-100 text-gray-700 border-gray-200'
}
}
```
### 6.2 響應式設計模式
#### 斷點策略
```css
/* 手機版 */
@media (max-width: 767px) {
.flashcard-grid { grid-template-columns: 1fr; }
.search-filters { flex-direction: column; }
}
/* 平板版 */
@media (min-width: 768px) and (max-width: 1023px) {
.flashcard-grid { grid-template-columns: repeat(2, 1fr); }
}
/* 桌面版 */
@media (min-width: 1024px) {
.flashcard-grid { grid-template-columns: repeat(3, 1fr); }
}
```
## 7. 效能優化策略
### 7.1 React 效能優化
#### useMemo 和 useCallback 使用
```typescript
// 快取詞彙統計計算
const vocabularyStats = useMemo(() => {
if (!sentenceAnalysis?.vocabularyAnalysis) return defaultStats;
// 複雜計算邏輯
return calculateStats(sentenceAnalysis.vocabularyAnalysis);
}, [sentenceAnalysis])
// 快取事件處理函數
const handleWordClick = useCallback(async (word: string, event: React.MouseEvent) => {
// 事件處理邏輯
}, [findWordAnalysis, onWordClick, calculatePopupPosition])
```
#### 組件懶載入
```typescript
// 動態導入大型組件
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <div>載入中...</div>,
ssr: false
})
```
### 7.2 資料載入優化
#### 條件式資料載入
```typescript
useEffect(() => {
// 只在需要時載入資料
if (activeTab === 'favorites') {
loadFavoriteFlashcards();
} else {
loadAllFlashcards();
}
}, [activeTab])
```
#### 錯誤邊界處理
```typescript
// API 服務層錯誤處理
private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
try {
const response = await fetch(`${this.baseURL}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Network error' }));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
return response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
```
## 8. 狀態管理模式
### 8.1 本地狀態管理
#### 頁面級狀態
```typescript
// 每個頁面管理自己的狀態
function FlashcardsContent() {
// UI 狀態
const [activeTab, setActiveTab] = useState('all-cards')
const [showForm, setShowForm] = useState(false)
// 資料狀態
const [flashcards, setFlashcards] = useState<Flashcard[]>([])
const [loading, setLoading] = useState(true)
// 搜尋狀態
const [searchTerm, setSearchTerm] = useState('')
const [searchFilters, setSearchFilters] = useState(defaultFilters)
}
```
#### 跨組件狀態同步
```typescript
// 通過 props 和 callback 實現父子組件通信
<FlashcardForm
initialData={editingCard}
isEdit={!!editingCard}
onSuccess={handleFormSuccess} // 成功後重新載入資料
onCancel={() => setShowForm(false)}
/>
```
### 8.2 持久化狀態
#### localStorage 使用
```typescript
// 用戶偏好設定
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
// 搜尋歷史 (未來功能)
const searchHistory = JSON.parse(localStorage.getItem('searchHistory') || '[]')
```
## 9. 用戶體驗設計
### 9.1 載入狀態處理
#### 統一載入狀態
```typescript
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-lg">載入中...</div>
</div>
)
}
```
#### 按鈕載入狀態
```typescript
<button
disabled={loading}
className="disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? '載入中...' : '提交'}
</button>
```
### 9.2 錯誤狀態處理
#### 頁面級錯誤
```typescript
if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-red-600">{error}</div>
</div>
)
}
```
#### 表單錯誤
```typescript
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg">
{error}
</div>
)}
```
### 9.3 空狀態設計
#### 友善的空狀態
```typescript
{filteredCards.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-500 mb-4">沒有找到詞卡</p>
<Link href="/generate" className="bg-primary text-white px-4 py-2 rounded-lg">
創建新詞卡
</Link>
</div>
) : (
// 詞卡列表
)}
```
## 10. 互動設計模式
### 10.1 搜尋互動
#### 即時搜尋
```typescript
// 輸入時即時過濾,無防抖延遲
const filteredCards = allCards.filter(card => {
if (searchTerm) {
const searchLower = searchTerm.toLowerCase()
return card.word?.toLowerCase().includes(searchLower) ||
card.translation?.toLowerCase().includes(searchLower) ||
card.definition?.toLowerCase().includes(searchLower)
}
return true
})
```
#### 搜尋結果高亮
```typescript
const highlightSearchTerm = (text: string, searchTerm: string) => {
if (!searchTerm || !text) return text
const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
const parts = text.split(regex)
return parts.map((part, index) =>
regex.test(part) ? (
<mark key={index} className="bg-yellow-200 text-yellow-900 px-1 rounded">
{part}
</mark>
) : part
)
}
```
### 10.2 彈窗互動設計
#### Portal 渲染模式
```typescript
import { createPortal } from 'react-dom'
const VocabPopup = () => {
if (!selectedWord || !mounted) return null
return createPortal(
<>
{/* 遮罩層 */}
<div className="fixed inset-0 bg-black bg-opacity-50 z-40" onClick={closePopup} />
{/* 彈窗內容 */}
<div className="fixed z-50 bg-white rounded-xl shadow-lg" style={{...}}>
{/* 詞彙詳細資訊 */}
</div>
</>,
document.body
)
}
```
### 10.3 鍵盤操作支援
#### ESC 鍵清除搜尋
```typescript
<input
onKeyDown={(e) => {
if (e.key === 'Escape') {
setSearchTerm('')
}
}}
/>
```
## 11. 開發與建置
### 11.1 開發環境
#### 開發伺服器啟動
```bash
cd frontend
npm run dev
# 開發伺服器運行於: http://localhost:3000
# Hot Reload 啟用
# Fast Refresh 啟用
```
#### 環境變數配置
```bash
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:5008
```
### 11.2 建置優化
#### Next.js 配置 (next.config.js)
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true, // 啟用 App Router
},
images: {
domains: ['localhost'], // 圖片域名白名單
}
}
module.exports = nextConfig
```
#### TypeScript 配置 (tsconfig.json)
```json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"] // 路徑別名
}
}
}
```
## 12. 測試策略
### 12.1 組件測試
```typescript
// 未來實作Jest + React Testing Library
import { render, screen, fireEvent } from '@testing-library/react'
import { FlashcardForm } from './FlashcardForm'
test('should submit form with valid data', async () => {
const onSuccess = jest.fn()
render(<FlashcardForm onSuccess={onSuccess} onCancel={() => {}} />)
// 填寫表單
fireEvent.change(screen.getByLabelText('單字'), { target: { value: 'test' } })
// 提交表單
fireEvent.click(screen.getByText('創建詞卡'))
// 驗證結果
expect(onSuccess).toHaveBeenCalled()
})
```
### 12.2 API 服務測試
```typescript
// Mock fetch 進行單元測試
global.fetch = jest.fn()
test('should create flashcard successfully', async () => {
const mockResponse = { success: true, data: mockFlashcard }
;(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
})
const result = await flashcardsService.createFlashcard(mockData)
expect(result.success).toBe(true)
})
```
---
**文檔版本**: v1.0
**建立日期**: 2025-09-24
**維護負責**: 前端開發團隊
**下次審核**: 架構變更時
> 📋 相關文檔:
> - [系統架構總覽](./system-architecture.md)
> - [後端架構詳細說明](./backend-architecture.md)
> - [詞卡 API 規格](./flashcard-api-specification.md)