# Drama Ling Vue.js前端技術架構規劃 ## 📋 架構概述 **專案名稱**: Drama Ling 語言學習應用 (Web端) **建立日期**: 2025-09-09 **技術主軸**: Vue.js 3 + Composition API **對應後端**: .NET Core API **部署目標**: 響應式Web應用程式 ### 核心設計目標 - 🎯 **學習體驗優化**: 支援語音互動、即時AI分析 - 📱 **響應式設計**: 桌面優先,兼容平板和手機 - 🚀 **效能優化**: 快速載入、流暢互動 - 🔒 **企業級安全**: 資料保護、安全認證 - 💎 **商業功能**: 支付整合、訂閱管理 ## 🛠️ Vue.js 技術堆疊 ### 核心框架 ```javascript { "vue": "3.4.x", // 最新穩定版,全面使用Composition API "vue-router": "4.3.x", // Vue 3 官方路由 "pinia": "2.1.x", // Vue 3 推薦狀態管理 "vite": "5.x", // 現代化建構工具 "typescript": "5.x", // 強型別支援 "vue-tsc": "latest" // Vue TypeScript支援 } ``` ### UI框架選擇 ```javascript // 推薦方案:Quasar Framework { "quasar": "2.16.x", // 企業級UI框架,支援響應式和PWA "quasar-cli": "2.4.x", // 完整開發工具鏈 // 替代方案 "element-plus": "2.7.x", // 成熟穩定,企業級組件 "vuetify": "3.6.x", // Material Design風格 "ant-design-vue": "4.2.x" // 豐富組件生態 } ``` ### 工具鏈配置 ```javascript { "dev_tools": { "eslint": "9.x", // 代碼檢查 "prettier": "3.x", // 代碼格式化 "stylelint": "16.x", // CSS檢查 "husky": "9.x", // Git hooks "lint-staged": "15.x", // 暫存檔檢查 "vitest": "1.6.x", // Vue生態測試框架 "@vue/test-utils": "2.4.x" // Vue組件測試 }, "build_tools": { "vite": "5.x", // 快速建構 "unplugin-vue-components": "0.27.x", // 自動導入組件 "unplugin-auto-import": "0.17.x", // 自動導入API "@vitejs/plugin-pwa": "0.20.x" // PWA支援 } } ``` ## 🏗️ 專案結構設計 ### 整體目錄結構 ``` web-frontend/ ├── public/ │ ├── icons/ # PWA圖標 │ ├── audio/ # 音頻素材 │ └── manifest.json # PWA配置 ├── src/ │ ├── assets/ # 靜態資源 │ │ ├── images/ │ │ ├── audio/ │ │ └── styles/ │ ├── components/ # 共用組件 │ │ ├── base/ # 基礎組件 │ │ ├── business/ # 業務組件 │ │ └── layout/ # 佈局組件 │ ├── composables/ # 組合式函數 │ │ ├── useAuth.ts │ │ ├── useAudio.ts │ │ └── useApi.ts │ ├── modules/ # 功能模組 │ │ ├── auth/ # 認證模組 │ │ ├── vocabulary/ # 詞彙學習 │ │ ├── dialogue/ # 情境對話 │ │ ├── learning-map/ # 學習地圖 │ │ └── shop/ # 道具商店 │ ├── router/ # 路由配置 │ ├── stores/ # Pinia狀態管理 │ ├── services/ # API服務層 │ ├── types/ # TypeScript類型定義 │ ├── utils/ # 工具函數 │ ├── App.vue # 根組件 │ └── main.ts # 應用入口 ├── tests/ # 測試檔案 ├── docs/ # 專案文檔 └── deployment/ # 部署配置 ``` ### 模組化設計 ```typescript // modules/vocabulary/index.ts export interface VocabularyModule { routes: RouteRecordRaw[] store: StoreDefinition components: ComponentMap services: ServiceMap } // 每個模組包含完整的功能實現 // modules/vocabulary/ ├── components/ ├── composables/ ├── services/ ├── stores/ ├── types/ ├── views/ └── router.ts ``` ## 🔄 狀態管理架構 ### Pinia Store 組織 ```typescript // stores/index.ts export interface StoreMap { auth: AuthStore user: UserStore vocabulary: VocabularyStore dialogue: DialogueStore shop: ShopStore ui: UIStore audio: AudioStore } // stores/auth.ts export const useAuthStore = defineStore('auth', () => { const user = ref(null) const token = ref(null) const isAuthenticated = computed(() => !!token.value) const login = async (credentials: LoginCredentials) => { // API調用和狀態更新 } const logout = () => { user.value = null token.value = null router.push('/login') } return { user: readonly(user), token: readonly(token), isAuthenticated, login, logout } }) ``` ### 全局狀態設計 ```typescript // stores/ui.ts - UI狀態管理 export const useUIStore = defineStore('ui', () => { const theme = ref<'light' | 'dark'>('light') const language = ref<'zh-TW' | 'en'>('zh-TW') const sidebar = ref(false) const loading = ref(false) const notifications = ref([]) return { theme, language, sidebar, loading, notifications } }) // stores/learning.ts - 學習進度狀態 export const useLearningStore = defineStore('learning', () => { const currentLevel = ref(1) const diamonds = ref(500) const lives = ref(5) const streak = ref(0) const vocabularyProgress = ref(new Map()) return { currentLevel, diamonds, lives, streak, vocabularyProgress } }) ``` ## 🎨 UI框架整合 ### Quasar框架配置 ```javascript // quasar.config.js export default configure(function (ctx) { return { framework: { config: { brand: { primary: '#1976d2', // Drama Ling 主色調 secondary: '#26a69a', accent: '#9c27b0', positive: '#21ba45', negative: '#c10015', info: '#31ccec', warning: '#f2c037' } }, plugins: [ 'Loading', 'QSpinnerGears', 'Dialog', 'Notify', 'LocalStorage', 'SessionStorage' ], components: [ 'QLayout', 'QHeader', 'QDrawer', 'QPageContainer', 'QPage', 'QToolbar', 'QToolbarTitle', 'QBtn', 'QIcon', 'QList', 'QItem', 'QItemSection', 'QItemLabel', 'QCard', 'QCardSection', 'QInput', 'QSelect', 'QCheckbox', 'QRadio', 'QToggle', 'QSlider', 'QRange', 'QBadge', 'QChip', 'QAvatar', 'QImg', 'QVideo', 'QAudio', 'QLinearProgress', 'QCircularProgress' ] } } }) ``` ### 響應式設計策略 ```scss // assets/styles/responsive.scss $breakpoint-xs: 0; $breakpoint-sm: 600px; $breakpoint-md: 1024px; $breakpoint-lg: 1440px; $breakpoint-xl: 1920px; // Vue組件中的響應式設計 .vocabulary-card { @media (max-width: 600px) { padding: 8px; font-size: 14px; } @media (min-width: 601px) and (max-width: 1023px) { padding: 12px; font-size: 16px; } @media (min-width: 1024px) { padding: 16px; font-size: 18px; } } ``` ## 🚦 路由和導航 ### Vue Router配置 ```typescript // router/index.ts import { createRouter, createWebHistory } from 'vue-router' import type { RouteRecordRaw } from 'vue-router' const routes: RouteRecordRaw[] = [ { path: '/', component: () => import('@/layouts/MainLayout.vue'), children: [ { path: '', name: 'Home', component: () => import('@/views/HomePage.vue'), meta: { requiresAuth: false } }, { path: '/vocabulary', name: 'VocabularyModule', component: () => import('@/modules/vocabulary/VocabularyLayout.vue'), children: [ { path: 'introduction/:wordId', name: 'VocabularyIntroduction', component: () => import('@/modules/vocabulary/views/IntroductionPage.vue'), props: true } ] } ] }, { path: '/auth', component: () => import('@/layouts/AuthLayout.vue'), children: [ { path: 'login', name: 'Login', component: () => import('@/modules/auth/views/LoginPage.vue') } ] } ] export const router = createRouter({ history: createWebHistory(), routes, scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } return { top: 0 } } }) // 路由守衛 router.beforeEach((to, from, next) => { const authStore = useAuthStore() if (to.meta.requiresAuth && !authStore.isAuthenticated) { next({ name: 'Login' }) } else { next() } }) ``` ### 導航組件設計 ```vue ``` ## 🔌 API服務層架構 ### HTTP客戶端配置 ```typescript // services/http.ts import axios from 'axios' import type { AxiosInstance, AxiosResponse } from 'axios' class HttpClient { private client: AxiosInstance constructor() { this.client = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 10000, headers: { 'Content-Type': 'application/json' } }) this.setupInterceptors() } private setupInterceptors() { // 請求攔截器 this.client.interceptors.request.use( (config) => { const authStore = useAuthStore() if (authStore.token) { config.headers.Authorization = `Bearer ${authStore.token}` } return config }, (error) => Promise.reject(error) ) // 響應攔截器 this.client.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { const authStore = useAuthStore() authStore.logout() } return Promise.reject(error) } ) } async get(url: string, params?: any): Promise { const response: AxiosResponse = await this.client.get(url, { params }) return response.data } async post(url: string, data?: any): Promise { const response: AxiosResponse = await this.client.post(url, data) return response.data } } export const httpClient = new HttpClient() ``` ### API服務抽象 ```typescript // services/vocabularyApi.ts export class VocabularyApiService { private basePath = '/api/vocabulary' async getWordIntroduction(wordId: string): Promise { return httpClient.get(`${this.basePath}/words/${wordId}`) } async submitPracticeResult(result: PracticeResult): Promise { return httpClient.post(`${this.basePath}/practice`, result) } async getReviewSchedule(): Promise { return httpClient.get(`${this.basePath}/review/schedule`) } } export const vocabularyApi = new VocabularyApiService() ``` ### API類型定義 ```typescript // types/api.ts export interface VocabularyWord { id: string word: string pronunciation: string definition_zh: string definition_en?: string part_of_speech: string examples: Example[] usage_context: string related_words: string[] frequency_level: number audio_url: string } export interface PracticeResult { word_id: string practice_type: 'choice' | 'matching' | 'reorganize' is_correct: boolean response_time: number user_answer: string correct_answer: string } ``` ## 🎵 音頻處理整合 ### Web Audio API整合 ```typescript // composables/useAudio.ts export const useAudio = () => { const audioContext = ref(null) const isPlaying = ref(false) const currentTrack = ref(null) const initializeAudio = () => { if (!audioContext.value) { audioContext.value = new AudioContext() } } const playAudio = async (url: string, playbackRate: number = 1.0) => { try { initializeAudio() isPlaying.value = true currentTrack.value = url const response = await fetch(url) const arrayBuffer = await response.arrayBuffer() const audioBuffer = await audioContext.value!.decodeAudioData(arrayBuffer) const source = audioContext.value!.createBufferSource() source.buffer = audioBuffer source.playbackRate.value = playbackRate source.connect(audioContext.value!.destination) source.onended = () => { isPlaying.value = false currentTrack.value = null } source.start() return source } catch (error) { console.error('Audio playback failed:', error) isPlaying.value = false } } const recordAudio = async (): Promise => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) const mediaRecorder = new MediaRecorder(stream) return mediaRecorder } catch (error) { console.error('Audio recording failed:', error) return null } } return { isPlaying: readonly(isPlaying), currentTrack: readonly(currentTrack), playAudio, recordAudio } } ``` ### 語音識別整合 ```typescript // composables/useSpeechRecognition.ts export const useSpeechRecognition = () => { const isListening = ref(false) const transcript = ref('') const confidence = ref(0) let recognition: SpeechRecognition | null = null const startListening = (language: string = 'zh-TW') => { if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) { throw new Error('Speech recognition not supported') } const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition recognition = new SpeechRecognition() recognition.continuous = false recognition.interimResults = true recognition.lang = language recognition.onstart = () => { isListening.value = true } recognition.onresult = (event) => { const result = event.results[event.results.length - 1] transcript.value = result[0].transcript confidence.value = result[0].confidence } recognition.onend = () => { isListening.value = false } recognition.start() } const stopListening = () => { if (recognition) { recognition.stop() } } return { isListening: readonly(isListening), transcript: readonly(transcript), confidence: readonly(confidence), startListening, stopListening } } ``` ## 💎 商業功能整合 ### 支付系統整合 ```typescript // services/paymentService.ts export class PaymentService { async initializeStripe() { const stripe = await loadStripe(import.meta.env.VITE_STRIPE_PUBLIC_KEY) return stripe } async createPaymentIntent(amount: number, currency: string = 'TWD') { return httpClient.post('/api/payment/create-intent', { amount, currency }) } async purchaseDiamonds(packageId: string) { const stripe = await this.initializeStripe() const { client_secret } = await this.createPaymentIntent(packageId) const result = await stripe!.confirmCardPayment(client_secret) if (result.error) { throw new Error(result.error.message) } return result.paymentIntent } } ``` ### 訂閱管理 ```typescript // composables/useSubscription.ts export const useSubscription = () => { const subscription = ref(null) const isVIP = computed(() => subscription.value?.status === 'active') const subscribeToVIP = async (planId: string) => { const paymentService = new PaymentService() const result = await paymentService.createSubscription(planId) subscription.value = result return result } const cancelSubscription = async () => { await httpClient.post('/api/subscription/cancel') await refreshSubscription() } const refreshSubscription = async () => { subscription.value = await httpClient.get('/api/subscription/current') } return { subscription: readonly(subscription), isVIP, subscribeToVIP, cancelSubscription, refreshSubscription } } ``` ## 🚀 效能優化策略 ### 代碼分割和懶載入 ```typescript // router/index.ts - 路由層面的懶載入 const routes = [ { path: '/vocabulary', component: () => import('@/modules/vocabulary/VocabularyModule.vue') }, { path: '/dialogue', component: () => import( /* webpackChunkName: "dialogue" */ '@/modules/dialogue/DialogueModule.vue' ) } ] // components - 組件層面的懶載入 export const LazyVocabularyCard = defineAsyncComponent({ loader: () => import('./VocabularyCard.vue'), loadingComponent: LoadingSpinner, errorComponent: ErrorComponent, delay: 200, timeout: 3000 }) ``` ### 狀態持久化 ```typescript // stores/persistence.ts import { createPersistencePlugin } from 'pinia-plugin-persistedstate' export const persistencePlugin = createPersistencePlugin({ key: (id) => `__dramaling_${id}__`, storage: localStorage, serializer: { serialize: JSON.stringify, deserialize: JSON.parse }, beforeRestore: (context) => { console.log('Before restore:', context.store.$id) }, afterRestore: (context) => { console.log('After restore:', context.store.$id) } }) ``` ### 快取策略 ```typescript // composables/useCache.ts export const useCache = (key: string, ttl: number = 300000) => { const cache = new Map() const get = (cacheKey: string): T | null => { const cached = cache.get(cacheKey) if (!cached) return null if (Date.now() - cached.timestamp > ttl) { cache.delete(cacheKey) return null } return cached.data } const set = (cacheKey: string, data: T): void => { cache.set(cacheKey, { data, timestamp: Date.now() }) } return { get, set } } ``` ## 🔒 安全性實作 ### XSS防護 ```typescript // utils/sanitizer.ts import DOMPurify from 'dompurify' export const sanitizeHtml = (html: string): string => { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'span'], ALLOWED_ATTR: ['class'] }) } export const sanitizeInput = (input: string): string => { return input.replace(/[<>'"&]/g, (match) => { const escapeMap: Record = { '<': '<', '>': '>', '"': '"', "'": ''', '&': '&' } return escapeMap[match] }) } ``` ### CSRF防護 ```typescript // services/csrf.ts export class CSRFService { private token: string | null = null async getCSRFToken(): Promise { if (!this.token) { const response = await httpClient.get<{token: string}>('/api/csrf-token') this.token = response.token } return this.token } async addCSRFHeader(config: any) { const token = await this.getCSRFToken() config.headers['X-CSRF-Token'] = token return config } } ``` ## 🧪 測試策略 ### 單元測試配置 ```typescript // vitest.config.ts import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], test: { environment: 'happy-dom', globals: true, coverage: { provider: 'v8', reporter: ['text', 'json-summary', 'html'], threshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } } } } }) ``` ### 組件測試範例 ```typescript // tests/components/VocabularyCard.test.ts import { mount } from '@vue/test-utils' import { describe, it, expect } from 'vitest' import VocabularyCard from '@/components/VocabularyCard.vue' describe('VocabularyCard', () => { it('renders vocabulary word correctly', () => { const wrapper = mount(VocabularyCard, { props: { word: { id: '1', word: 'hello', pronunciation: '/həˈloʊ/', definition_zh: '你好', part_of_speech: 'interjection' } } }) expect(wrapper.find('[data-test="word"]').text()).toBe('hello') expect(wrapper.find('[data-test="pronunciation"]').text()).toBe('/həˈloʊ/') expect(wrapper.find('[data-test="definition"]').text()).toBe('你好') }) it('plays audio when pronunciation button is clicked', async () => { const mockPlayAudio = vi.fn() const wrapper = mount(VocabularyCard, { props: { word: mockWord }, global: { provide: { playAudio: mockPlayAudio } } }) await wrapper.find('[data-test="play-audio"]').trigger('click') expect(mockPlayAudio).toHaveBeenCalledWith(mockWord.audio_url) }) }) ``` ## 📦 建構和部署 ### Vite建構配置 ```typescript // vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { VitePWA } from 'vite-plugin-pwa' import Components from 'unplugin-vue-components/vite' import AutoImport from 'unplugin-auto-import/vite' export default defineConfig({ plugins: [ vue(), Components({ resolvers: [ // 自動導入Quasar組件 (componentName) => { if (componentName.startsWith('Q')) return { name: componentName, from: 'quasar' } } ] }), AutoImport({ imports: [ 'vue', 'vue-router', 'pinia', { 'quasar': ['useQuasar', '$q'] } ] }), 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' } ] } }) ], resolve: { alias: { '@': path.resolve(__dirname, 'src') } }, build: { rollupOptions: { output: { manualChunks: { vendor: ['vue', 'vue-router', 'pinia'], quasar: ['quasar'], utils: ['axios', 'lodash-es'] } } } } }) ``` ### Docker部署配置 ```dockerfile # Dockerfile FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build FROM nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/nginx.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` ### Nginx配置 ```nginx # nginx.conf server { listen 80; server_name dramaling.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } gzip on; gzip_types text/plain text/css application/json application/javascript; } ``` ## 📊 開發工作流程 ### Git工作流程 ```yaml # .github/workflows/ci.yml name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - run: npm ci - run: npm run lint - run: npm run type-check - run: npm run test:unit - run: npm run build deploy: needs: test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - name: Deploy to production run: echo "Deploying to production server" ``` ### 開發環境設置 ```json // package.json scripts { "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "test:unit": "vitest", "test:e2e": "cypress run", "lint": "eslint . --ext .vue,.ts,.tsx --fix", "type-check": "vue-tsc --noEmit" } } ``` ## 📈 監控和分析 ### 性能監控 ```typescript // utils/performance.ts export class PerformanceMonitor { static trackPageLoad(routeName: string) { if (typeof window !== 'undefined' && 'performance' in window) { const loadTime = window.performance.timing.loadEventEnd - window.performance.timing.navigationStart // 發送到分析服務 this.sendMetric('page_load_time', loadTime, { route: routeName }) } } static trackUserAction(action: string, duration?: number) { this.sendMetric('user_action', duration || 0, { action }) } private static sendMetric(name: string, value: number, labels: Record) { // 發送到Google Analytics、Mixpanel等 if (window.gtag) { window.gtag('event', name, { value: value, custom_parameter: labels }) } } } ``` ## 📋 開發檢查清單 ### 代碼品質檢查 - [ ] ESLint檢查通過 - [ ] TypeScript類型檢查無錯誤 - [ ] 單元測試覆蓋率 > 80% - [ ] 組件測試完整 - [ ] E2E測試關鍵流程 ### 性能檢查 - [ ] 首屏載入時間 < 2秒 - [ ] 代碼分割策略實施 - [ ] 圖片資源優化 - [ ] API請求優化 - [ ] PWA功能測試 ### 安全檢查 - [ ] XSS防護實施 - [ ] CSRF防護配置 - [ ] 輸入驗證完整 - [ ] 敏感資料加密 - [ ] HTTPS配置 ### 響應式檢查 - [ ] 手機版佈局測試 - [ ] 平板版佈局測試 - [ ] 桌面版佈局測試 - [ ] 觸控操作優化 - [ ] 鍵盤導航支援 --- **文檔狀態**: 🟢 已完成技術架構規劃 **最後更新**: 2025-09-09 **負責團隊**: 前端開發團隊 **下次檢查**: 開發開始前進行技術選型確認