# Drama Ling 原生前端技術架構規劃 ## 📋 架構概述 **專案名稱**: Drama Ling 語言學習應用 (Web端) **建立日期**: 2025-09-10 **技術主軸**: 原生Web技術 (HTML5 + CSS3 + Modern JavaScript) **對應後端**: .NET Core API **部署目標**: 響應式Web應用程式 ### 核心設計目標 - 🎯 **像素級精確**: 完全按照HTML原型設計實現,無框架抽象干擾 - 📱 **響應式設計**: 桌面優先,兼容平板和手機 - 🚀 **輕量高效**: 無框架負擔,快速載入和流暢互動 - 🔒 **企業級安全**: 資料保護、安全認證 - 💎 **Claude Code友好**: 適合AI輔助開發,易於理解和修改 ## 🛠️ 技術堆疊 ### 核心技術 ```javascript { "markup": "HTML5", // 語義化標記,無障礙支援 "styling": "CSS3 + SCSS", // 現代CSS特性 + 預處理器 "scripting": "ES2022+", // 現代JavaScript特性 "bundler": "Vite 5.x", // 快速開發伺服器和打包工具 "typescript": "5.x" // 可選的強型別支援 } ``` ### 開發工具鏈 ```javascript { "dev_tools": { "bundler": "Vite 5.x", // 快速HMR和打包 "css_preprocessor": "Sass/SCSS", // CSS預處理器 "linter": "ESLint 9.x", // JavaScript代碼檢查 "formatter": "Prettier 3.x", // 代碼格式化 "css_linter": "Stylelint 16.x", // CSS代碼檢查 "testing": "Vitest 1.6.x", // 快速單元測試 "e2e_testing": "Playwright" // 端到端測試 }, "build_optimization": { "minification": "Terser", // JS壓縮 "css_optimization": "cssnano", // CSS優化 "image_optimization": "vite-plugin-imagemin", "pwa_support": "@vite/plugin-pwa" } } ``` ## 🏗️ 專案結構設計 ### 整體目錄結構 ``` web-frontend/ ├── public/ │ ├── icons/ # PWA圖標和favicon │ ├── assets/ # 靜態資源 │ │ ├── audio/ # 音頻檔案 │ │ ├── images/ # 圖片資源 │ │ └── fonts/ # 字體檔案 │ └── manifest.json # PWA配置 ├── src/ │ ├── pages/ # 頁面HTML檔案 │ │ ├── home/ # 首頁相關 │ │ ├── auth/ # 認證相關頁面 │ │ ├── vocabulary/ # 詞彙學習頁面 │ │ ├── dialogue/ # 情境對話頁面 │ │ └── profile/ # 用戶相關頁面 │ ├── components/ # 可重用組件 │ │ ├── ui/ # 基礎UI組件 │ │ ├── layout/ # 佈局組件 │ │ └── business/ # 業務邏輯組件 │ ├── styles/ # 樣式檔案 │ │ ├── base/ # 基礎樣式 │ │ │ ├── reset.scss # CSS重置 │ │ │ ├── typography.scss # 字體樣式 │ │ │ └── variables.scss # SCSS變數 │ │ ├── components/ # 組件樣式 │ │ ├── pages/ # 頁面樣式 │ │ └── utilities/ # 工具類樣式 │ ├── scripts/ # JavaScript模組 │ │ ├── core/ # 核心功能 │ │ │ ├── router.js # 路由管理 │ │ │ ├── state.js # 狀態管理 │ │ │ ├── api.js # API客戶端 │ │ │ └── events.js # 事件系統 │ │ ├── modules/ # 功能模組 │ │ │ ├── auth/ # 認證模組 │ │ │ ├── vocabulary/ # 詞彙學習 │ │ │ ├── audio/ # 音頻處理 │ │ │ └── analytics/ # 數據分析 │ │ ├── utils/ # 工具函數 │ │ └── services/ # 業務服務 │ └── main.js # 應用入口 ├── tests/ # 測試檔案 ├── docs/ # 專案文檔 └── vite.config.js # Vite配置 ``` ### 模組化設計 ```javascript // modules/vocabulary/index.js export class VocabularyModule { constructor() { this.state = new VocabularyState() this.api = new VocabularyAPI() this.ui = new VocabularyUI() } init() { this.setupEventListeners() this.loadInitialData() } setupEventListeners() { // 事件監聽設置 } } // 每個模組包含完整的功能實現 // modules/vocabulary/ ├── components/ # 詞彙相關組件 ├── services/ # API服務 ├── state/ # 狀態管理 ├── utils/ # 工具函數 └── index.js # 模組入口 ``` ## 🔄 狀態管理架構 ### 簡單狀態管理模式 ```javascript // core/state.js class ApplicationState { constructor() { this.stores = { auth: new AuthState(), vocabulary: new VocabularyState(), ui: new UIState(), audio: new AudioState() } this.subscribers = new Map() } // 訂閱狀態變化 subscribe(storeName, callback) { if (!this.subscribers.has(storeName)) { this.subscribers.set(storeName, []) } this.subscribers.get(storeName).push(callback) } // 通知訂閱者 notify(storeName, data) { const callbacks = this.subscribers.get(storeName) || [] callbacks.forEach(callback => callback(data)) } // 更新狀態 updateStore(storeName, updater) { const store = this.stores[storeName] if (store) { store.update(updater) this.notify(storeName, store.getState()) } } } // 認證狀態管理 class AuthState { constructor() { this.state = { user: null, token: localStorage.getItem('auth_token'), isAuthenticated: false } this.checkAuthStatus() } update(updater) { this.state = { ...this.state, ...updater(this.state) } this.persistState() } persistState() { if (this.state.token) { localStorage.setItem('auth_token', this.state.token) } else { localStorage.removeItem('auth_token') } } getState() { return { ...this.state } } } // 全域狀態實例 export const appState = new ApplicationState() ``` ### 本地存儲策略 ```javascript // utils/storage.js class StorageManager { constructor() { this.prefix = 'dramaling_' } // localStorage封裝 setLocal(key, value) { try { localStorage.setItem( `${this.prefix}${key}`, JSON.stringify(value) ) } catch (error) { console.error('LocalStorage write failed:', error) } } getLocal(key, defaultValue = null) { try { const item = localStorage.getItem(`${this.prefix}${key}`) return item ? JSON.parse(item) : defaultValue } catch (error) { console.error('LocalStorage read failed:', error) return defaultValue } } // sessionStorage封裝 setSession(key, value) { try { sessionStorage.setItem( `${this.prefix}${key}`, JSON.stringify(value) ) } catch (error) { console.error('SessionStorage write failed:', error) } } getSession(key, defaultValue = null) { try { const item = sessionStorage.getItem(`${this.prefix}${key}`) return item ? JSON.parse(item) : defaultValue } catch (error) { console.error('SessionStorage read failed:', error) return defaultValue } } } export const storage = new StorageManager() ``` ## 🎨 CSS架構設計 ### SCSS組織結構 ```scss // styles/main.scss // 1. 工具和變數 @import 'base/variables'; @import 'base/functions'; @import 'base/mixins'; // 2. 基礎樣式 @import 'base/reset'; @import 'base/typography'; @import 'base/layout'; // 3. 組件樣式 @import 'components/buttons'; @import 'components/forms'; @import 'components/cards'; @import 'components/modals'; // 4. 頁面樣式 @import 'pages/home'; @import 'pages/vocabulary'; @import 'pages/auth'; // 5. 工具類 @import 'utilities/spacing'; @import 'utilities/colors'; @import 'utilities/responsive'; ``` ### 變數系統 ```scss // styles/base/_variables.scss // 色彩系統 $color-primary: #1976d2; $color-primary-light: lighten($color-primary, 20%); $color-primary-dark: darken($color-primary, 20%); $color-secondary: #26a69a; $color-accent: #9c27b0; $color-success: #21ba45; $color-warning: #f2c037; $color-error: #c10015; $color-info: #31ccec; // 字體系統 $font-family-primary: 'Inter', 'Noto Sans TC', sans-serif; $font-family-secondary: 'Roboto', 'Microsoft JhengHei', sans-serif; $font-size-xs: 0.75rem; // 12px $font-size-sm: 0.875rem; // 14px $font-size-base: 1rem; // 16px $font-size-lg: 1.125rem; // 18px $font-size-xl: 1.25rem; // 20px // 間距系統 $spacing-xs: 0.25rem; // 4px $spacing-sm: 0.5rem; // 8px $spacing-md: 1rem; // 16px $spacing-lg: 1.5rem; // 24px $spacing-xl: 2rem; // 32px // 響應式斷點 $breakpoint-xs: 0; $breakpoint-sm: 600px; $breakpoint-md: 1024px; $breakpoint-lg: 1440px; $breakpoint-xl: 1920px; ``` ### 響應式設計混合器 ```scss // styles/base/_mixins.scss // 響應式斷點混合器 @mixin respond-to($breakpoint) { @if $breakpoint == xs { @media (max-width: #{$breakpoint-sm - 1px}) { @content; } } @if $breakpoint == sm { @media (min-width: #{$breakpoint-sm}) and (max-width: #{$breakpoint-md - 1px}) { @content; } } @if $breakpoint == md { @media (min-width: #{$breakpoint-md}) and (max-width: #{$breakpoint-lg - 1px}) { @content; } } @if $breakpoint == lg { @media (min-width: #{$breakpoint-lg}) { @content; } } } // CSS Grid佈局工具 @mixin grid-container($columns: 1, $gap: $spacing-md) { display: grid; grid-template-columns: repeat($columns, 1fr); gap: $gap; } // 按鈕樣式基礎 @mixin button-base { display: inline-flex; align-items: center; justify-content: center; padding: $spacing-sm $spacing-md; border: none; border-radius: 4px; font-family: $font-family-primary; font-size: $font-size-base; cursor: pointer; transition: all 0.2s ease; text-decoration: none; &:disabled { opacity: 0.5; cursor: not-allowed; } } // 卡片組件樣式 @mixin card-shadow($level: 1) { @if $level == 1 { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } @if $level == 2 { box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.08); } @if $level == 3 { box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15), 0 4px 8px rgba(0, 0, 0, 0.1); } } ``` ## 🚦 路由和導航 ### 簡單路由器實現 ```javascript // core/router.js class SimpleRouter { constructor() { this.routes = new Map() this.currentRoute = null this.init() } init() { window.addEventListener('popstate', () => { this.handleRoute() }) // 處理初始路由 this.handleRoute() } // 註冊路由 addRoute(path, handler, options = {}) { this.routes.set(path, { handler, requiresAuth: options.requiresAuth || false, title: options.title || 'Drama Ling' }) } // 導航到指定路由 navigate(path, state = {}) { history.pushState(state, '', path) this.handleRoute() } // 處理路由變化 handleRoute() { const path = window.location.pathname const route = this.findRoute(path) if (route) { // 檢查認證要求 if (route.requiresAuth && !this.checkAuth()) { this.navigate('/login') return } // 設置頁面標題 document.title = route.title // 執行路由處理器 route.handler(path) this.currentRoute = path } else { this.handle404() } } // 查找匹配的路由 findRoute(path) { // 精確匹配 if (this.routes.has(path)) { return this.routes.get(path) } // 動態路由匹配 (例如: /vocabulary/:id) for (const [pattern, route] of this.routes) { const regex = this.pathToRegex(pattern) if (regex.test(path)) { return route } } return null } // 路徑轉正則表達式 pathToRegex(path) { const regexPath = path .replace(/:\w+/g, '([^/]+)') // :id -> ([^/]+) .replace(/\*/g, '.*') // * -> .* return new RegExp(`^${regexPath}$`) } // 檢查用戶認證狀態 checkAuth() { return appState.stores.auth.getState().isAuthenticated } // 處理404錯誤 handle404() { document.title = 'Page Not Found - Drama Ling' // 顯示404頁面 } } // 路由配置 export const router = new SimpleRouter() // 註冊路由 router.addRoute('/', () => import('../pages/home/index.js'), { title: 'Drama Ling - AI語言學習' }) router.addRoute('/login', () => import('../pages/auth/login.js'), { title: '登入 - Drama Ling' }) router.addRoute('/vocabulary', () => import('../pages/vocabulary/index.js'), { requiresAuth: true, title: '詞彙學習 - Drama Ling' }) router.addRoute('/vocabulary/:id', (path) => { const id = path.split('/').pop() import('../pages/vocabulary/detail.js').then(module => { module.default.init(id) }) }, { requiresAuth: true, title: '詞彙詳情 - Drama Ling' }) ``` ### 導航組件 ```javascript // components/layout/Navigation.js export class Navigation { constructor() { this.element = null this.init() } init() { this.render() this.setupEventListeners() } render() { this.element = document.createElement('nav') this.element.className = 'main-navigation' this.element.innerHTML = ` ` } setupEventListeners() { // 導航點擊處理 this.element.addEventListener('click', (e) => { const link = e.target.closest('[data-navigate]') if (link) { e.preventDefault() const path = link.getAttribute('href') router.navigate(path) } }) // 登出處理 this.element.querySelector('#logout-btn').addEventListener('click', () => { appState.updateStore('auth', (state) => ({ ...state, user: null, token: null, isAuthenticated: false })) router.navigate('/login') }) } mount(container) { container.appendChild(this.element) } } ``` ## 🔌 API服務層架構 ### HTTP客戶端 ```javascript // services/httpClient.js class HttpClient { constructor() { this.baseURL = import.meta.env.VITE_API_BASE_URL || '/api' this.timeout = 10000 } // 通用請求方法 async request(url, options = {}) { const config = { method: 'GET', headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders(), ...options.headers }, timeout: this.timeout, ...options } try { const response = await fetch(`${this.baseURL}${url}`, config) return await this.handleResponse(response) } catch (error) { throw this.handleError(error) } } // 獲取認證標頭 getAuthHeaders() { const token = appState.stores.auth.getState().token return token ? { Authorization: `Bearer ${token}` } : {} } // 處理回應 async handleResponse(response) { if (!response.ok) { if (response.status === 401) { // 處理認證過期 appState.updateStore('auth', (state) => ({ ...state, user: null, token: null, isAuthenticated: false })) router.navigate('/login') } throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const contentType = response.headers.get('content-type') if (contentType && contentType.includes('application/json')) { return await response.json() } return await response.text() } // 錯誤處理 handleError(error) { console.error('API Error:', error) return error } // 便捷方法 get(url, params = {}) { const queryString = new URLSearchParams(params).toString() const fullUrl = queryString ? `${url}?${queryString}` : url return this.request(fullUrl) } post(url, data = {}) { return this.request(url, { method: 'POST', body: JSON.stringify(data) }) } put(url, data = {}) { return this.request(url, { method: 'PUT', body: JSON.stringify(data) }) } delete(url) { return this.request(url, { method: 'DELETE' }) } } export const httpClient = new HttpClient() ``` ### API服務類別 ```javascript // services/vocabularyApi.js class VocabularyAPI { constructor() { this.basePath = '/vocabulary' } // 獲取詞彙介紹 async getWordIntroduction(wordId) { return await httpClient.get(`${this.basePath}/words/${wordId}`) } // 提交練習結果 async submitPracticeResult(result) { return await httpClient.post(`${this.basePath}/practice`, result) } // 獲取複習計劃 async getReviewSchedule() { return await httpClient.get(`${this.basePath}/review/schedule`) } // 獲取分析數據 async getAnalytics(timeRange) { return await httpClient.get(`${this.basePath}/analytics`, { timeRange }) } } export const vocabularyAPI = new VocabularyAPI() ``` ## 🎵 音頻處理整合 ### Web Audio API封裝 ```javascript // modules/audio/audioManager.js class AudioManager { constructor() { this.audioContext = null this.currentSource = null this.isPlaying = false } // 初始化音頻上下文 async initializeAudioContext() { if (!this.audioContext) { this.audioContext = new (window.AudioContext || window.webkitAudioContext)() // 處理瀏覽器自動播放政策 if (this.audioContext.state === 'suspended') { await this.audioContext.resume() } } } // 播放音頻 async playAudio(url, playbackRate = 1.0) { try { await this.initializeAudioContext() // 停止當前播放 this.stopCurrentAudio() // 載入音頻 const response = await fetch(url) const arrayBuffer = await response.arrayBuffer() const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer) // 創建音頻源 this.currentSource = this.audioContext.createBufferSource() this.currentSource.buffer = audioBuffer this.currentSource.playbackRate.value = playbackRate this.currentSource.connect(this.audioContext.destination) // 播放結束處理 this.currentSource.onended = () => { this.isPlaying = false this.currentSource = null } // 開始播放 this.currentSource.start() this.isPlaying = true return this.currentSource } catch (error) { console.error('Audio playback failed:', error) this.isPlaying = false throw error } } // 停止當前播放 stopCurrentAudio() { if (this.currentSource && this.isPlaying) { this.currentSource.stop() this.currentSource = null this.isPlaying = false } } // 錄製音頻 async startRecording() { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const mediaRecorder = new MediaRecorder(stream) const audioChunks = [] mediaRecorder.ondataavailable = (event) => { audioChunks.push(event.data) } return new Promise((resolve, reject) => { mediaRecorder.onstop = () => { const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }) resolve(audioBlob) } mediaRecorder.onerror = (event) => { reject(event.error) } mediaRecorder.start() // 返回停止函數 setTimeout(() => { mediaRecorder.stop() stream.getTracks().forEach(track => track.stop()) }, 5000) // 5秒自動停止 }) } catch (error) { console.error('Recording failed:', error) throw error } } } export const audioManager = new AudioManager() ``` ## 🧩 組件系統 ### Web Components基礎 ```javascript // components/ui/BaseComponent.js class BaseComponent extends HTMLElement { constructor() { super() this.state = {} this.listeners = new Map() } // 組件生命週期 connectedCallback() { this.render() this.setupEventListeners() this.onMounted() } disconnectedCallback() { this.cleanup() this.onUnmounted() } // 狀態管理 setState(newState) { this.state = { ...this.state, ...newState } this.render() } getState() { return { ...this.state } } // 事件處理 addEventListener(event, handler) { if (!this.listeners.has(event)) { this.listeners.set(event, []) } this.listeners.get(event).push(handler) super.addEventListener(event, handler) } // 清理資源 cleanup() { for (const [event, handlers] of this.listeners) { handlers.forEach(handler => { super.removeEventListener(event, handler) }) } this.listeners.clear() } // 子類需要實現的方法 render() { throw new Error('render method must be implemented') } onMounted() { // 組件掛載後的邏輯 } onUnmounted() { // 組件卸載前的清理 } setupEventListeners() { // 事件監聽設置 } } export { BaseComponent } ``` ### 詞彙卡片組件範例 ```javascript // components/business/VocabularyCard.js import { BaseComponent } from '../ui/BaseComponent.js' class VocabularyCard extends BaseComponent { constructor() { super() this.state = { word: null, isFlipped: false, isLoading: false } } static get observedAttributes() { return ['word-id', 'show-pronunciation', 'interactive'] } attributeChangedCallback(name, oldValue, newValue) { if (name === 'word-id' && newValue !== oldValue) { this.loadWord(newValue) } } async loadWord(wordId) { this.setState({ isLoading: true }) try { const word = await vocabularyAPI.getWordIntroduction(wordId) this.setState({ word, isLoading: false }) } catch (error) { console.error('Failed to load word:', error) this.setState({ isLoading: false }) } } render() { if (this.state.isLoading) { this.innerHTML = `
` return } if (!this.state.word) { this.innerHTML = `

無法載入詞彙資料

` return } const { word } = this.state const showPronunciation = this.getAttribute('show-pronunciation') !== 'false' const interactive = this.getAttribute('interactive') !== 'false' this.innerHTML = `

${word.word}

${showPronunciation ? `${word.pronunciation}` : ''}
${interactive ? ` ` : ''}

定義

${word.definition_zh}

例句

${word.examples.map(example => `

${example.sentence}

`).join('')}
${interactive ? ` ` : ''}
` } setupEventListeners() { this.addEventListener('click', (e) => { if (e.target.matches('.flip-btn')) { this.setState({ isFlipped: !this.state.isFlipped }) } if (e.target.matches('.audio-btn')) { const audioUrl = e.target.getAttribute('data-audio-url') audioManager.playAudio(audioUrl) } }) } } // 註冊自定義元素 customElements.define('vocabulary-card', VocabularyCard) export { VocabularyCard } ``` ## 🚀 效能優化策略 ### 載入優化 ```javascript // utils/lazyLoader.js class LazyLoader { constructor() { this.observer = new IntersectionObserver( this.handleIntersection.bind(this), { threshold: 0.1 } ) } // 延遲載入圖片 lazyLoadImages() { const images = document.querySelectorAll('img[data-src]') images.forEach(img => this.observer.observe(img)) } // 延遲載入組件 lazyLoadComponent(element, importFunction) { const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { importFunction().then(module => { module.default.init(element) }) observer.disconnect() } }) }, { threshold: 0.1 } ) observer.observe(element) } handleIntersection(entries) { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target img.src = img.getAttribute('data-src') img.removeAttribute('data-src') this.observer.unobserve(img) } }) } } export const lazyLoader = new LazyLoader() ``` ### 快取策略 ```javascript // utils/cache.js class CacheManager { constructor() { this.memoryCache = new Map() this.maxAge = 5 * 60 * 1000 // 5分鐘 } // 記憶體快取 setMemoryCache(key, data, maxAge = this.maxAge) { this.memoryCache.set(key, { data, timestamp: Date.now(), maxAge }) } getMemoryCache(key) { const cached = this.memoryCache.get(key) if (!cached) return null if (Date.now() - cached.timestamp > cached.maxAge) { this.memoryCache.delete(key) return null } return cached.data } // localStorage快取 setLocalCache(key, data, maxAge = 24 * 60 * 60 * 1000) { const cacheData = { data, timestamp: Date.now(), maxAge } storage.setLocal(`cache_${key}`, cacheData) } getLocalCache(key) { const cached = storage.getLocal(`cache_${key}`) if (!cached) return null if (Date.now() - cached.timestamp > cached.maxAge) { storage.removeLocal(`cache_${key}`) return null } return cached.data } } export const cacheManager = new CacheManager() ``` ## 🔒 安全性實作 ### XSS防護 ```javascript // utils/security.js class SecurityUtils { // HTML清理 sanitizeHTML(html) { const temp = document.createElement('div') temp.textContent = html return temp.innerHTML } // 輸入驗證 validateInput(input, type = 'text') { const patterns = { email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, phone: /^\+?[\d\s\-\(\)]+$/, username: /^[a-zA-Z0-9_]{3,20}$/, password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/ } if (patterns[type]) { return patterns[type].test(input) } // 基本長度檢查 return input.length > 0 && input.length < 1000 } // CSRF Token處理 async getCSRFToken() { try { const response = await httpClient.get('/csrf-token') return response.token } catch (error) { console.error('Failed to get CSRF token:', error) return null } } } export const security = new SecurityUtils() ``` ## 🧪 測試策略 ### Vitest配置 ```javascript // vite.config.js import { defineConfig } from 'vite' export default defineConfig({ test: { environment: 'happy-dom', globals: true, coverage: { provider: 'v8', reporter: ['text', 'json-summary', 'html'], threshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } } } } }) ``` ### 組件測試範例 ```javascript // tests/components/VocabularyCard.test.js import { describe, it, expect, beforeEach, vi } from 'vitest' import { VocabularyCard } from '../../src/components/business/VocabularyCard.js' describe('VocabularyCard', () => { let container let vocabularyCard beforeEach(() => { // 設置DOM容器 container = document.createElement('div') document.body.appendChild(container) // 創建組件實例 vocabularyCard = document.createElement('vocabulary-card') container.appendChild(vocabularyCard) }) afterEach(() => { // 清理DOM document.body.removeChild(container) }) it('should render word information correctly', () => { vocabularyCard.setAttribute('word-id', 'test-word') // 模擬API回應 vi.mock('../../src/services/vocabularyApi.js', () => ({ vocabularyAPI: { getWordIntroduction: vi.fn().mockResolvedValue({ word: 'hello', pronunciation: '/həˈloʊ/', definition_zh: '你好', examples: [ { sentence: 'Hello, world!' } ] }) } })) // 等待組件渲染 return new Promise(resolve => { setTimeout(() => { expect(vocabularyCard.querySelector('.word-text')).toHaveTextContent('hello') expect(vocabularyCard.querySelector('.pronunciation')).toHaveTextContent('/həˈloʊ/') resolve() }, 100) }) }) }) ``` ## 📦 建構和部署 ### Vite建構配置 ```javascript // vite.config.js import { defineConfig } from 'vite' import { VitePWA } from 'vite-plugin-pwa' export default defineConfig({ plugins: [ VitePWA({ registerType: 'autoUpdate', manifest: { name: 'Drama Ling', short_name: 'DramaLing', description: 'AI-powered language learning app', theme_color: '#1976d2', background_color: '#ffffff', display: 'standalone', icons: [ { src: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' } ] } }) ], build: { rollupOptions: { output: { manualChunks: { vendor: ['vite'], utils: ['./src/utils/index.js'] } } }, minify: 'terser', terserOptions: { compress: { drop_console: true, drop_debugger: true } } }, css: { preprocessorOptions: { scss: { additionalData: `@import "./src/styles/base/variables.scss";` } } } }) ``` --- **文檔狀態**: 🟢 已完成原生前端技術架構規劃 **最後更新**: 2025-09-10 **負責團隊**: 前端開發團隊 **下次檢查**: 開發開始前進行技術實施確認