857 lines
24 KiB
Markdown
857 lines
24 KiB
Markdown
# 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
|
|
**下次更新**: 根據開發進度調整 |