dramaling-app/docs/04_technical/03_frontend/vue-project-structure.md

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
**下次更新**: 根據開發進度調整