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

2564 lines
56 KiB
Markdown
Raw Normal View 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
```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
// 页面组件示例
<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)
```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<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)
```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<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服务
```typescript
// 用户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
```typescript
// 认证状态管理
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 路由守卫
```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
<!-- 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
```typescript
// 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 动态表单组件
```vue
<!-- 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
```typescript
// 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 权限模型
```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
<!-- 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 动态菜单
```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
<!-- 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 路由懒加载
```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
<!-- 页面缓存 -->
<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 数据缓存
```typescript
// 数据缓存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 虚拟列表组件
```vue
<!-- 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 组件测试
```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<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. **开发效率**
- 代码规范
- 自动化测试
- 开发工具链
通过以上架构设计,解班客管理后台将具备高性能、高可用、易维护的特点,为运营团队提供强大的管理工具,支撑业务的快速发展。