# 结伴客管理后台架构文档 ## 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 ```javascript // 选型理由 { "框架": "Vue.js 3.x", "版本": "^3.3.0", "优势": [ "Composition API,逻辑复用性强", "TypeScript支持完善", "性能优化,体积更小", "生态系统成熟" ], "特性": [ "响应式系统重构", "Tree-shaking支持", "Fragment支持", "Teleport组件" ] } ``` #### 2.1.2 构建工具 - Vite ```javascript { "构建工具": "Vite", "版本": "^4.4.0", "优势": [ "极快的冷启动", "热更新速度快", "原生ES模块支持", "插件生态丰富" ] } ``` ### 2.2 UI组件库 #### 2.2.1 Element Plus ```javascript { "组件库": "Element Plus", "版本": "^2.3.0", "优势": [ "组件丰富完整", "设计规范统一", "Vue 3原生支持", "TypeScript支持" ], "核心组件": [ "Table", "Form", "Dialog", "Menu", "Breadcrumb", "Pagination", "DatePicker", "Select", "Upload" ] } ``` ### 2.3 状态管理 #### 2.3.1 Pinia ```javascript { "状态管理": "Pinia", "版本": "^2.1.0", "优势": [ "Vue 3官方推荐", "TypeScript支持完善", "DevTools支持", "模块化设计" ] } ``` ### 2.4 路由管理 #### 2.4.1 Vue Router 4 ```javascript { "路由": "Vue Router 4", "版本": "^4.2.0", "特性": [ "Composition API支持", "动态路由匹配", "路由守卫", "懒加载支持" ] } ``` ### 2.5 开发工具 #### 2.5.1 TypeScript ```javascript { "类型系统": "TypeScript", "版本": "^5.0.0", "优势": [ "静态类型检查", "IDE支持完善", "代码可维护性高", "重构安全" ] } ``` #### 2.5.2 ESLint + Prettier ```javascript { "代码规范": { "ESLint": "^8.45.0", "Prettier": "^3.0.0", "配置": "@vue/eslint-config-typescript" } } ``` ## 3. 架构设计 ### 3.1 整体架构 ```mermaid 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) ```typescript // 页面组件示例 ``` #### 3.3.2 业务逻辑层 (Business Layer) ```typescript // 业务逻辑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) ```typescript // 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([]) const currentUser = ref(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 => { 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 => { const response = await userApi.getDetail(userId) currentUser.value = response return response } const createUser = async (userData: Partial): Promise => { const response = await userApi.create(userData) userList.value.unshift(response) return response } const updateUser = async (userId: string, userData: Partial): Promise => { 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 => { 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) ```typescript // 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(url: string, config?: AxiosRequestConfig): Promise { return this.instance.get(url, config) } post(url: string, data?: any, config?: AxiosRequestConfig): Promise { return this.instance.post(url, data, config) } put(url: string, data?: any, config?: AxiosRequestConfig): Promise { return this.instance.put(url, data, config) } delete(url: string, config?: AxiosRequestConfig): Promise { return this.instance.delete(url, config) } } export const http = new HttpClient() ``` #### 3.3.5 API服务 ```typescript // 用户API服务 import { http } from '@/services/http' import type { User, UserListParams, UserListResponse } from '@/types/api' export const userApi = { // 获取用户列表 getList(params: UserListParams): Promise { return http.get('/admin/users', { params }) }, // 获取用户详情 getDetail(userId: string): Promise { return http.get(`/admin/users/${userId}`) }, // 创建用户 create(userData: Partial): Promise { return http.post('/admin/users', userData) }, // 更新用户 update(userId: string, userData: Partial): Promise { return http.put(`/admin/users/${userId}`, userData) }, // 删除用户 delete(userId: string): Promise { return http.delete(`/admin/users/${userId}`) }, // 批量操作 batchUpdate(userIds: string[], data: Partial): Promise { return http.post('/admin/users/batch', { userIds, data }) }, // 导出用户数据 export(params: UserListParams): Promise { return http.get('/admin/users/export', { params, responseType: 'blob' }) } } ``` ## 4. 核心模块设计 ### 4.1 认证模块 #### 4.1.1 认证Store ```typescript // 认证状态管理 export const useAuthStore = defineStore('auth', () => { // 状态 const token = ref('') const userInfo = ref(null) const permissions = ref([]) const roles = ref([]) // 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 路由守卫 ```typescript // 路由守卫 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 通用表格组件 ```vue ``` #### 4.2.2 表格Hook ```typescript // useTable Hook export function useTable( api: (params: any) => Promise<{ list: T[]; total: number }>, options: { immediate?: boolean defaultParams?: Record 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) => { 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 动态表单组件 ```vue ``` #### 4.3.2 表单Hook ```typescript // useForm Hook export function useForm>( initialData: T, options: { resetAfterSubmit?: boolean validateOnSubmit?: boolean } = {} ) { const { resetAfterSubmit = false, validateOnSubmit = true } = options // 表单数据 const formData = ref({ ...initialData }) const formRef = ref() // 表单状态 const state = reactive({ loading: false, errors: {} as Record }) // 重置表单 const resetForm = () => { formData.value = { ...initialData } formRef.value?.resetFields() state.errors = {} } // 验证表单 const validateForm = async (): Promise => { if (!formRef.value) return false try { await formRef.value.validate() state.errors = {} return true } catch (errors) { state.errors = errors as Record return false } } // 提交表单 const submitForm = async ( submitFn: (data: T) => Promise ): Promise => { 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 = (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 权限模型 ```typescript // 权限类型定义 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 权限指令 ```typescript // 权限指令 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 权限组件 ```vue ``` ### 5.2 菜单权限 #### 5.2.1 动态菜单 ```typescript // 菜单配置 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 菜单组件 ```vue ``` ## 6. 性能优化 ### 6.1 代码分割 #### 6.1.1 路由懒加载 ```typescript // 路由配置 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 组件懒加载 ```typescript // 异步组件 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 组件缓存 ```vue ``` #### 6.2.2 数据缓存 ```typescript // 数据缓存Hook export function useCache( key: string, fetcher: () => Promise, options: { ttl?: number // 缓存时间(毫秒) staleWhileRevalidate?: boolean // 后台更新 } = {} ) { const { ttl = 5 * 60 * 1000, staleWhileRevalidate = true } = options const data = ref() const loading = ref(false) const error = ref() 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 => { // 检查缓存 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 虚拟列表组件 ```vue ``` ## 7. 测试策略 ### 7.1 单元测试 #### 7.1.1 组件测试 ```typescript // 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测试 ```typescript // 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 页面测试 ```typescript // 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 用户流程测试 ```typescript // 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配置 ```typescript // 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 环境配置 ```typescript // .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 ```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 # 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 ```yaml # .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 ```yaml # 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 性能指标收集 ```typescript // 性能监控 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 错误监控 ```typescript // 错误监控 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 埋点系统 ```typescript // 埋点系统 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) { 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. **开发效率** - 代码规范 - 自动化测试 - 开发工具链 通过以上架构设计,结伴客管理后台将具备高性能、高可用、易维护的特点,为运营团队提供强大的管理工具,支撑业务的快速发展。