Files
jiebanke/docs/管理后台架构文档.md

56 KiB
Raw Permalink Blame History

结伴客管理后台架构文档

1. 项目概述

1.1 项目简介

结伴客管理后台是一个基于Vue.js 3.x + Element Plus的现代化管理系统为运营人员提供用户管理、内容管理、数据分析等功能。采用前后端分离架构支持多角色权限管理和实时数据监控。

1.2 业务目标

  • 运营管理:提供完整的运营管理功能
  • 数据分析:实时数据监控和分析报表
  • 权限控制:细粒度的角色权限管理
  • 系统监控:系统状态和性能监控

1.3 技术目标

  • 现代化技术栈Vue 3 + TypeScript + Vite
  • 组件化开发:高复用性的组件设计
  • 响应式设计:适配不同屏幕尺寸
  • 高性能:快速加载和流畅交互

2. 技术选型

2.1 核心框架

2.1.1 Vue.js 3.x

// 选型理由
{
  "框架": "Vue.js 3.x",
  "版本": "^3.3.0",
  "优势": [
    "Composition API逻辑复用性强",
    "TypeScript支持完善",
    "性能优化,体积更小",
    "生态系统成熟"
  ],
  "特性": [
    "响应式系统重构",
    "Tree-shaking支持",
    "Fragment支持",
    "Teleport组件"
  ]
}

2.1.2 构建工具 - Vite

{
  "构建工具": "Vite",
  "版本": "^4.4.0",
  "优势": [
    "极快的冷启动",
    "热更新速度快",
    "原生ES模块支持",
    "插件生态丰富"
  ]
}

2.2 UI组件库

2.2.1 Element Plus

{
  "组件库": "Element Plus",
  "版本": "^2.3.0",
  "优势": [
    "组件丰富完整",
    "设计规范统一",
    "Vue 3原生支持",
    "TypeScript支持"
  ],
  "核心组件": [
    "Table", "Form", "Dialog",
    "Menu", "Breadcrumb", "Pagination",
    "DatePicker", "Select", "Upload"
  ]
}

2.3 状态管理

2.3.1 Pinia

{
  "状态管理": "Pinia",
  "版本": "^2.1.0",
  "优势": [
    "Vue 3官方推荐",
    "TypeScript支持完善",
    "DevTools支持",
    "模块化设计"
  ]
}

2.4 路由管理

2.4.1 Vue Router 4

{
  "路由": "Vue Router 4",
  "版本": "^4.2.0",
  "特性": [
    "Composition API支持",
    "动态路由匹配",
    "路由守卫",
    "懒加载支持"
  ]
}

2.5 开发工具

2.5.1 TypeScript

{
  "类型系统": "TypeScript",
  "版本": "^5.0.0",
  "优势": [
    "静态类型检查",
    "IDE支持完善",
    "代码可维护性高",
    "重构安全"
  ]
}

2.5.2 ESLint + Prettier

{
  "代码规范": {
    "ESLint": "^8.45.0",
    "Prettier": "^3.0.0",
    "配置": "@vue/eslint-config-typescript"
  }
}

3. 架构设计

3.1 整体架构

graph TB
    subgraph "管理后台架构"
        A[表现层 Presentation Layer]
        B[业务逻辑层 Business Layer]
        C[数据管理层 Data Layer]
        D[服务层 Service Layer]
        E[工具层 Utils Layer]
    end
    
    subgraph "外部服务"
        F[后端API]
        G[文件存储]
        H[第三方服务]
    end
    
    A --> B
    B --> C
    B --> D
    D --> F
    D --> G
    D --> H
    B --> E
    C --> E

3.2 目录结构

src/
├── assets/              # 静态资源
│   ├── images/         # 图片资源
│   ├── icons/          # 图标资源
│   └── styles/         # 样式文件
├── components/          # 公共组件
│   ├── common/         # 通用组件
│   ├── business/       # 业务组件
│   └── layout/         # 布局组件
├── views/              # 页面组件
│   ├── dashboard/      # 仪表板
│   ├── user/           # 用户管理
│   ├── travel/         # 旅行管理
│   ├── animal/         # 动物管理
│   └── system/         # 系统管理
├── stores/             # 状态管理
│   ├── modules/        # 状态模块
│   └── index.ts        # Store入口
├── services/           # API服务
│   ├── api/            # API接口
│   ├── http/           # HTTP客户端
│   └── types/          # 类型定义
├── utils/              # 工具函数
│   ├── common.ts       # 通用工具
│   ├── date.ts         # 日期工具
│   ├── format.ts       # 格式化工具
│   └── validate.ts     # 验证工具
├── router/             # 路由配置
│   ├── index.ts        # 路由入口
│   ├── modules/        # 路由模块
│   └── guards.ts       # 路由守卫
├── hooks/              # 组合式函数
│   ├── useAuth.ts      # 认证Hook
│   ├── useTable.ts     # 表格Hook
│   └── useForm.ts      # 表单Hook
└── types/              # 全局类型定义
    ├── api.ts          # API类型
    ├── common.ts       # 通用类型
    └── store.ts        # Store类型

3.3 分层架构详解

3.3.1 表现层 (Presentation Layer)

// 页面组件示例
<template>
  <div class="user-management">
    <div class="header">
      <el-breadcrumb>
        <el-breadcrumb-item>用户管理</el-breadcrumb-item>
        <el-breadcrumb-item>用户列表</el-breadcrumb-item>
      </el-breadcrumb>
    </div>
    
    <div class="content">
      <SearchForm @search="handleSearch" />
      <DataTable 
        :data="userList" 
        :loading="loading"
        @edit="handleEdit"
        @delete="handleDelete"
      />
      <Pagination 
        :total="total"
        @change="handlePageChange"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/modules/user'
import type { User, SearchParams } from '@/types/api'

const userStore = useUserStore()
const userList = ref<User[]>([])
const loading = ref(false)
const total = ref(0)

onMounted(() => {
  loadUserList()
})

const loadUserList = async (params?: SearchParams) => {
  loading.value = true
  try {
    const result = await userStore.getUserList(params)
    userList.value = result.list
    total.value = result.total
  } finally {
    loading.value = false
  }
}
</script>

3.3.2 业务逻辑层 (Business Layer)

// 业务逻辑Hook
export function useUserManagement() {
  const userStore = useUserStore()
  const { message, messageBox } = useMessage()
  
  // 用户列表状态
  const state = reactive({
    userList: [] as User[],
    loading: false,
    total: 0,
    currentPage: 1,
    pageSize: 20,
    searchParams: {} as SearchParams
  })
  
  // 加载用户列表
  const loadUserList = async (refresh = false) => {
    if (refresh) {
      state.currentPage = 1
    }
    
    state.loading = true
    try {
      const params = {
        page: state.currentPage,
        pageSize: state.pageSize,
        ...state.searchParams
      }
      
      const result = await userStore.getUserList(params)
      state.userList = result.list
      state.total = result.total
    } catch (error) {
      message.error('加载用户列表失败')
    } finally {
      state.loading = false
    }
  }
  
  // 删除用户
  const deleteUser = async (userId: string) => {
    try {
      await messageBox.confirm('确定要删除该用户吗?')
      await userStore.deleteUser(userId)
      message.success('删除成功')
      await loadUserList()
    } catch (error) {
      if (error !== 'cancel') {
        message.error('删除失败')
      }
    }
  }
  
  // 搜索用户
  const searchUsers = (params: SearchParams) => {
    state.searchParams = params
    loadUserList(true)
  }
  
  return {
    state: readonly(state),
    loadUserList,
    deleteUser,
    searchUsers
  }
}

3.3.3 数据管理层 (Data Layer)

// Pinia Store
import { defineStore } from 'pinia'
import type { User, UserListParams, UserListResponse } from '@/types/api'
import { userApi } from '@/services/api/user'

export const useUserStore = defineStore('user', () => {
  // 状态
  const userList = ref<User[]>([])
  const currentUser = ref<User | null>(null)
  const loading = ref(false)
  
  // Getters
  const activeUsers = computed(() => 
    userList.value.filter(user => user.status === 'active')
  )
  
  const userCount = computed(() => userList.value.length)
  
  // Actions
  const getUserList = async (params: UserListParams): Promise<UserListResponse> => {
    loading.value = true
    try {
      const response = await userApi.getList(params)
      userList.value = response.list
      return response
    } finally {
      loading.value = false
    }
  }
  
  const getUserDetail = async (userId: string): Promise<User> => {
    const response = await userApi.getDetail(userId)
    currentUser.value = response
    return response
  }
  
  const createUser = async (userData: Partial<User>): Promise<User> => {
    const response = await userApi.create(userData)
    userList.value.unshift(response)
    return response
  }
  
  const updateUser = async (userId: string, userData: Partial<User>): Promise<User> => {
    const response = await userApi.update(userId, userData)
    const index = userList.value.findIndex(user => user.id === userId)
    if (index !== -1) {
      userList.value[index] = response
    }
    return response
  }
  
  const deleteUser = async (userId: string): Promise<void> => {
    await userApi.delete(userId)
    const index = userList.value.findIndex(user => user.id === userId)
    if (index !== -1) {
      userList.value.splice(index, 1)
    }
  }
  
  return {
    // State
    userList: readonly(userList),
    currentUser: readonly(currentUser),
    loading: readonly(loading),
    
    // Getters
    activeUsers,
    userCount,
    
    // Actions
    getUserList,
    getUserDetail,
    createUser,
    updateUser,
    deleteUser
  }
})

3.3.4 服务层 (Service Layer)

// HTTP客户端
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
import { useAuthStore } from '@/stores/modules/auth'
import { ElMessage } from 'element-plus'

class HttpClient {
  private instance: AxiosInstance
  
  constructor() {
    this.instance = axios.create({
      baseURL: import.meta.env.VITE_API_BASE_URL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })
    
    this.setupInterceptors()
  }
  
  private setupInterceptors() {
    // 请求拦截器
    this.instance.interceptors.request.use(
      (config) => {
        const authStore = useAuthStore()
        const token = authStore.token
        
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )
    
    // 响应拦截器
    this.instance.interceptors.response.use(
      (response) => {
        const { code, data, message } = response.data
        
        if (code === 200) {
          return data
        } else {
          ElMessage.error(message || '请求失败')
          return Promise.reject(new Error(message))
        }
      },
      (error) => {
        this.handleError(error)
        return Promise.reject(error)
      }
    )
  }
  
  private handleError(error: any) {
    if (error.response) {
      const { status, data } = error.response
      
      switch (status) {
        case 401:
          // 未授权,跳转登录
          const authStore = useAuthStore()
          authStore.logout()
          break
        case 403:
          ElMessage.error('权限不足')
          break
        case 404:
          ElMessage.error('请求的资源不存在')
          break
        case 500:
          ElMessage.error('服务器内部错误')
          break
        default:
          ElMessage.error(data?.message || '请求失败')
      }
    } else {
      ElMessage.error('网络错误')
    }
  }
  
  // HTTP方法封装
  get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.get(url, config)
  }
  
  post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post(url, data, config)
  }
  
  put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.put(url, data, config)
  }
  
  delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.delete(url, config)
  }
}

export const http = new HttpClient()

3.3.5 API服务

// 用户API服务
import { http } from '@/services/http'
import type { User, UserListParams, UserListResponse } from '@/types/api'

export const userApi = {
  // 获取用户列表
  getList(params: UserListParams): Promise<UserListResponse> {
    return http.get('/admin/users', { params })
  },
  
  // 获取用户详情
  getDetail(userId: string): Promise<User> {
    return http.get(`/admin/users/${userId}`)
  },
  
  // 创建用户
  create(userData: Partial<User>): Promise<User> {
    return http.post('/admin/users', userData)
  },
  
  // 更新用户
  update(userId: string, userData: Partial<User>): Promise<User> {
    return http.put(`/admin/users/${userId}`, userData)
  },
  
  // 删除用户
  delete(userId: string): Promise<void> {
    return http.delete(`/admin/users/${userId}`)
  },
  
  // 批量操作
  batchUpdate(userIds: string[], data: Partial<User>): Promise<void> {
    return http.post('/admin/users/batch', { userIds, data })
  },
  
  // 导出用户数据
  export(params: UserListParams): Promise<Blob> {
    return http.get('/admin/users/export', { 
      params, 
      responseType: 'blob' 
    })
  }
}

4. 核心模块设计

4.1 认证模块

4.1.1 认证Store

// 认证状态管理
export const useAuthStore = defineStore('auth', () => {
  // 状态
  const token = ref<string>('')
  const userInfo = ref<AdminUser | null>(null)
  const permissions = ref<string[]>([])
  const roles = ref<string[]>([])
  
  // Getters
  const isLogin = computed(() => !!token.value)
  const hasPermission = computed(() => (permission: string) => 
    permissions.value.includes(permission)
  )
  const hasRole = computed(() => (role: string) => 
    roles.value.includes(role)
  )
  
  // Actions
  const login = async (credentials: LoginCredentials) => {
    try {
      const response = await authApi.login(credentials)
      
      token.value = response.token
      userInfo.value = response.userInfo
      permissions.value = response.permissions
      roles.value = response.roles
      
      // 保存到本地存储
      localStorage.setItem('admin_token', response.token)
      localStorage.setItem('admin_user', JSON.stringify(response.userInfo))
      
      return response
    } catch (error) {
      throw error
    }
  }
  
  const logout = async () => {
    try {
      await authApi.logout()
    } finally {
      // 清除状态
      token.value = ''
      userInfo.value = null
      permissions.value = []
      roles.value = []
      
      // 清除本地存储
      localStorage.removeItem('admin_token')
      localStorage.removeItem('admin_user')
      
      // 跳转到登录页
      router.push('/login')
    }
  }
  
  const refreshToken = async () => {
    try {
      const response = await authApi.refreshToken()
      token.value = response.token
      localStorage.setItem('admin_token', response.token)
      return response.token
    } catch (error) {
      await logout()
      throw error
    }
  }
  
  const initAuth = () => {
    const savedToken = localStorage.getItem('admin_token')
    const savedUser = localStorage.getItem('admin_user')
    
    if (savedToken && savedUser) {
      token.value = savedToken
      userInfo.value = JSON.parse(savedUser)
    }
  }
  
  return {
    // State
    token: readonly(token),
    userInfo: readonly(userInfo),
    permissions: readonly(permissions),
    roles: readonly(roles),
    
    // Getters
    isLogin,
    hasPermission,
    hasRole,
    
    // Actions
    login,
    logout,
    refreshToken,
    initAuth
  }
})

4.1.2 路由守卫

// 路由守卫
import { useAuthStore } from '@/stores/modules/auth'

export function setupRouterGuards(router: Router) {
  // 全局前置守卫
  router.beforeEach(async (to, from, next) => {
    const authStore = useAuthStore()
    
    // 白名单路由
    const whiteList = ['/login', '/404', '/403']
    
    if (whiteList.includes(to.path)) {
      next()
      return
    }
    
    // 检查登录状态
    if (!authStore.isLogin) {
      next('/login')
      return
    }
    
    // 检查权限
    if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
      next('/403')
      return
    }
    
    // 检查角色
    if (to.meta.roles && !to.meta.roles.some(role => authStore.hasRole(role))) {
      next('/403')
      return
    }
    
    next()
  })
  
  // 全局后置守卫
  router.afterEach((to) => {
    // 设置页面标题
    document.title = `${to.meta.title || '管理后台'} - 结伴客`
    
    // 页面访问统计
    // analytics.trackPageView(to.path)
  })
}

4.2 表格组件

4.2.1 通用表格组件

<!-- DataTable.vue -->
<template>
  <div class="data-table">
    <el-table
      :data="data"
      :loading="loading"
      v-bind="$attrs"
      @selection-change="handleSelectionChange"
      @sort-change="handleSortChange"
    >
      <el-table-column
        v-if="showSelection"
        type="selection"
        width="55"
      />
      
      <el-table-column
        v-for="column in columns"
        :key="column.prop"
        v-bind="column"
      >
        <template #default="scope" v-if="column.slot">
          <slot :name="column.slot" :row="scope.row" :index="scope.$index" />
        </template>
      </el-table-column>
      
      <el-table-column
        v-if="showActions"
        label="操作"
        :width="actionWidth"
        fixed="right"
      >
        <template #default="scope">
          <slot name="actions" :row="scope.row" :index="scope.$index">
            <el-button
              v-for="action in actions"
              :key="action.key"
              :type="action.type"
              :size="action.size || 'small'"
              @click="handleAction(action.key, scope.row)"
            >
              {{ action.label }}
            </el-button>
          </slot>
        </template>
      </el-table-column>
    </el-table>
    
    <div class="table-footer" v-if="showPagination">
      <el-pagination
        v-model:current-page="currentPage"
        v-model:page-size="pageSize"
        :total="total"
        :page-sizes="pageSizes"
        layout="total, sizes, prev, pager, next, jumper"
        @current-change="handlePageChange"
        @size-change="handleSizeChange"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
interface Column {
  prop: string
  label: string
  width?: number | string
  minWidth?: number | string
  sortable?: boolean
  slot?: string
  formatter?: (row: any, column: any, cellValue: any) => string
}

interface Action {
  key: string
  label: string
  type?: 'primary' | 'success' | 'warning' | 'danger'
  size?: 'large' | 'default' | 'small'
}

interface Props {
  data: any[]
  columns: Column[]
  loading?: boolean
  showSelection?: boolean
  showActions?: boolean
  actions?: Action[]
  actionWidth?: number
  showPagination?: boolean
  total?: number
  pageSizes?: number[]
}

const props = withDefaults(defineProps<Props>(), {
  loading: false,
  showSelection: false,
  showActions: true,
  actionWidth: 200,
  showPagination: true,
  pageSizes: () => [10, 20, 50, 100]
})

const emit = defineEmits<{
  selectionChange: [selection: any[]]
  sortChange: [sort: { prop: string; order: string }]
  action: [key: string, row: any]
  pageChange: [page: number]
  sizeChange: [size: number]
}>()

const currentPage = ref(1)
const pageSize = ref(20)

const handleSelectionChange = (selection: any[]) => {
  emit('selectionChange', selection)
}

const handleSortChange = (sort: { prop: string; order: string }) => {
  emit('sortChange', sort)
}

const handleAction = (key: string, row: any) => {
  emit('action', key, row)
}

const handlePageChange = (page: number) => {
  emit('pageChange', page)
}

const handleSizeChange = (size: number) => {
  emit('sizeChange', size)
}
</script>

4.2.2 表格Hook

// useTable Hook
export function useTable<T = any>(
  api: (params: any) => Promise<{ list: T[]; total: number }>,
  options: {
    immediate?: boolean
    defaultParams?: Record<string, any>
    defaultPageSize?: number
  } = {}
) {
  const { immediate = true, defaultParams = {}, defaultPageSize = 20 } = options
  
  // 状态
  const state = reactive({
    data: [] as T[],
    loading: false,
    total: 0,
    currentPage: 1,
    pageSize: defaultPageSize,
    searchParams: { ...defaultParams },
    selectedRows: [] as T[]
  })
  
  // 加载数据
  const loadData = async (resetPage = false) => {
    if (resetPage) {
      state.currentPage = 1
    }
    
    state.loading = true
    try {
      const params = {
        page: state.currentPage,
        pageSize: state.pageSize,
        ...state.searchParams
      }
      
      const result = await api(params)
      state.data = result.list
      state.total = result.total
    } catch (error) {
      console.error('加载数据失败:', error)
    } finally {
      state.loading = false
    }
  }
  
  // 搜索
  const search = (params: Record<string, any>) => {
    state.searchParams = { ...defaultParams, ...params }
    loadData(true)
  }
  
  // 重置搜索
  const resetSearch = () => {
    state.searchParams = { ...defaultParams }
    loadData(true)
  }
  
  // 刷新
  const refresh = () => {
    loadData()
  }
  
  // 分页变化
  const handlePageChange = (page: number) => {
    state.currentPage = page
    loadData()
  }
  
  // 页面大小变化
  const handleSizeChange = (size: number) => {
    state.pageSize = size
    loadData(true)
  }
  
  // 选择变化
  const handleSelectionChange = (selection: T[]) => {
    state.selectedRows = selection
  }
  
  // 初始化
  if (immediate) {
    onMounted(() => {
      loadData()
    })
  }
  
  return {
    state: readonly(state),
    loadData,
    search,
    resetSearch,
    refresh,
    handlePageChange,
    handleSizeChange,
    handleSelectionChange
  }
}

4.3 表单组件

4.3.1 动态表单组件

<!-- DynamicForm.vue -->
<template>
  <el-form
    ref="formRef"
    :model="formData"
    :rules="formRules"
    v-bind="$attrs"
  >
    <el-form-item
      v-for="field in fields"
      :key="field.prop"
      :label="field.label"
      :prop="field.prop"
      :required="field.required"
    >
      <!-- 输入框 -->
      <el-input
        v-if="field.type === 'input'"
        v-model="formData[field.prop]"
        v-bind="field.attrs"
      />
      
      <!-- 数字输入框 -->
      <el-input-number
        v-else-if="field.type === 'number'"
        v-model="formData[field.prop]"
        v-bind="field.attrs"
      />
      
      <!-- 选择器 -->
      <el-select
        v-else-if="field.type === 'select'"
        v-model="formData[field.prop]"
        v-bind="field.attrs"
      >
        <el-option
          v-for="option in field.options"
          :key="option.value"
          :label="option.label"
          :value="option.value"
        />
      </el-select>
      
      <!-- 日期选择器 -->
      <el-date-picker
        v-else-if="field.type === 'date'"
        v-model="formData[field.prop]"
        v-bind="field.attrs"
      />
      
      <!-- 开关 -->
      <el-switch
        v-else-if="field.type === 'switch'"
        v-model="formData[field.prop]"
        v-bind="field.attrs"
      />
      
      <!-- 文本域 -->
      <el-input
        v-else-if="field.type === 'textarea'"
        v-model="formData[field.prop]"
        type="textarea"
        v-bind="field.attrs"
      />
      
      <!-- 上传组件 -->
      <el-upload
        v-else-if="field.type === 'upload'"
        v-bind="field.attrs"
        @success="(response) => handleUploadSuccess(response, field.prop)"
      >
        <el-button type="primary">点击上传</el-button>
      </el-upload>
      
      <!-- 自定义插槽 -->
      <slot
        v-else-if="field.type === 'slot'"
        :name="field.slot"
        :field="field"
        :value="formData[field.prop]"
        @update:value="(value) => formData[field.prop] = value"
      />
    </el-form-item>
    
    <el-form-item v-if="showActions">
      <slot name="actions" :form-data="formData" :validate="validate">
        <el-button type="primary" @click="handleSubmit">
          {{ submitText }}
        </el-button>
        <el-button @click="handleReset">
          {{ resetText }}
        </el-button>
      </slot>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
interface FormField {
  prop: string
  label: string
  type: 'input' | 'number' | 'select' | 'date' | 'switch' | 'textarea' | 'upload' | 'slot'
  required?: boolean
  attrs?: Record<string, any>
  options?: { label: string; value: any }[]
  slot?: string
  rules?: any[]
}

interface Props {
  fields: FormField[]
  modelValue: Record<string, any>
  showActions?: boolean
  submitText?: string
  resetText?: string
}

const props = withDefaults(defineProps<Props>(), {
  showActions: true,
  submitText: '提交',
  resetText: '重置'
})

const emit = defineEmits<{
  'update:modelValue': [value: Record<string, any>]
  submit: [data: Record<string, any>]
  reset: []
}>()

const formRef = ref<FormInstance>()
const formData = ref({ ...props.modelValue })

// 监听外部数据变化
watch(() => props.modelValue, (newValue) => {
  formData.value = { ...newValue }
}, { deep: true })

// 监听内部数据变化
watch(formData, (newValue) => {
  emit('update:modelValue', newValue)
}, { deep: true })

// 生成表单规则
const formRules = computed(() => {
  const rules: Record<string, any[]> = {}
  
  props.fields.forEach(field => {
    if (field.required || field.rules) {
      rules[field.prop] = [
        ...(field.required ? [{ required: true, message: `请输入${field.label}` }] : []),
        ...(field.rules || [])
      ]
    }
  })
  
  return rules
})

// 验证表单
const validate = async (): Promise<boolean> => {
  if (!formRef.value) return false
  
  try {
    await formRef.value.validate()
    return true
  } catch {
    return false
  }
}

// 提交表单
const handleSubmit = async () => {
  const isValid = await validate()
  if (isValid) {
    emit('submit', formData.value)
  }
}

// 重置表单
const handleReset = () => {
  formRef.value?.resetFields()
  emit('reset')
}

// 上传成功处理
const handleUploadSuccess = (response: any, prop: string) => {
  formData.value[prop] = response.url
}

// 暴露方法
defineExpose({
  validate,
  resetFields: () => formRef.value?.resetFields()
})
</script>

4.3.2 表单Hook

// useForm Hook
export function useForm<T extends Record<string, any>>(
  initialData: T,
  options: {
    resetAfterSubmit?: boolean
    validateOnSubmit?: boolean
  } = {}
) {
  const { resetAfterSubmit = false, validateOnSubmit = true } = options
  
  // 表单数据
  const formData = ref<T>({ ...initialData })
  const formRef = ref<FormInstance>()
  
  // 表单状态
  const state = reactive({
    loading: false,
    errors: {} as Record<string, string>
  })
  
  // 重置表单
  const resetForm = () => {
    formData.value = { ...initialData }
    formRef.value?.resetFields()
    state.errors = {}
  }
  
  // 验证表单
  const validateForm = async (): Promise<boolean> => {
    if (!formRef.value) return false
    
    try {
      await formRef.value.validate()
      state.errors = {}
      return true
    } catch (errors) {
      state.errors = errors as Record<string, string>
      return false
    }
  }
  
  // 提交表单
  const submitForm = async (
    submitFn: (data: T) => Promise<any>
  ): Promise<any> => {
    if (validateOnSubmit) {
      const isValid = await validateForm()
      if (!isValid) return
    }
    
    state.loading = true
    try {
      const result = await submitFn(formData.value)
      
      if (resetAfterSubmit) {
        resetForm()
      }
      
      return result
    } finally {
      state.loading = false
    }
  }
  
  // 设置字段值
  const setFieldValue = <K extends keyof T>(field: K, value: T[K]) => {
    formData.value[field] = value
  }
  
  // 设置字段错误
  const setFieldError = (field: string, error: string) => {
    state.errors[field] = error
  }
  
  // 清除字段错误
  const clearFieldError = (field: string) => {
    delete state.errors[field]
  }
  
  return {
    formData,
    formRef,
    state: readonly(state),
    resetForm,
    validateForm,
    submitForm,
    setFieldValue,
    setFieldError,
    clearFieldError
  }
}

5. 权限管理

5.1 权限设计

5.1.1 权限模型

// 权限类型定义
interface Permission {
  id: string
  name: string
  code: string
  type: 'menu' | 'button' | 'api'
  resource: string
  action: string
  description?: string
}

interface Role {
  id: string
  name: string
  code: string
  permissions: Permission[]
  description?: string
}

interface AdminUser {
  id: string
  username: string
  email: string
  roles: Role[]
  permissions: Permission[]
  status: 'active' | 'inactive'
}

5.1.2 权限指令

// 权限指令
import type { App, DirectiveBinding } from 'vue'
import { useAuthStore } from '@/stores/modules/auth'

export function setupPermissionDirective(app: App) {
  // v-permission 指令
  app.directive('permission', {
    mounted(el: HTMLElement, binding: DirectiveBinding) {
      const { value } = binding
      const authStore = useAuthStore()
      
      if (value && !authStore.hasPermission(value)) {
        el.style.display = 'none'
      }
    },
    
    updated(el: HTMLElement, binding: DirectiveBinding) {
      const { value } = binding
      const authStore = useAuthStore()
      
      if (value && !authStore.hasPermission(value)) {
        el.style.display = 'none'
      } else {
        el.style.display = ''
      }
    }
  })
  
  // v-role 指令
  app.directive('role', {
    mounted(el: HTMLElement, binding: DirectiveBinding) {
      const { value } = binding
      const authStore = useAuthStore()
      
      const hasRole = Array.isArray(value) 
        ? value.some(role => authStore.hasRole(role))
        : authStore.hasRole(value)
      
      if (!hasRole) {
        el.style.display = 'none'
      }
    }
  })
}

5.1.3 权限组件

<!-- PermissionWrapper.vue -->
<template>
  <div v-if="hasAccess">
    <slot />
  </div>
  <div v-else-if="showFallback">
    <slot name="fallback">
      <div class="no-permission">
        <el-empty description="暂无权限访问" />
      </div>
    </slot>
  </div>
</template>

<script setup lang="ts">
interface Props {
  permission?: string | string[]
  role?: string | string[]
  showFallback?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  showFallback: false
})

const authStore = useAuthStore()

const hasAccess = computed(() => {
  // 检查权限
  if (props.permission) {
    const permissions = Array.isArray(props.permission) 
      ? props.permission 
      : [props.permission]
    
    return permissions.some(p => authStore.hasPermission(p))
  }
  
  // 检查角色
  if (props.role) {
    const roles = Array.isArray(props.role) 
      ? props.role 
      : [props.role]
    
    return roles.some(r => authStore.hasRole(r))
  }
  
  return true
})
</script>

5.2 菜单权限

5.2.1 动态菜单

// 菜单配置
export interface MenuItem {
  id: string
  title: string
  icon?: string
  path?: string
  permission?: string
  roles?: string[]
  children?: MenuItem[]
  hidden?: boolean
}

// 菜单数据
export const menuConfig: MenuItem[] = [
  {
    id: 'dashboard',
    title: '仪表板',
    icon: 'Dashboard',
    path: '/dashboard',
    permission: 'dashboard:view'
  },
  {
    id: 'user',
    title: '用户管理',
    icon: 'User',
    permission: 'user:view',
    children: [
      {
        id: 'user-list',
        title: '用户列表',
        path: '/user/list',
        permission: 'user:list'
      },
      {
        id: 'user-role',
        title: '角色管理',
        path: '/user/role',
        permission: 'user:role'
      }
    ]
  },
  {
    id: 'travel',
    title: '旅行管理',
    icon: 'Location',
    permission: 'travel:view',
    children: [
      {
        id: 'travel-list',
        title: '旅行列表',
        path: '/travel/list',
        permission: 'travel:list'
      },
      {
        id: 'travel-category',
        title: '分类管理',
        path: '/travel/category',
        permission: 'travel:category'
      }
    ]
  }
]

// 菜单过滤
export function filterMenuByPermission(
  menus: MenuItem[], 
  hasPermission: (permission: string) => boolean,
  hasRole: (role: string) => boolean
): MenuItem[] {
  return menus.filter(menu => {
    // 检查权限
    if (menu.permission && !hasPermission(menu.permission)) {
      return false
    }
    
    // 检查角色
    if (menu.roles && !menu.roles.some(role => hasRole(role))) {
      return false
    }
    
    // 递归过滤子菜单
    if (menu.children) {
      menu.children = filterMenuByPermission(menu.children, hasPermission, hasRole)
    }
    
    return true
  })
}

5.2.2 菜单组件

<!-- SideMenu.vue -->
<template>
  <el-menu
    :default-active="activeMenu"
    :collapse="isCollapse"
    :unique-opened="true"
    router
  >
    <menu-item
      v-for="menu in filteredMenus"
      :key="menu.id"
      :menu="menu"
    />
  </el-menu>
</template>

<script setup lang="ts">
import MenuItem from './MenuItem.vue'
import { menuConfig, filterMenuByPermission } from '@/config/menu'

interface Props {
  isCollapse?: boolean
}

defineProps<Props>()

const route = useRoute()
const authStore = useAuthStore()

// 当前激活菜单
const activeMenu = computed(() => route.path)

// 过滤后的菜单
const filteredMenus = computed(() => {
  return filterMenuByPermission(
    menuConfig,
    authStore.hasPermission,
    authStore.hasRole
  )
})
</script>

6. 性能优化

6.1 代码分割

6.1.1 路由懒加载

// 路由配置
const routes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/dashboard/index.vue'),
    meta: {
      title: '仪表板',
      permission: 'dashboard:view'
    }
  },
  {
    path: '/user',
    name: 'User',
    component: () => import('@/views/user/index.vue'),
    meta: {
      title: '用户管理',
      permission: 'user:view'
    },
    children: [
      {
        path: 'list',
        name: 'UserList',
        component: () => import('@/views/user/list.vue'),
        meta: {
          title: '用户列表',
          permission: 'user:list'
        }
      }
    ]
  }
]

6.1.2 组件懒加载

// 异步组件
import { defineAsyncComponent } from 'vue'

export const AsyncDataTable = defineAsyncComponent({
  loader: () => import('@/components/DataTable.vue'),
  loadingComponent: () => h('div', '加载中...'),
  errorComponent: () => h('div', '加载失败'),
  delay: 200,
  timeout: 3000
})

6.2 缓存优化

6.2.1 组件缓存

<!-- 页面缓存 -->
<template>
  <router-view v-slot="{ Component, route }">
    <keep-alive :include="cachedViews">
      <component :is="Component" :key="route.path" />
    </keep-alive>
  </router-view>
</template>

<script setup lang="ts">
// 缓存的页面组件
const cachedViews = ref(['UserList', 'TravelList'])
</script>

6.2.2 数据缓存

// 数据缓存Hook
export function useCache<T>(
  key: string,
  fetcher: () => Promise<T>,
  options: {
    ttl?: number // 缓存时间(毫秒)
    staleWhileRevalidate?: boolean // 后台更新
  } = {}
) {
  const { ttl = 5 * 60 * 1000, staleWhileRevalidate = true } = options
  
  const data = ref<T>()
  const loading = ref(false)
  const error = ref<Error>()
  
  const cacheKey = `cache_${key}`
  
  // 从缓存获取数据
  const getFromCache = (): { data: T; timestamp: number } | null => {
    try {
      const cached = localStorage.getItem(cacheKey)
      return cached ? JSON.parse(cached) : null
    } catch {
      return null
    }
  }
  
  // 保存到缓存
  const saveToCache = (value: T) => {
    try {
      localStorage.setItem(cacheKey, JSON.stringify({
        data: value,
        timestamp: Date.now()
      }))
    } catch {
      // 忽略存储错误
    }
  }
  
  // 检查缓存是否过期
  const isCacheExpired = (timestamp: number): boolean => {
    return Date.now() - timestamp > ttl
  }
  
  // 获取数据
  const fetchData = async (useCache = true): Promise<T> => {
    // 检查缓存
    if (useCache) {
      const cached = getFromCache()
      if (cached && !isCacheExpired(cached.timestamp)) {
        data.value = cached.data
        
        // 后台更新
        if (staleWhileRevalidate) {
          fetchData(false).catch(() => {})
        }
        
        return cached.data
      }
    }
    
    // 获取新数据
    loading.value = true
    error.value = undefined
    
    try {
      const result = await fetcher()
      data.value = result
      saveToCache(result)
      return result
    } catch (err) {
      error.value = err as Error
      throw err
    } finally {
      loading.value = false
    }
  }
  
  // 清除缓存
  const clearCache = () => {
    localStorage.removeItem(cacheKey)
  }
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    fetchData,
    clearCache
  }
}

6.3 虚拟滚动

6.3.1 虚拟列表组件

<!-- VirtualList.vue -->
<template>
  <div 
    ref="containerRef"
    class="virtual-list"
    :style="{ height: containerHeight + 'px' }"
    @scroll="handleScroll"
  >
    <div 
      class="virtual-list-phantom"
      :style="{ height: totalHeight + 'px' }"
    />
    
    <div 
      class="virtual-list-content"
      :style="{ transform: `translateY(${offsetY}px)` }"
    >
      <div
        v-for="item in visibleItems"
        :key="item.index"
        class="virtual-list-item"
        :style="{ height: itemHeight + 'px' }"
      >
        <slot :item="item.data" :index="item.index" />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
interface Props {
  items: any[]
  itemHeight: number
  containerHeight: number
  buffer?: number
}

const props = withDefaults(defineProps<Props>(), {
  buffer: 5
})

const containerRef = ref<HTMLElement>()
const scrollTop = ref(0)

// 计算属性
const totalHeight = computed(() => props.items.length * props.itemHeight)

const visibleCount = computed(() => 
  Math.ceil(props.containerHeight / props.itemHeight)
)

const startIndex = computed(() => 
  Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.buffer)
)

const endIndex = computed(() => 
  Math.min(
    props.items.length,
    startIndex.value + visibleCount.value + props.buffer * 2
  )
)

const visibleItems = computed(() => {
  const items = []
  for (let i = startIndex.value; i < endIndex.value; i++) {
    items.push({
      index: i,
      data: props.items[i]
    })
  }
  return items
})

const offsetY = computed(() => startIndex.value * props.itemHeight)

// 滚动处理
const handleScroll = (e: Event) => {
  const target = e.target as HTMLElement
  scrollTop.value = target.scrollTop
}
</script>

7. 测试策略

7.1 单元测试

7.1.1 组件测试

// DataTable.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import DataTable from '@/components/DataTable.vue'

describe('DataTable', () => {
  const mockData = [
    { id: 1, name: 'John', age: 25 },
    { id: 2, name: 'Jane', age: 30 }
  ]
  
  const mockColumns = [
    { prop: 'name', label: '姓名' },
    { prop: 'age', label: '年龄' }
  ]
  
  it('renders table with data', () => {
    const wrapper = mount(DataTable, {
      props: {
        data: mockData,
        columns: mockColumns
      }
    })
    
    expect(wrapper.find('.el-table').exists()).toBe(true)
    expect(wrapper.findAll('.el-table__row')).toHaveLength(2)
  })
  
  it('emits selection-change event', async () => {
    const wrapper = mount(DataTable, {
      props: {
        data: mockData,
        columns: mockColumns,
        showSelection: true
      }
    })
    
    const checkbox = wrapper.find('.el-checkbox')
    await checkbox.trigger('click')
    
    expect(wrapper.emitted('selectionChange')).toBeTruthy()
  })
})

7.1.2 Store测试

// userStore.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { useUserStore } from '@/stores/modules/user'
import { userApi } from '@/services/api/user'

// Mock API
vi.mock('@/services/api/user')

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  it('loads user list', async () => {
    const mockResponse = {
      list: [{ id: 1, name: 'John' }],
      total: 1
    }
    
    vi.mocked(userApi.getList).mockResolvedValue(mockResponse)
    
    const store = useUserStore()
    const result = await store.getUserList({ page: 1 })
    
    expect(result).toEqual(mockResponse)
    expect(store.userList).toEqual(mockResponse.list)
  })
  
  it('handles API error', async () => {
    vi.mocked(userApi.getList).mockRejectedValue(new Error('API Error'))
    
    const store = useUserStore()
    
    await expect(store.getUserList({ page: 1 })).rejects.toThrow('API Error')
  })
})

7.2 集成测试

7.2.1 页面测试

// UserList.test.ts
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { describe, it, expect, vi } from 'vitest'
import UserList from '@/views/user/list.vue'

describe('UserList Page', () => {
  it('loads and displays user list', async () => {
    const wrapper = mount(UserList, {
      global: {
        plugins: [
          createTestingPinia({
            createSpy: vi.fn
          })
        ]
      }
    })
    
    // 等待数据加载
    await wrapper.vm.$nextTick()
    
    expect(wrapper.find('.user-list').exists()).toBe(true)
    expect(wrapper.find('.data-table').exists()).toBe(true)
  })
})

7.3 E2E测试

7.3.1 用户流程测试

// user-management.e2e.ts
import { test, expect } from '@playwright/test'

test.describe('User Management', () => {
  test.beforeEach(async ({ page }) => {
    // 登录
    await page.goto('/login')
    await page.fill('[data-testid="username"]', 'admin')
    await page.fill('[data-testid="password"]', 'password')
    await page.click('[data-testid="login-btn"]')
    await page.waitForURL('/dashboard')
  })
  
  test('should create new user', async ({ page }) => {
    // 导航到用户管理
    await page.click('[data-testid="user-menu"]')
    await page.click('[data-testid="user-list"]')
    
    // 点击新建用户
    await page.click('[data-testid="create-user-btn"]')
    
    // 填写表单
    await page.fill('[data-testid="username"]', 'testuser')
    await page.fill('[data-testid="email"]', 'test@example.com')
    await page.selectOption('[data-testid="role"]', 'user')
    
    // 提交表单
    await page.click('[data-testid="submit-btn"]')
    
    // 验证结果
    await expect(page.locator('.el-message--success')).toBeVisible()
    await expect(page.locator('text=testuser')).toBeVisible()
  })
})

8. 部署配置

8.1 构建配置

8.1.1 Vite配置

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  
  build: {
    target: 'es2015',
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false,
    
    rollupOptions: {
      output: {
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[ext]/[name]-[hash].[ext]',
        
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          element: ['element-plus'],
          utils: ['axios', 'dayjs']
        }
      }
    },
    
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
})

8.1.2 环境配置

// .env.development
VITE_APP_TITLE=结伴客管理后台
VITE_API_BASE_URL=http://localhost:8080/api
VITE_UPLOAD_URL=http://localhost:8080/upload

// .env.production
VITE_APP_TITLE=结伴客管理后台
VITE_API_BASE_URL=https://api.jiebanke.com
VITE_UPLOAD_URL=https://cdn.jiebanke.com/upload

8.2 Docker部署

8.2.1 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

# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

8.2.2 Nginx配置

# nginx.conf
server {
    listen 80;
    server_name localhost;
    
    root /usr/share/nginx/html;
    index index.html;
    
    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    
    # SPA路由支持
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    # API代理
    location /api/ {
        proxy_pass http://backend:8080/;
        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;
    }
    
    # 安全头
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
}

8.3 CI/CD流程

8.3.1 GitHub Actions

# .github/workflows/deploy.yml
name: Deploy Admin System

on:
  push:
    branches: [main]
    paths: ['admin-system/**']

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
        cache-dependency-path: admin-system/package-lock.json
        
    - name: Install dependencies
      working-directory: admin-system
      run: npm ci
      
    - name: Run tests
      working-directory: admin-system
      run: npm run test
      
    - name: Build application
      working-directory: admin-system
      run: npm run build
      
    - name: Build Docker image
      run: |
        docker build -t jiebanke/admin-system:${{ github.sha }} ./admin-system
        docker tag jiebanke/admin-system:${{ github.sha }} jiebanke/admin-system:latest
        
    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
        
    - name: Push Docker image
      run: |
        docker push jiebanke/admin-system:${{ github.sha }}
        docker push jiebanke/admin-system:latest
        
    - name: Deploy to production
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.PROD_HOST }}
        username: ${{ secrets.PROD_USER }}
        key: ${{ secrets.PROD_SSH_KEY }}
        script: |
          cd /opt/jiebanke
          docker-compose pull admin-system
          docker-compose up -d admin-system

8.3.2 Docker Compose

# docker-compose.yml
version: '3.8'

services:
  admin-system:
    image: jiebanke/admin-system:latest
    container_name: jiebanke-admin
    ports:
      - "3000:80"
    environment:
      - NODE_ENV=production
    depends_on:
      - backend
    networks:
      - jiebanke-network
    restart: unless-stopped
    
  backend:
    image: jiebanke/backend:latest
    container_name: jiebanke-backend
    ports:
      - "8080:8080"
    environment:
      - NODE_ENV=production
      - DB_HOST=mysql
      - REDIS_HOST=redis
    depends_on:
      - mysql
      - redis
    networks:
      - jiebanke-network
    restart: unless-stopped

networks:
  jiebanke-network:
    driver: bridge

9. 监控与分析

9.1 性能监控

9.1.1 性能指标收集

// 性能监控
export class PerformanceMonitor {
  private static instance: PerformanceMonitor
  
  static getInstance(): PerformanceMonitor {
    if (!this.instance) {
      this.instance = new PerformanceMonitor()
    }
    return this.instance
  }
  
  // 页面加载性能
  measurePageLoad() {
    if (typeof window !== 'undefined' && 'performance' in window) {
      window.addEventListener('load', () => {
        setTimeout(() => {
          const perfData = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
          
          const metrics = {
            // 页面加载时间
            loadTime: perfData.loadEventEnd - perfData.navigationStart,
            // DOM解析时间
            domParseTime: perfData.domContentLoadedEventEnd - perfData.navigationStart,
            // 首次内容绘制
            firstContentfulPaint: this.getFCP(),
            // 最大内容绘制
            largestContentfulPaint: this.getLCP(),
            // 累积布局偏移
            cumulativeLayoutShift: this.getCLS()
          }
          
          this.sendMetrics('page_load', metrics)
        }, 0)
      })
    }
  }
  
  // 获取FCP
  private getFCP(): number {
    const entries = performance.getEntriesByType('paint')
    const fcpEntry = entries.find(entry => entry.name === 'first-contentful-paint')
    return fcpEntry ? fcpEntry.startTime : 0
  }
  
  // 获取LCP
  private getLCP(): number {
    return new Promise((resolve) => {
      new PerformanceObserver((list) => {
        const entries = list.getEntries()
        const lastEntry = entries[entries.length - 1]
        resolve(lastEntry.startTime)
      }).observe({ entryTypes: ['largest-contentful-paint'] })
    })
  }
  
  // 获取CLS
  private getCLS(): number {
    let clsValue = 0
    new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          clsValue += entry.value
        }
      }
    }).observe({ entryTypes: ['layout-shift'] })
    return clsValue
  }
  
  // 发送指标数据
  private sendMetrics(type: string, data: any) {
    // 发送到监控服务
    fetch('/api/metrics', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        type,
        data,
        timestamp: Date.now(),
        userAgent: navigator.userAgent,
        url: location.href
      })
    }).catch(console.error)
  }
}

9.1.2 错误监控

// 错误监控
export class ErrorMonitor {
  private static instance: ErrorMonitor
  
  static getInstance(): ErrorMonitor {
    if (!this.instance) {
      this.instance = new ErrorMonitor()
    }
    return this.instance
  }
  
  init() {
    // JavaScript错误
    window.addEventListener('error', (event) => {
      this.handleError({
        type: 'javascript',
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        stack: event.error?.stack
      })
    })
    
    // Promise错误
    window.addEventListener('unhandledrejection', (event) => {
      this.handleError({
        type: 'promise',
        message: event.reason?.message || 'Unhandled Promise Rejection',
        stack: event.reason?.stack
      })
    })
    
    // Vue错误处理
    app.config.errorHandler = (err, instance, info) => {
      this.handleError({
        type: 'vue',
        message: err.message,
        stack: err.stack,
        componentName: instance?.$options.name,
        info
      })
    }
  }
  
  private handleError(error: any) {
    console.error('Error caught:', error)
    
    // 发送错误报告
    fetch('/api/errors', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        ...error,
        timestamp: Date.now(),
        userAgent: navigator.userAgent,
        url: location.href,
        userId: this.getCurrentUserId()
      })
    }).catch(console.error)
  }
  
  private getCurrentUserId(): string | null {
    const authStore = useAuthStore()
    return authStore.userInfo?.id || null
  }
}

9.2 用户行为分析

9.2.1 埋点系统

// 埋点系统
export class Analytics {
  private static instance: Analytics
  private queue: any[] = []
  private isInitialized = false
  
  static getInstance(): Analytics {
    if (!this.instance) {
      this.instance = new Analytics()
    }
    return this.instance
  }
  
  init(config: { apiUrl: string; appId: string }) {
    this.isInitialized = true
    
    // 发送队列中的事件
    this.queue.forEach(event => this.sendEvent(event))
    this.queue = []
  }
  
  // 页面访问
  trackPageView(path: string, title?: string) {
    this.track('page_view', {
      path,
      title,
      referrer: document.referrer,
      timestamp: Date.now()
    })
  }
  
  // 用户行为
  trackEvent(action: string, category: string, label?: string, value?: number) {
    this.track('user_action', {
      action,
      category,
      label,
      value,
      timestamp: Date.now()
    })
  }
  
  // 业务事件
  trackBusiness(event: string, properties: Record<string, any>) {
    this.track('business_event', {
      event,
      properties,
      timestamp: Date.now()
    })
  }
  
  private track(type: string, data: any) {
    const event = {
      type,
      data,
      sessionId: this.getSessionId(),
      userId: this.getUserId(),
      deviceInfo: this.getDeviceInfo()
    }
    
    if (this.isInitialized) {
      this.sendEvent(event)
    } else {
      this.queue.push(event)
    }
  }
  
  private sendEvent(event: any) {
    fetch('/api/analytics', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(event)
    }).catch(console.error)
  }
  
  private getSessionId(): string {
    let sessionId = sessionStorage.getItem('analytics_session_id')
    if (!sessionId) {
      sessionId = this.generateId()
      sessionStorage.setItem('analytics_session_id', sessionId)
    }
    return sessionId
  }
  
  private getUserId(): string | null {
    const authStore = useAuthStore()
    return authStore.userInfo?.id || null
  }
  
  private getDeviceInfo() {
    return {
      userAgent: navigator.userAgent,
      language: navigator.language,
      platform: navigator.platform,
      screenResolution: `${screen.width}x${screen.height}`,
      viewportSize: `${window.innerWidth}x${window.innerHeight}`
    }
  }
  
  private generateId(): string {
    return Math.random().toString(36).substr(2, 9)
  }
}

10. 总结

10.1 架构优势

  1. 现代化技术栈

    • Vue 3 + TypeScript提供类型安全和开发体验
    • Vite构建工具提供极快的开发和构建速度
    • Element Plus提供丰富的UI组件
  2. 组件化设计

    • 高度复用的组件库
    • 清晰的组件层次结构
    • 统一的设计规范
  3. 状态管理

    • Pinia提供现代化的状态管理
    • 模块化的Store设计
    • TypeScript完美支持
  4. 权限控制

    • 细粒度的权限管理
    • 动态菜单和路由
    • 多角色支持

10.2 扩展性设计

  1. 模块化架构

    • 清晰的模块边界
    • 松耦合的组件设计
    • 易于扩展新功能
  2. 插件化支持

    • 支持第三方插件
    • 可配置的功能模块
    • 灵活的扩展机制
  3. 国际化支持

    • 多语言切换
    • 本地化配置
    • 文化适配

10.3 运维保障

  1. 监控体系

    • 性能监控
    • 错误监控
    • 用户行为分析
  2. 部署自动化

    • CI/CD流程
    • Docker容器化
    • 蓝绿部署
  3. 安全保障

    • 权限控制
    • 数据加密
    • 安全头配置

10.4 持续改进

  1. 性能优化

    • 代码分割
    • 懒加载
    • 缓存策略
  2. 用户体验

    • 响应式设计
    • 交互优化
    • 无障碍支持
  3. 开发效率

    • 代码规范
    • 自动化测试
    • 开发工具链

通过以上架构设计,结伴客管理后台将具备高性能、高可用、易维护的特点,为运营团队提供强大的管理工具,支撑业务的快速发展。