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

18 KiB
Raw Blame History

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 維護負責: 前端開發團隊 下次審核: 架構變更時

📋 相關文檔: