# Vue.js 項目結構和配置範例 ## 📁 完整項目結構 ``` dramaling-web/ ├── .env.development # 開發環境變數 ├── .env.production # 生產環境變數 ├── .eslintrc.js # ESLint配置 ├── .gitignore # Git忽略檔案 ├── .prettierrc # Prettier配置 ├── index.html # HTML入口 ├── package.json # 專案依賴和腳本 ├── README.md # 專案說明 ├── tsconfig.json # TypeScript配置 ├── vite.config.ts # Vite配置 ├── vitest.config.ts # Vitest測試配置 ├── cypress.config.ts # E2E測試配置 ├── docker-compose.yml # Docker開發環境 ├── Dockerfile # Docker部署 ├── nginx.conf # Nginx配置 │ ├── public/ # 公開靜態資源 │ ├── favicon.ico │ ├── manifest.json # PWA配置 │ ├── robots.txt │ └── icons/ # PWA圖標 │ ├── icon-192x192.png │ └── icon-512x512.png │ ├── src/ # 源代碼 │ ├── App.vue # 根組件 │ ├── main.ts # 應用入口 │ │ │ ├── assets/ # 資源檔案 │ │ ├── images/ # 圖片資源 │ │ │ ├── logo.svg │ │ │ └── backgrounds/ │ │ ├── audio/ # 音頻檔案 │ │ │ └── pronunciation/ │ │ └── styles/ # 全域樣式 │ │ ├── main.scss │ │ ├── variables.scss │ │ └── components.scss │ │ │ ├── components/ # 共用組件 │ │ ├── base/ # 基礎組件 │ │ │ ├── BaseButton.vue │ │ │ ├── BaseCard.vue │ │ │ ├── BaseInput.vue │ │ │ └── BaseModal.vue │ │ ├── business/ # 業務組件 │ │ │ ├── VocabularyCard.vue │ │ │ ├── DialogueMessage.vue │ │ │ ├── ProgressBar.vue │ │ │ └── AudioPlayer.vue │ │ └── layout/ # 佈局組件 │ │ ├── AppHeader.vue │ │ ├── AppFooter.vue │ │ ├── NavigationDrawer.vue │ │ └── Breadcrumb.vue │ │ │ ├── composables/ # 組合式函數 │ │ ├── useAuth.ts # 認證相關 │ │ ├── useAudio.ts # 音頻處理 │ │ ├── useApi.ts # API調用 │ │ ├── useCache.ts # 快取管理 │ │ ├── useNotification.ts # 通知系統 │ │ └── useLocalStorage.ts # 本地存儲 │ │ │ ├── layouts/ # 佈局模板 │ │ ├── MainLayout.vue # 主要佈局 │ │ ├── AuthLayout.vue # 認證佈局 │ │ ├── EmptyLayout.vue # 空白佈局 │ │ └── AdminLayout.vue # 管理佈局 │ │ │ ├── modules/ # 功能模組 │ │ ├── auth/ # 認證模組 │ │ │ ├── components/ │ │ │ │ ├── LoginForm.vue │ │ │ │ ├── RegisterForm.vue │ │ │ │ └── PasswordResetForm.vue │ │ │ ├── composables/ │ │ │ │ └── useAuthValidation.ts │ │ │ ├── services/ │ │ │ │ └── authService.ts │ │ │ ├── stores/ │ │ │ │ └── authStore.ts │ │ │ ├── types/ │ │ │ │ └── auth.types.ts │ │ │ ├── views/ │ │ │ │ ├── LoginPage.vue │ │ │ │ ├── RegisterPage.vue │ │ │ │ └── ProfilePage.vue │ │ │ └── router.ts │ │ │ │ │ ├── vocabulary/ # 詞彙學習模組 │ │ │ ├── components/ │ │ │ │ ├── WordCard.vue │ │ │ │ ├── PracticeCard.vue │ │ │ │ ├── ResultsPanel.vue │ │ │ │ └── ProgressTracker.vue │ │ │ ├── composables/ │ │ │ │ ├── useVocabulary.ts │ │ │ │ └── usePractice.ts │ │ │ ├── services/ │ │ │ │ └── vocabularyService.ts │ │ │ ├── stores/ │ │ │ │ └── vocabularyStore.ts │ │ │ ├── types/ │ │ │ │ └── vocabulary.types.ts │ │ │ ├── views/ │ │ │ │ ├── IntroductionPage.vue │ │ │ │ ├── PracticePage.vue │ │ │ │ ├── ResultsPage.vue │ │ │ │ └── ReviewPage.vue │ │ │ └── router.ts │ │ │ │ │ ├── dialogue/ # 情境對話模組 │ │ │ ├── components/ │ │ │ │ ├── ChatInterface.vue │ │ │ │ ├── MessageBubble.vue │ │ │ │ ├── VoiceRecorder.vue │ │ │ │ └── ScenarioSelector.vue │ │ │ ├── composables/ │ │ │ │ ├── useDialogue.ts │ │ │ │ └── useSpeechRecognition.ts │ │ │ ├── services/ │ │ │ │ └── dialogueService.ts │ │ │ ├── stores/ │ │ │ │ └── dialogueStore.ts │ │ │ ├── types/ │ │ │ │ └── dialogue.types.ts │ │ │ ├── views/ │ │ │ │ ├── DialogueMainPage.vue │ │ │ │ ├── ScenarioPage.vue │ │ │ │ └── TimedChallengePage.vue │ │ │ └── router.ts │ │ │ │ │ ├── learning-map/ # 學習地圖模組 │ │ │ └── ... (類似結構) │ │ │ │ │ └── shop/ # 商店模組 │ │ └── ... (類似結構) │ │ │ ├── router/ # 路由配置 │ │ ├── index.ts # 主路由 │ │ ├── guards.ts # 路由守衛 │ │ └── types.ts # 路由類型 │ │ │ ├── stores/ # Pinia狀態管理 │ │ ├── index.ts # Store註冊 │ │ ├── auth.ts # 認證Store │ │ ├── user.ts # 用戶Store │ │ ├── ui.ts # UI狀態Store │ │ ├── learning.ts # 學習進度Store │ │ └── notification.ts # 通知Store │ │ │ ├── services/ # 服務層 │ │ ├── api/ # API服務 │ │ │ ├── index.ts # API客戶端 │ │ │ ├── auth.api.ts │ │ │ ├── vocabulary.api.ts │ │ │ ├── dialogue.api.ts │ │ │ └── payment.api.ts │ │ ├── storage/ # 存儲服務 │ │ │ ├── localStorage.ts │ │ │ └── sessionStorage.ts │ │ ├── notification/ # 通知服務 │ │ │ └── pushNotification.ts │ │ └── analytics/ # 分析服務 │ │ └── tracking.ts │ │ │ ├── types/ # 全域類型定義 │ │ ├── index.ts # 導出所有類型 │ │ ├── api.types.ts # API相關類型 │ │ ├── user.types.ts # 用戶相關類型 │ │ ├── learning.types.ts # 學習相關類型 │ │ └── common.types.ts # 通用類型 │ │ │ ├── utils/ # 工具函數 │ │ ├── index.ts # 導出所有工具 │ │ ├── format.ts # 格式化工具 │ │ ├── validation.ts # 驗證工具 │ │ ├── storage.ts # 存儲工具 │ │ ├── audio.ts # 音頻工具 │ │ ├── date.ts # 日期工具 │ │ └── constants.ts # 常數定義 │ │ │ └── plugins/ # Vue插件 │ ├── index.ts # 插件註冊 │ ├── quasar.ts # Quasar配置 │ ├── pwa.ts # PWA配置 │ └── i18n.ts # 國際化配置 │ ├── tests/ # 測試檔案 │ ├── unit/ # 單元測試 │ │ ├── components/ │ │ ├── composables/ │ │ ├── stores/ │ │ └── utils/ │ ├── integration/ # 整合測試 │ │ └── api/ │ ├── e2e/ # E2E測試 │ │ ├── specs/ │ │ ├── fixtures/ │ │ └── support/ │ └── __mocks__/ # Mock檔案 │ ├── docs/ # 專案文檔 │ ├── api.md # API文檔 │ ├── components.md # 組件文檔 │ ├── deployment.md # 部署指南 │ └── development.md # 開發指南 │ └── scripts/ # 建構腳本 ├── build.sh # 建構腳本 ├── deploy.sh # 部署腳本 └── test.sh # 測試腳本 ``` ## 📋 核心配置檔案 ### package.json ```json { "name": "dramaling-web", "version": "1.0.0", "type": "module", "scripts": { "dev": "vite --host", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview", "test": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest --coverage", "test:e2e": "cypress run", "test:e2e:dev": "cypress open", "lint": "eslint . --ext .vue,.ts,.tsx --fix", "lint:style": "stylelint **/*.{css,scss,vue} --fix", "type-check": "vue-tsc --noEmit", "prepare": "husky install" }, "dependencies": { "vue": "^3.4.21", "vue-router": "^4.3.0", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "quasar": "^2.16.0", "@quasar/extras": "^1.16.4", "axios": "^1.6.8", "vee-validate": "^4.12.6", "yup": "^1.4.0", "lodash-es": "^4.17.21", "dayjs": "^1.11.10", "dompurify": "^3.0.11", "@vueuse/core": "^10.9.0", "workbox-window": "^7.0.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.4", "vite": "^5.2.0", "vue-tsc": "^2.0.6", "typescript": "^5.4.0", "@types/node": "^20.12.7", "@types/lodash-es": "^4.17.12", "@types/dompurify": "^3.0.5", "vitest": "^1.5.0", "@vue/test-utils": "^2.4.5", "happy-dom": "^14.7.1", "@vitest/coverage-v8": "^1.5.0", "@vitest/ui": "^1.5.0", "cypress": "^13.7.2", "eslint": "^9.1.1", "@vue/eslint-config-typescript": "^13.0.0", "@vue/eslint-config-prettier": "^9.0.0", "prettier": "^3.2.5", "stylelint": "^16.4.0", "stylelint-config-standard-scss": "^13.1.0", "stylelint-config-standard-vue": "^1.0.0", "husky": "^9.0.11", "lint-staged": "^15.2.2", "unplugin-vue-components": "^0.27.0", "unplugin-auto-import": "^0.17.5", "@vite/plugin-pwa": "^0.20.0" } } ``` ### vite.config.ts ```typescript 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' import { quasar, transformAssetUrls } from '@quasar/vite-plugin' import path from 'path' export default defineConfig({ plugins: [ vue({ template: { transformAssetUrls } }), quasar({ sassVariables: 'src/assets/styles/quasar-variables.sass' }), Components({ resolvers: [ (componentName) => { if (componentName.startsWith('Q')) return { name: componentName, from: 'quasar' } } ], dts: true, dirs: ['src/components'], extensions: ['vue'], deep: true }), AutoImport({ imports: [ 'vue', 'vue-router', 'pinia', { 'quasar': ['useQuasar', '$q', 'Notify', 'Loading', 'Dialog'], '@vueuse/core': ['useLocalStorage', 'useSessionStorage', 'useFetch'] } ], dts: true, dirs: ['src/composables', 'src/stores'], vueTemplate: true }), VitePWA({ registerType: 'autoUpdate', workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'], runtimeCaching: [ { urlPattern: /^https:\/\/api\.dramaling\.com\/.*/i, handler: 'NetworkFirst', options: { cacheName: 'api-cache', expiration: { maxEntries: 50, maxAgeSeconds: 5 * 60 // 5分鐘 } } } ] }, manifest: { name: 'Drama Ling - AI語言學習', short_name: 'Drama Ling', description: 'AI驅動的情境式語言學習應用', theme_color: '#1976d2', background_color: '#ffffff', display: 'standalone', orientation: 'portrait-primary', scope: '/', start_url: '/', icons: [ { src: '/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' }, { src: '/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' } ] } }) ], resolve: { alias: { '@': path.resolve(__dirname, 'src'), '~': path.resolve(__dirname, 'src'), '@components': path.resolve(__dirname, 'src/components'), '@modules': path.resolve(__dirname, 'src/modules'), '@stores': path.resolve(__dirname, 'src/stores'), '@services': path.resolve(__dirname, 'src/services'), '@utils': path.resolve(__dirname, 'src/utils'), '@assets': path.resolve(__dirname, 'src/assets') } }, css: { preprocessorOptions: { scss: { additionalData: `@import "@/assets/styles/variables.scss";` } } }, build: { target: 'es2020', rollupOptions: { output: { manualChunks: { // 框架核心 'vue-vendor': ['vue', 'vue-router', 'pinia'], // UI框架 'quasar-vendor': ['quasar'], // 工具庫 'utils-vendor': ['axios', 'lodash-es', 'dayjs', '@vueuse/core'], // 驗證相關 'validation-vendor': ['vee-validate', 'yup'], // 各模組 'auth-module': ['src/modules/auth'], 'vocabulary-module': ['src/modules/vocabulary'], 'dialogue-module': ['src/modules/dialogue'] } } } }, server: { host: '0.0.0.0', port: 3000, proxy: { '/api': { target: 'http://localhost:5000', changeOrigin: true } } } }) ``` ### tsconfig.json ```json { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, /* 模組解析選項 */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", /* 嚴格性檢查選項 */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, /* 路徑對應 */ "baseUrl": ".", "paths": { "@/*": ["src/*"], "~/*": ["src/*"], "@components/*": ["src/components/*"], "@modules/*": ["src/modules/*"], "@stores/*": ["src/stores/*"], "@services/*": ["src/services/*"], "@utils/*": ["src/utils/*"], "@assets/*": ["src/assets/*"] }, /* Vue 相關 */ "types": ["node", "vue/ref-macros"], "allowJs": true }, "include": [ "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts" ], "exclude": [ "node_modules", "dist" ] } ``` ### .eslintrc.js ```javascript module.exports = { root: true, env: { node: true, browser: true, es2021: true }, extends: [ 'plugin:vue/vue3-essential', 'eslint:recommended', '@vue/typescript/recommended', '@vue/prettier', '@vue/prettier/@typescript-eslint' ], parser: 'vue-eslint-parser', parserOptions: { parser: '@typescript-eslint/parser', ecmaVersion: 2021, sourceType: 'module' }, plugins: ['@typescript-eslint'], rules: { // Vue規則 'vue/multi-word-component-names': 'off', 'vue/no-unused-vars': 'error', 'vue/component-definition-name-casing': ['error', 'PascalCase'], 'vue/component-name-in-template-casing': ['error', 'PascalCase'], // TypeScript規則 '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], '@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', // 一般規則 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'prefer-const': 'error', 'no-var': 'error', 'object-shorthand': 'error', 'prefer-template': 'error' }, globals: { defineProps: 'readonly', defineEmits: 'readonly', defineExpose: 'readonly', withDefaults: 'readonly' }, overrides: [ { files: ['**/*.test.{js,ts}', '**/tests/**/*'], env: { jest: true, vitest: true } } ] } ``` ### .env.development ```env # API配置 VITE_API_BASE_URL=http://localhost:5000/api VITE_WS_BASE_URL=ws://localhost:5000/ws # 第三方服務 VITE_OPENAI_API_KEY=your_openai_key_here VITE_STRIPE_PUBLIC_KEY=pk_test_your_stripe_key # 分析服務 VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX VITE_MIXPANEL_TOKEN=your_mixpanel_token # 功能開關 VITE_ENABLE_PWA=true VITE_ENABLE_DEV_TOOLS=true VITE_ENABLE_MOCK_API=false # 除錯設定 VITE_DEBUG_MODE=true VITE_LOG_LEVEL=debug ``` ### .env.production ```env # API配置 VITE_API_BASE_URL=https://api.dramaling.com/api VITE_WS_BASE_URL=wss://api.dramaling.com/ws # 第三方服務 VITE_STRIPE_PUBLIC_KEY=pk_live_your_stripe_key # 分析服務 VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX VITE_MIXPANEL_TOKEN=your_mixpanel_token # 功能開關 VITE_ENABLE_PWA=true VITE_ENABLE_DEV_TOOLS=false VITE_ENABLE_MOCK_API=false # 除錯設定 VITE_DEBUG_MODE=false VITE_LOG_LEVEL=error ``` ### vitest.config.ts ```typescript import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import path from 'path' export default defineConfig({ plugins: [vue()], test: { globals: true, environment: 'happy-dom', setupFiles: ['tests/setup.ts'], coverage: { provider: 'v8', reporter: ['text', 'json-summary', 'html'], exclude: [ 'node_modules/', 'tests/', '**/*.d.ts', '**/*.config.*', 'src/main.ts' ], threshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80 } } } }, resolve: { alias: { '@': path.resolve(__dirname, 'src'), '~': path.resolve(__dirname, 'src') } } }) ``` ### docker-compose.yml ```yaml version: '3.8' services: # 開發環境前端服務 web: build: context: . dockerfile: Dockerfile.dev ports: - "3000:3000" volumes: - .:/app - /app/node_modules environment: - NODE_ENV=development - VITE_API_BASE_URL=http://api:5000/api depends_on: - api networks: - dramaling_network # Mock API服務 (開發時使用) api: image: mockserver/mockserver:latest ports: - "5000:1080" environment: MOCKSERVER_PROPERTY_FILE: /config/mockserver.properties volumes: - ./mock-api:/config networks: - dramaling_network # Redis (用於開發環境快取) redis: image: redis:7-alpine ports: - "6379:6379" networks: - dramaling_network networks: dramaling_network: driver: bridge ``` ### 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 AS production # 複製建構結果 COPY --from=builder /app/dist /usr/share/nginx/html # 複製Nginx配置 COPY nginx.conf /etc/nginx/conf.d/default.conf # 健康檢查 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` ### nginx.conf ```nginx server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; # 安全頭 add_header X-Frame-Options DENY; add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block"; add_header Referrer-Policy "strict-origin-when-cross-origin"; # 靜態資源快取 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; } # API代理 location /api/ { proxy_pass http://api-server:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # SPA路由支援 location / { try_files $uri $uri/ /index.html; } # Gzip壓縮 gzip on; gzip_vary on; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; } ``` ## 📋 專案初始化腳本 ### setup.sh ```bash #!/bin/bash echo "🚀 初始化 Drama Ling Vue.js 專案..." # 創建專案目錄 mkdir -p dramaling-web cd dramaling-web # 初始化 package.json npm init -y # 安裝依賴 echo "📦 安裝依賴..." npm install vue@^3.4.21 vue-router@^4.3.0 pinia@^2.1.7 quasar@^2.16.0 # 安裝開發依賴 npm install -D @vitejs/plugin-vue vite typescript vue-tsc @types/node # 創建基本檔案結構 echo "📁 創建檔案結構..." mkdir -p src/{components,modules,stores,services,utils,assets,router} mkdir -p src/assets/{styles,images,audio} mkdir -p src/components/{base,business,layout} mkdir -p public/icons mkdir -p tests/{unit,integration,e2e} # 創建基本檔案 touch src/main.ts src/App.vue touch src/assets/styles/main.scss touch vite.config.ts tsconfig.json echo "✅ 專案初始化完成!" echo "下一步:" echo "1. 配置 vite.config.ts" echo "2. 設定 TypeScript" echo "3. 安裝 UI 框架" echo "4. 開始開發 🎉" ``` ## 📊 專案規模估算 ### 文件數量預估 ``` 總計約 300-400 個檔案: ├── 組件檔案: ~80個 ├── 頁面檔案: ~40個 ├── 服務檔案: ~20個 ├── Store檔案: ~15個 ├── 工具檔案: ~25個 ├── 測試檔案: ~100個 ├── 配置檔案: ~15個 └── 其他檔案: ~20個 ``` ### 開發時間估算 ``` 階段性開發預估: ├── 專案初始化和環境配置: 1週 ├── 基礎組件和佈局: 2週 ├── 認證模組: 1週 ├── 詞彙學習模組: 3週 ├── 情境對話模組: 3週 ├── 學習地圖模組: 2週 ├── 商店模組: 2週 ├── 整合測試和優化: 2週 └── 部署和上線: 1週 總計: ~17週 (約4個月) ``` --- **文檔狀態**: 🟢 完整項目結構規劃 **最後更新**: 2025-09-09 **下次更新**: 根據開發進度調整