38 KiB
38 KiB
管理后台架构文档
版本历史
| 版本 | 日期 | 作者 | 变更说明 |
|---|---|---|---|
| 1.0 | 2024-01-20 | 前端团队 | 初始版本 |
1. 管理后台架构概述
1.1 项目背景
管理后台是养殖管理平台的Web端管理系统,主要面向系统管理员、运营人员和客服人员,提供用户管理、数据统计、系统配置等管理功能。
1.2 架构目标
- 易用性:直观的操作界面和良好的用户体验
- 功能完整:覆盖所有业务管理需求
- 性能优化:快速的页面加载和响应
- 可维护性:清晰的代码结构和组件化开发
- 扩展性:支持功能模块的快速扩展
- 安全性:完善的权限控制和安全防护
1.3 技术栈
- 前端框架:Vue.js 3.x + Composition API
- 开发语言:TypeScript 5.x
- UI框架:Element Plus 2.x
- 状态管理:Pinia 2.x
- 路由管理:Vue Router 4.x
- 构建工具:Vite 4.x
- HTTP客户端:Axios
- 图表库:ECharts 5.x
- 代码规范:ESLint + Prettier
- CSS预处理:Sass/SCSS
2. 系统架构设计
2.1 整体架构
┌─────────────────────────────────────────────────────────────┐
│ 表现层 (View) │
│ Pages + Components │
├─────────────────────────────────────────────────────────────┤
│ 状态层 (State) │
│ Pinia Stores │
├─────────────────────────────────────────────────────────────┤
│ 服务层 (Service) │
│ API + Utils + Plugins │
├─────────────────────────────────────────────────────────────┤
│ 数据层 (Data) │
│ HTTP + WebSocket + Storage │
└─────────────────────────────────────────────────────────────┘
2.2 目录结构
admin-system/
├── public/ # 静态资源
│ ├── favicon.ico
│ └── index.html
├── src/ # 源代码
│ ├── api/ # API接口
│ │ ├── auth.ts # 认证接口
│ │ ├── user.ts # 用户接口
│ │ ├── farm.ts # 养殖接口
│ │ └── trade.ts # 交易接口
│ ├── assets/ # 静态资源
│ │ ├── images/ # 图片资源
│ │ ├── icons/ # 图标资源
│ │ └── styles/ # 样式文件
│ ├── components/ # 组件
│ │ ├── common/ # 通用组件
│ │ ├── business/ # 业务组件
│ │ └── layout/ # 布局组件
│ ├── composables/ # 组合式函数
│ │ ├── useAuth.ts # 认证逻辑
│ │ ├── useTable.ts # 表格逻辑
│ │ └── useChart.ts # 图表逻辑
│ ├── directives/ # 自定义指令
│ │ ├── permission.ts # 权限指令
│ │ └── loading.ts # 加载指令
│ ├── layouts/ # 布局
│ │ ├── DefaultLayout.vue # 默认布局
│ │ └── AuthLayout.vue # 认证布局
│ ├── pages/ # 页面
│ │ ├── dashboard/ # 仪表板
│ │ ├── user/ # 用户管理
│ │ ├── farm/ # 养殖管理
│ │ ├── trade/ # 交易管理
│ │ └── system/ # 系统管理
│ ├── plugins/ # 插件
│ │ ├── element-plus.ts # Element Plus
│ │ └── echarts.ts # ECharts
│ ├── router/ # 路由
│ │ ├── index.ts # 路由配置
│ │ └── guards.ts # 路由守卫
│ ├── stores/ # 状态管理
│ │ ├── auth.ts # 认证状态
│ │ ├── user.ts # 用户状态
│ │ └── app.ts # 应用状态
│ ├── types/ # 类型定义
│ │ ├── api.ts # API类型
│ │ ├── user.ts # 用户类型
│ │ └── common.ts # 通用类型
│ ├── utils/ # 工具函数
│ │ ├── request.ts # 请求封装
│ │ ├── auth.ts # 认证工具
│ │ ├── validator.ts # 验证工具
│ │ └── formatter.ts # 格式化工具
│ ├── App.vue # 根组件
│ └── main.ts # 应用入口
├── .env # 环境变量
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── vite.config.ts # Vite配置
├── tsconfig.json # TypeScript配置
├── package.json # 项目配置
└── README.md # 项目说明
3. 核心模块设计
3.1 认证授权模块
功能: 用户登录、权限验证、角色管理
核心组件:
- 登录页面: 用户名密码登录
- 权限控制: 基于RBAC的权限控制
- 角色管理: 角色创建、编辑、权限分配
状态管理:
// 认证状态管理
export const useAuthStore = defineStore('auth', () => {
const token = ref<string>('')
const userInfo = ref<UserInfo | null>(null)
const permissions = ref<string[]>([])
// 登录
const login = async (credentials: LoginCredentials) => {
const response = await authApi.login(credentials)
if (response.success) {
token.value = response.data.token
userInfo.value = response.data.user
permissions.value = response.data.permissions
// 保存到本地存储
localStorage.setItem('token', token.value)
localStorage.setItem('userInfo', JSON.stringify(userInfo.value))
}
return response
}
// 登出
const logout = async () => {
await authApi.logout()
token.value = ''
userInfo.value = null
permissions.value = []
// 清除本地存储
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
// 检查权限
const hasPermission = (permission: string): boolean => {
return permissions.value.includes(permission)
}
return {
token,
userInfo,
permissions,
login,
logout,
hasPermission
}
})
3.2 用户管理模块
功能: 用户列表、用户详情、用户操作
核心页面:
- 用户列表: 分页展示用户信息,支持搜索和筛选
- 用户详情: 查看和编辑用户详细信息
- 用户操作: 启用/禁用用户、重置密码等
表格组件:
<template>
<div class="user-management">
<!-- 搜索栏 -->
<el-form :model="searchForm" inline>
<el-form-item label="用户名">
<el-input v-model="searchForm.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态">
<el-option label="全部" value="" />
<el-option label="正常" value="1" />
<el-option label="禁用" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="tableData" v-loading="loading">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '正常' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button size="small" @click="handleView(row)">查看</el-button>
<el-button size="small" type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.limit"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
const userStore = useUserStore()
const loading = ref(false)
const tableData = ref<User[]>([])
const searchForm = reactive({
username: '',
status: ''
})
const pagination = reactive({
page: 1,
limit: 20,
total: 0
})
// 加载用户列表
const loadUsers = async () => {
loading.value = true
try {
const response = await userStore.getUsers({
...searchForm,
page: pagination.page,
limit: pagination.limit
})
if (response.success) {
tableData.value = response.data.items
pagination.total = response.data.total
}
} catch (error) {
ElMessage.error('加载用户列表失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.page = 1
loadUsers()
}
// 重置
const handleReset = () => {
Object.assign(searchForm, {
username: '',
status: ''
})
handleSearch()
}
// 查看用户
const handleView = (user: User) => {
// 跳转到用户详情页
router.push(`/user/detail/${user.id}`)
}
// 编辑用户
const handleEdit = (user: User) => {
// 跳转到用户编辑页
router.push(`/user/edit/${user.id}`)
}
// 删除用户
const handleDelete = async (user: User) => {
try {
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const response = await userStore.deleteUser(user.id)
if (response.success) {
ElMessage.success('删除成功')
loadUsers()
}
} catch (error) {
// 用户取消删除
}
}
onMounted(() => {
loadUsers()
})
</script>
3.3 数据统计模块
功能: 数据可视化、报表生成、趋势分析
核心组件:
- 仪表板: 关键指标展示
- 图表组件: 各类数据图表
- 报表页面: 详细数据报表
图表组件:
<template>
<div class="chart-container">
<div ref="chartRef" :style="{ width: '100%', height: height + 'px' }"></div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
interface Props {
data: any[]
type: 'line' | 'bar' | 'pie'
height?: number
}
const props = withDefaults(defineProps<Props>(), {
height: 400
})
const chartRef = ref<HTMLDivElement>()
let chartInstance: echarts.ECharts | null = null
// 初始化图表
const initChart = () => {
if (!chartRef.value) return
chartInstance = echarts.init(chartRef.value)
updateChart()
}
// 更新图表
const updateChart = () => {
if (!chartInstance) return
const option = getChartOption()
chartInstance.setOption(option)
}
// 获取图表配置
const getChartOption = () => {
switch (props.type) {
case 'line':
return getLineChartOption()
case 'bar':
return getBarChartOption()
case 'pie':
return getPieChartOption()
default:
return {}
}
}
// 折线图配置
const getLineChartOption = () => {
return {
title: {
text: '趋势图'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: props.data.map(item => item.name)
},
yAxis: {
type: 'value'
},
series: [{
data: props.data.map(item => item.value),
type: 'line',
smooth: true
}]
}
}
// 柱状图配置
const getBarChartOption = () => {
return {
title: {
text: '柱状图'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: props.data.map(item => item.name)
},
yAxis: {
type: 'value'
},
series: [{
data: props.data.map(item => item.value),
type: 'bar'
}]
}
}
// 饼图配置
const getPieChartOption = () => {
return {
title: {
text: '饼图'
},
tooltip: {
trigger: 'item'
},
series: [{
type: 'pie',
radius: '50%',
data: props.data,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
}
}
// 监听数据变化
watch(() => props.data, () => {
updateChart()
}, { deep: true })
// 窗口大小变化时重新调整图表
const handleResize = () => {
if (chartInstance) {
chartInstance.resize()
}
}
onMounted(() => {
initChart()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
}
window.removeEventListener('resize', handleResize)
})
</script>
3.4 系统配置模块
功能: 系统参数配置、菜单管理、字典管理
核心页面:
- 系统参数: 系统基础配置
- 菜单管理: 动态菜单配置
- 字典管理: 数据字典维护
4. 路由设计
4.1 路由配置
// 路由配置
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/pages/auth/Login.vue'),
meta: {
title: '登录',
requiresAuth: false
}
},
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/pages/dashboard/Index.vue'),
meta: {
title: '仪表板',
icon: 'dashboard'
}
},
{
path: 'user',
name: 'UserManagement',
redirect: '/user/list',
meta: {
title: '用户管理',
icon: 'user'
},
children: [
{
path: 'list',
name: 'UserList',
component: () => import('@/pages/user/List.vue'),
meta: {
title: '用户列表'
}
},
{
path: 'detail/:id',
name: 'UserDetail',
component: () => import('@/pages/user/Detail.vue'),
meta: {
title: '用户详情',
hidden: true
}
}
]
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
4.2 路由守卫
// 路由守卫
import { useAuthStore } from '@/stores/auth'
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - 养殖管理平台`
}
// 检查是否需要认证
if (to.meta.requiresAuth !== false) {
if (!authStore.token) {
next('/login')
return
}
// 检查权限
if (to.meta.permission && !authStore.hasPermission(to.meta.permission)) {
ElMessage.error('权限不足')
next('/403')
return
}
}
next()
})
5. 状态管理
5.1 应用状态
// 应用状态管理
export const useAppStore = defineStore('app', () => {
const sidebar = ref({
opened: true,
withoutAnimation: false
})
const device = ref('desktop')
const size = ref('default')
// 切换侧边栏
const toggleSidebar = () => {
sidebar.value.opened = !sidebar.value.opened
sidebar.value.withoutAnimation = false
}
// 关闭侧边栏
const closeSidebar = (withoutAnimation: boolean) => {
sidebar.value.opened = false
sidebar.value.withoutAnimation = withoutAnimation
}
// 设置设备类型
const setDevice = (deviceType: string) => {
device.value = deviceType
}
// 设置组件大小
const setSize = (sizeType: string) => {
size.value = sizeType
}
return {
sidebar,
device,
size,
toggleSidebar,
closeSidebar,
setDevice,
setSize
}
})
5.2 用户状态
// 用户状态管理
export const useUserStore = defineStore('user', () => {
const users = ref<User[]>([])
const currentUser = ref<User | null>(null)
// 获取用户列表
const getUsers = async (params: UserListParams) => {
const response = await userApi.getUsers(params)
if (response.success) {
users.value = response.data.items
}
return response
}
// 获取用户详情
const getUserDetail = async (id: string) => {
const response = await userApi.getUserDetail(id)
if (response.success) {
currentUser.value = response.data
}
return response
}
// 创建用户
const createUser = async (userData: CreateUserData) => {
const response = await userApi.createUser(userData)
if (response.success) {
users.value.push(response.data)
}
return response
}
// 更新用户
const updateUser = async (id: string, userData: UpdateUserData) => {
const response = await userApi.updateUser(id, userData)
if (response.success) {
const index = users.value.findIndex(user => user.id === id)
if (index !== -1) {
users.value[index] = { ...users.value[index], ...response.data }
}
}
return response
}
// 删除用户
const deleteUser = async (id: string) => {
const response = await userApi.deleteUser(id)
if (response.success) {
const index = users.value.findIndex(user => user.id === id)
if (index !== -1) {
users.value.splice(index, 1)
}
}
return response
}
return {
users,
currentUser,
getUsers,
getUserDetail,
createUser,
updateUser,
deleteUser
}
})
6. 网络层设计
6.1 HTTP请求封装
// HTTP请求封装
import axios from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
const authStore = useAuthStore()
// 添加认证token
if (authStore.token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${authStore.token}`
}
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response
// 统一处理响应
if (data.success) {
return data
} else {
ElMessage.error(data.message || '请求失败')
return Promise.reject(data)
}
},
(error) => {
const { response } = error
if (response) {
switch (response.status) {
case 401:
ElMessage.error('登录已过期,请重新登录')
const authStore = useAuthStore()
authStore.logout()
router.push('/login')
break
case 403:
ElMessage.error('权限不足')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
default:
ElMessage.error('网络错误')
}
} else {
ElMessage.error('网络连接失败')
}
return Promise.reject(error)
}
)
export default service
6.2 API接口定义
// API接口定义
import request from '@/utils/request'
import type {
LoginCredentials,
LoginResponse,
UserListParams,
UserListResponse,
CreateUserData,
UpdateUserData
} from '@/types/api'
// 认证相关接口
export const authApi = {
// 登录
login: (data: LoginCredentials): Promise<LoginResponse> => {
return request.post('/auth/login', data)
},
// 登出
logout: (): Promise<void> => {
return request.post('/auth/logout')
},
// 获取用户信息
getUserInfo: (): Promise<UserInfoResponse> => {
return request.get('/auth/user-info')
}
}
// 用户相关接口
export const userApi = {
// 获取用户列表
getUsers: (params: UserListParams): Promise<UserListResponse> => {
return request.get('/users', { params })
},
// 获取用户详情
getUserDetail: (id: string): Promise<UserDetailResponse> => {
return request.get(`/users/${id}`)
},
// 创建用户
createUser: (data: CreateUserData): Promise<UserResponse> => {
return request.post('/users', data)
},
// 更新用户
updateUser: (id: string, data: UpdateUserData): Promise<UserResponse> => {
return request.put(`/users/${id}`, data)
},
// 删除用户
deleteUser: (id: string): Promise<void> => {
return request.delete(`/users/${id}`)
}
}
7. 组件化设计
7.1 通用组件
<!-- 表格组件 -->
<template>
<div class="data-table">
<el-table
:data="data"
:loading="loading"
v-bind="$attrs"
@selection-change="handleSelectionChange"
>
<slot />
</el-table>
<el-pagination
v-if="showPagination"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="pageSizes"
:layout="paginationLayout"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup lang="ts">
interface Props {
data: any[]
loading?: boolean
showPagination?: boolean
total?: number
pageSizes?: number[]
paginationLayout?: string
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
showPagination: true,
total: 0,
pageSizes: () => [10, 20, 50, 100],
paginationLayout: 'total, sizes, prev, pager, next, jumper'
})
const emit = defineEmits<{
selectionChange: [selection: any[]]
sizeChange: [size: number]
currentChange: [current: number]
}>()
const currentPage = ref(1)
const pageSize = ref(20)
const handleSelectionChange = (selection: any[]) => {
emit('selectionChange', selection)
}
const handleSizeChange = (size: number) => {
pageSize.value = size
emit('sizeChange', size)
}
const handleCurrentChange = (current: number) => {
currentPage.value = current
emit('currentChange', current)
}
</script>
7.2 业务组件
<!-- 用户选择器组件 -->
<template>
<el-select
v-model="selectedValue"
:placeholder="placeholder"
:multiple="multiple"
:filterable="filterable"
:remote="remote"
:remote-method="remoteMethod"
:loading="loading"
@change="handleChange"
>
<el-option
v-for="user in users"
:key="user.id"
:label="user.username"
:value="user.id"
>
<span>{{ user.username }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">
{{ user.phone }}
</span>
</el-option>
</el-select>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { userApi } from '@/api/user'
interface Props {
modelValue: string | string[]
placeholder?: string
multiple?: boolean
filterable?: boolean
remote?: boolean
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择用户',
multiple: false,
filterable: true,
remote: true
})
const emit = defineEmits<{
'update:modelValue': [value: string | string[]]
change: [value: string | string[]]
}>()
const selectedValue = ref(props.modelValue)
const users = ref<User[]>([])
const loading = ref(false)
// 远程搜索用户
const remoteMethod = async (query: string) => {
if (!query) {
users.value = []
return
}
loading.value = true
try {
const response = await userApi.searchUsers({ keyword: query })
if (response.success) {
users.value = response.data
}
} catch (error) {
console.error('搜索用户失败:', error)
} finally {
loading.value = false
}
}
// 处理选择变化
const handleChange = (value: string | string[]) => {
emit('update:modelValue', value)
emit('change', value)
}
// 监听外部值变化
watch(() => props.modelValue, (newValue) => {
selectedValue.value = newValue
})
onMounted(() => {
if (!props.remote) {
// 非远程模式,加载所有用户
remoteMethod('')
}
})
</script>
8. 性能优化
8.1 代码分割
// 路由懒加载
const routes = [
{
path: '/user',
component: () => import('@/pages/user/Index.vue')
},
{
path: '/farm',
component: () => import('@/pages/farm/Index.vue')
}
]
// 组件懒加载
const LazyComponent = defineAsyncComponent(() => import('@/components/HeavyComponent.vue'))
8.2 虚拟滚动
<!-- 虚拟列表组件 -->
<template>
<div class="virtual-list" :style="{ height: height + 'px' }">
<div class="virtual-list-phantom" :style="{ height: totalHeight + 'px' }"></div>
<div class="virtual-list-content" :style="{ transform: `translateY(${offset}px)` }">
<div
v-for="item in visibleItems"
:key="item.id"
class="virtual-list-item"
:style="{ height: itemHeight + 'px' }"
>
<slot :item="item" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Props {
items: any[]
itemHeight: number
height: number
}
const props = defineProps<Props>()
const scrollTop = ref(0)
const visibleCount = computed(() => Math.ceil(props.height / props.itemHeight))
const totalHeight = computed(() => props.items.length * props.itemHeight)
const startIndex = computed(() => Math.floor(scrollTop.value / props.itemHeight))
const endIndex = computed(() => Math.min(startIndex.value + visibleCount.value, props.items.length))
const visibleItems = computed(() => props.items.slice(startIndex.value, endIndex.value))
const offset = computed(() => startIndex.value * props.itemHeight)
const handleScroll = (e: Event) => {
scrollTop.value = (e.target as HTMLElement).scrollTop
}
</script>
8.3 缓存策略
// 请求缓存
class RequestCache {
private cache = new Map<string, { data: any; timestamp: number }>()
private ttl = 5 * 60 * 1000 // 5分钟
get(key: string): any {
const item = this.cache.get(key)
if (!item) return null
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key)
return null
}
return item.data
}
set(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now()
})
}
clear(): void {
this.cache.clear()
}
}
// 使用缓存的API请求
const requestWithCache = async (url: string, options?: any) => {
const cacheKey = `${url}${JSON.stringify(options)}`
// 尝试从缓存获取
const cachedData = requestCache.get(cacheKey)
if (cachedData) {
return cachedData
}
// 发起请求
const response = await request(url, options)
// 缓存响应数据
requestCache.set(cacheKey, response)
return response
}
9. 安全设计
9.1 XSS防护
// XSS防护工具
export const xssFilter = {
// 转义HTML特殊字符
escapeHtml: (str: string): string => {
const map: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
return str.replace(/[&<>"']/g, (match) => map[match])
},
// 过滤危险标签
filterTags: (str: string): string => {
return str.replace(/<script[^>]*>.*?<\/script>/gi, '')
.replace(/<iframe[^>]*>.*?<\/iframe>/gi, '')
.replace(/javascript:/gi, '')
}
}
// 在组件中使用
const safeContent = computed(() => {
return xssFilter.escapeHtml(props.content)
})
9.2 CSRF防护
// CSRF Token处理
const csrfToken = ref('')
// 获取CSRF Token
const getCsrfToken = async () => {
const response = await request.get('/csrf-token')
csrfToken.value = response.data.token
}
// 在请求中添加CSRF Token
service.interceptors.request.use((config) => {
if (['post', 'put', 'delete'].includes(config.method?.toLowerCase() || '')) {
config.headers['X-CSRF-Token'] = csrfToken.value
}
return config
})
9.3 权限控制
// 权限指令
const permissionDirective = {
mounted(el: HTMLElement, binding: any) {
const { value } = binding
const authStore = useAuthStore()
if (!authStore.hasPermission(value)) {
el.style.display = 'none'
}
},
updated(el: HTMLElement, binding: any) {
const { value } = binding
const authStore = useAuthStore()
if (!authStore.hasPermission(value)) {
el.style.display = 'none'
} else {
el.style.display = ''
}
}
}
// 注册指令
app.directive('permission', permissionDirective)
// 在模板中使用
// <el-button v-permission="'user:delete'">删除</el-button>
10. 测试策略
10.1 单元测试
// 组件测试
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import UserList from '@/pages/user/List.vue'
describe('UserList', () => {
it('renders user list correctly', () => {
const wrapper = mount(UserList, {
props: {
users: [
{ id: 1, username: 'test1', email: 'test1@example.com' },
{ id: 2, username: 'test2', email: 'test2@example.com' }
]
}
})
expect(wrapper.find('.user-list').exists()).toBe(true)
expect(wrapper.findAll('.user-item')).toHaveLength(2)
})
it('emits edit event when edit button clicked', async () => {
const wrapper = mount(UserList)
await wrapper.find('.edit-btn').trigger('click')
expect(wrapper.emitted('edit')).toBeTruthy()
})
})
10.2 集成测试
// API测试
import { describe, it, expect, beforeEach } from 'vitest'
import { userApi } from '@/api/user'
describe('User API', () => {
beforeEach(() => {
// 设置测试环境
})
it('should get user list', async () => {
const response = await userApi.getUsers({ page: 1, limit: 10 })
expect(response.success).toBe(true)
expect(response.data.items).toBeInstanceOf(Array)
})
it('should create user', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
}
const response = await userApi.createUser(userData)
expect(response.success).toBe(true)
expect(response.data.username).toBe(userData.username)
})
})
11. 构建与部署
11.1 构建配置
// 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]'
}
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
11.2 Docker部署
# Dockerfile
FROM node:18-alpine as builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
11.3 CI/CD配置
# .gitlab-ci.yml
stages:
- test
- build
- deploy
test:
stage: test
script:
- npm ci
- npm run test
- npm run lint
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
deploy:
stage: deploy
script:
- docker build -t admin-system .
- docker push $CI_REGISTRY_IMAGE
- kubectl apply -f k8s/
12. 监控与运维
12.1 性能监控
// 性能监控
class PerformanceMonitor {
// 监控页面加载时间
monitorPageLoad() {
window.addEventListener('load', () => {
const timing = performance.timing
const loadTime = timing.loadEventEnd - timing.navigationStart
this.reportMetric('page_load_time', loadTime)
})
}
// 监控API响应时间
monitorApiResponse(url: string, startTime: number, endTime: number) {
const responseTime = endTime - startTime
this.reportMetric('api_response_time', responseTime, { url })
}
// 上报指标
reportMetric(name: string, value: number, tags?: Record<string, any>) {
// 发送到监控系统
fetch('/api/metrics', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
value,
tags,
timestamp: Date.now()
})
})
}
}
12.2 错误监控
// 错误监控
class ErrorMonitor {
constructor() {
this.setupGlobalErrorHandler()
this.setupUnhandledRejectionHandler()
this.setupVueErrorHandler()
}
// 全局错误处理
setupGlobalErrorHandler() {
window.addEventListener('error', (event) => {
this.reportError({
type: 'javascript',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack
})
})
}
// Promise错误处理
setupUnhandledRejectionHandler() {
window.addEventListener('unhandledrejection', (event) => {
this.reportError({
type: 'promise',
message: event.reason?.message || 'Unhandled Promise Rejection',
stack: event.reason?.stack
})
})
}
// Vue错误处理
setupVueErrorHandler() {
app.config.errorHandler = (err, vm, info) => {
this.reportError({
type: 'vue',
message: err.message,
stack: err.stack,
componentName: vm?.$options.name,
errorInfo: info
})
}
}
// 上报错误
reportError(error: any) {
fetch('/api/errors', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...error,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
})
})
}
}
13. 扩展性设计
13.1 插件系统
// 插件系统
interface Plugin {
name: string
version: string
install: (app: App) => void
}
class PluginManager {
private plugins: Map<string, Plugin> = new Map()
register(plugin: Plugin) {
this.plugins.set(plugin.name, plugin)
}
install(app: App, pluginName: string) {
const plugin = this.plugins.get(pluginName)
if (plugin) {
plugin.install(app)
}
}
installAll(app: App) {
this.plugins.forEach(plugin => {
plugin.install(app)
})
}
}
// 使用插件
const pluginManager = new PluginManager()
pluginManager.register({
name: 'chart',
version: '1.0.0',
install: (app) => {
app.component('Chart', ChartComponent)
}
})
13.2 主题系统
// 主题系统
interface Theme {
name: string
colors: Record<string, string>
fonts: Record<string, string>
}
class ThemeManager {
private themes: Map<string, Theme> = new Map()
private currentTheme = ref<string>('default')
register(theme: Theme) {
this.themes.set(theme.name, theme)
}
setTheme(themeName: string) {
const theme = this.themes.get(themeName)
if (theme) {
this.currentTheme.value = themeName
this.applyTheme(theme)
}
}
private applyTheme(theme: Theme) {
const root = document.documentElement
Object.entries(theme.colors).forEach(([key, value]) => {
root.style.setProperty(`--color-${key}`, value)
})
Object.entries(theme.fonts).forEach(([key, value]) => {
root.style.setProperty(`--font-${key}`, value)
})
}
}
14. 未来规划
14.1 技术升级
- Vue 3.4+: 升级到最新版本Vue.js
- Vite 5.x: 升级构建工具
- TypeScript 5.x: 使用最新TypeScript特性
- 微前端: 考虑微前端架构
14.2 功能扩展
- 国际化: 支持多语言
- PWA: 渐进式Web应用
- 离线功能: 支持离线操作
- 实时通信: WebSocket实时更新