18 KiB
18 KiB
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)
狀態管理架構
// 主要狀態
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
分析結果資料流
// AI 分析請求
handleAnalyzeSentence()
→ fetch('/api/ai/analyze-sentence')
→ setSentenceAnalysis(apiData)
→ setShowAnalysisView(true)
// 詞卡保存流程
handleSaveWord()
→ flashcardsService.createFlashcard()
→ alert(success/failure message)
4. 組件設計模式
4.1 可重用組件設計
ClickableTextV2 組件
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 組件
interface FlashcardFormProps {
initialData?: Partial<Flashcard>; // 編輯時的初始資料
isEdit?: boolean; // 是否為編輯模式
onSuccess: () => void; // 成功回調
onCancel: () => void; // 取消回調
}
// 表單欄位:
// - word (必填)
// - translation (必填)
// - definition (必填)
// - pronunciation (選填)
// - partOfSpeech (選填,下拉選單)
// - example (必填)
// - exampleTranslation (選填)
4.2 路由保護模式
ProtectedRoute 組件
// 用途:保護需要認證的頁面
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
// 檢查認證狀態
// 未登入時導向登入頁面
// 已登入時顯示子組件
return (
<div>
{/* 認證檢查邏輯 */}
{children}
</div>
)
}
// 使用方式:
<ProtectedRoute>
<FlashcardsContent />
</ProtectedRoute>
5. API 服務層設計
5.1 服務類別架構
FlashcardsService 類別
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 型別定義標準化
核心型別定義
// 詞卡介面定義
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 配置
主要設計系統
// 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 等級顏色系統
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 響應式設計模式
斷點策略
/* 手機版 */
@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 使用
// 快取詞彙統計計算
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])
組件懶載入
// 動態導入大型組件
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <div>載入中...</div>,
ssr: false
})
7.2 資料載入優化
條件式資料載入
useEffect(() => {
// 只在需要時載入資料
if (activeTab === 'favorites') {
loadFavoriteFlashcards();
} else {
loadAllFlashcards();
}
}, [activeTab])
錯誤邊界處理
// 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 本地狀態管理
頁面級狀態
// 每個頁面管理自己的狀態
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)
}
跨組件狀態同步
// 通過 props 和 callback 實現父子組件通信
<FlashcardForm
initialData={editingCard}
isEdit={!!editingCard}
onSuccess={handleFormSuccess} // 成功後重新載入資料
onCancel={() => setShowForm(false)}
/>
8.2 持久化狀態
localStorage 使用
// 用戶偏好設定
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
// 搜尋歷史 (未來功能)
const searchHistory = JSON.parse(localStorage.getItem('searchHistory') || '[]')
9. 用戶體驗設計
9.1 載入狀態處理
統一載入狀態
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-lg">載入中...</div>
</div>
)
}
按鈕載入狀態
<button
disabled={loading}
className="disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? '載入中...' : '提交'}
</button>
9.2 錯誤狀態處理
頁面級錯誤
if (error) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-red-600">{error}</div>
</div>
)
}
表單錯誤
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg">
{error}
</div>
)}
9.3 空狀態設計
友善的空狀態
{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 搜尋互動
即時搜尋
// 輸入時即時過濾,無防抖延遲
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
})
搜尋結果高亮
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 渲染模式
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 鍵清除搜尋
<input
onKeyDown={(e) => {
if (e.key === 'Escape') {
setSearchTerm('')
}
}}
/>
11. 開發與建置
11.1 開發環境
開發伺服器啟動
cd frontend
npm run dev
# 開發伺服器運行於: http://localhost:3000
# Hot Reload 啟用
# Fast Refresh 啟用
環境變數配置
# .env.local
NEXT_PUBLIC_API_URL=http://localhost:5008
11.2 建置優化
Next.js 配置 (next.config.js)
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true, // 啟用 App Router
},
images: {
domains: ['localhost'], // 圖片域名白名單
}
}
module.exports = nextConfig
TypeScript 配置 (tsconfig.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 組件測試
// 未來實作: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 服務測試
// 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 維護負責: 前端開發團隊 下次審核: 架構變更時
📋 相關文檔: