引言
随着前端技术的快速发展,Vue 3的Composition API为开发者提供了更加灵活和强大的组件开发方式。本文将通过一个完整的后台管理系统项目,深入探讨如何利用Vue 3的Composition API、TypeScript以及Element Plus构建现代化的企业级后台管理系统。
项目概述
项目背景
企业后台管理系统通常需要处理复杂的业务逻辑、数据管理、权限控制等需求。传统的Vue 2选项式API在处理复杂组件时存在代码分散、难以维护等问题。Vue 3的Composition API通过函数式编程的方式,让开发者能够更好地组织和复用代码。
技术栈选择
- Vue 3: 最新的Vue版本,提供更好的性能和开发体验
- Composition API: 组件逻辑复用的核心技术
- TypeScript: 提供类型安全和更好的开发体验
- Element Plus: 基于Vue 3的UI组件库
- Vue Router: 路由管理
- Vuex 4: 状态管理
环境搭建与项目初始化
项目初始化
# 使用Vite创建Vue 3项目
npm create vite@latest admin-system --template vue-ts
cd admin-system
npm install
依赖安装
npm install element-plus vue-router vuex@4 @types/node
项目结构设计
src/
├── assets/ # 静态资源
├── components/ # 公共组件
├── views/ # 页面组件
├── router/ # 路由配置
├── store/ # 状态管理
├── services/ # API服务
├── utils/ # 工具函数
├── types/ # 类型定义
├── styles/ # 样式文件
└── App.vue # 根组件
路由系统设计
路由配置基础结构
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/store/user'
// 路由元信息类型定义
interface RouteMeta {
title: string
icon?: string
requiresAuth?: boolean
permission?: string[]
}
// 路由类型定义
type RouteRecordRawWithMeta = RouteRecordRaw & {
meta: RouteMeta
}
// 路由配置
const routes: RouteRecordRawWithMeta[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录', requiresAuth: false }
},
{
path: '/',
name: 'Layout',
component: () => import('@/layouts/Layout.vue'),
redirect: '/dashboard',
meta: { title: '首页', requiresAuth: true },
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '仪表盘', icon: 'el-icon-home' }
},
{
path: 'users',
name: 'Users',
component: () => import('@/views/users/Users.vue'),
meta: { title: '用户管理', icon: 'el-icon-user' }
}
]
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.token) {
next({ name: 'Login' })
} else if (to.meta.permission && !userStore.hasPermission(to.meta.permission)) {
next({ name: 'Dashboard' })
} else {
next()
}
})
export default router
状态管理设计
用户状态管理
// src/store/user.ts
import { defineStore } from 'vuex'
import { User } from '@/types/user'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null as User | null,
permissions: [] as string[]
}),
getters: {
isAuthenticated: (state) => !!state.token,
hasPermission: (state) => (permission: string) => {
return state.permissions.includes(permission)
}
},
actions: {
setToken(token: string) {
this.token = token
localStorage.setItem('token', token)
},
setUserInfo(userInfo: User) {
this.userInfo = userInfo
},
setPermissions(permissions: string[]) {
this.permissions = permissions
},
logout() {
this.token = ''
this.userInfo = null
this.permissions = []
localStorage.removeItem('token')
}
}
})
Composition API核心实践
用户管理逻辑封装
// src/composables/useUser.ts
import { ref, reactive } from 'vue'
import { User, UserForm } from '@/types/user'
import { useUserStore } from '@/store/user'
import { userService } from '@/services/user'
export function useUser() {
const userStore = useUserStore()
const loading = ref(false)
const users = ref<User[]>([])
const userForm = reactive<UserForm>({
name: '',
email: '',
phone: '',
role: ''
})
const dialogVisible = ref(false)
const currentUserId = ref<number | null>(null)
// 获取用户列表
const fetchUsers = async () => {
loading.value = true
try {
const response = await userService.getUsers()
users.value = response.data
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 创建用户
const createUser = async (formData: UserForm) => {
try {
await userService.createUser(formData)
await fetchUsers()
dialogVisible.value = false
} catch (error) {
console.error('创建用户失败:', error)
throw error
}
}
// 更新用户
const updateUser = async (id: number, formData: UserForm) => {
try {
await userService.updateUser(id, formData)
await fetchUsers()
dialogVisible.value = false
} catch (error) {
console.error('更新用户失败:', error)
throw error
}
}
// 删除用户
const deleteUser = async (id: number) => {
try {
await userService.deleteUser(id)
await fetchUsers()
} catch (error) {
console.error('删除用户失败:', error)
throw error
}
}
// 打开编辑对话框
const openEditDialog = (user: User) => {
currentUserId.value = user.id
Object.assign(userForm, user)
dialogVisible.value = true
}
// 打开创建对话框
const openCreateDialog = () => {
currentUserId.value = null
Object.assign(userForm, {
name: '',
email: '',
phone: '',
role: ''
})
dialogVisible.value = true
}
// 提交表单
const handleSubmit = async () => {
if (currentUserId.value) {
await updateUser(currentUserId.value, userForm)
} else {
await createUser(userForm)
}
}
return {
loading,
users,
userForm,
dialogVisible,
fetchUsers,
createUser,
updateUser,
deleteUser,
openEditDialog,
openCreateDialog,
handleSubmit
}
}
表格组件封装
// src/components/Table.vue
<template>
<el-table
:data="tableData"
:loading="loading"
border
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column>
<el-table-column prop="phone" label="电话"></el-table-column>
<el-table-column prop="role" label="角色"></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { User } from '@/types/user'
interface TableProps {
data: User[]
loading: boolean
total: number
currentPage: number
pageSize: number
}
interface TableEmits {
(e: 'edit', row: User): void
(e: 'delete', row: User): void
(e: 'size-change', size: number): void
(e: 'current-change', page: number): void
}
const props = withDefaults(defineProps<TableProps>(), {
data: () => [],
loading: false,
total: 0,
currentPage: 1,
pageSize: 10
})
const emit = defineEmits<TableEmits>()
const handleEdit = (row: User) => {
emit('edit', row)
}
const handleDelete = (row: User) => {
emit('delete', row)
}
const handleSizeChange = (size: number) => {
emit('size-change', size)
}
const handleCurrentChange = (page: number) => {
emit('current-change', page)
}
const tableData = ref<User[]>(props.data)
watch(() => props.data, (newData) => {
tableData.value = newData
})
</script>
权限控制实现
权限指令封装
// src/directives/permission.ts
import { DirectiveBinding } from 'vue'
import { useUserStore } from '@/store/user'
export default {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const userStore = useUserStore()
const permission = binding.value
if (!userStore.hasPermission(permission)) {
el.style.display = 'none'
}
},
updated(el: HTMLElement, binding: DirectiveBinding) {
const userStore = useUserStore()
const permission = binding.value
if (!userStore.hasPermission(permission)) {
el.style.display = 'none'
} else {
el.style.display = ''
}
}
}
权限菜单渲染
// src/components/SidebarMenu.vue
<template>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
router
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<template v-for="route in filteredRoutes" :key="route.path">
<el-submenu v-if="route.children && route.children.length > 0" :index="route.path">
<template #title>
<i :class="route.meta?.icon"></i>
<span>{{ route.meta?.title }}</span>
</template>
<el-menu-item
v-for="child in route.children"
:key="child.path"
:index="child.path"
>
{{ child.meta?.title }}
</el-menu-item>
</el-submenu>
<el-menu-item v-else :index="route.path">
<i :class="route.meta?.icon"></i>
<template #title>{{ route.meta?.title }}</template>
</el-menu-item>
</template>
</el-menu>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useUserStore } from '@/store/user'
const route = useRoute()
const userStore = useUserStore()
// 过滤有权限的路由
const filteredRoutes = computed(() => {
const routes = []
// 这里可以根据实际的路由配置进行过滤
// 简化示例
return routes
})
const activeMenu = computed(() => {
return route.path
})
const isCollapse = computed(() => {
return false // 可以根据实际需求调整
})
</script>
API服务封装
通用API服务
// src/services/api.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/store/user'
// 创建axios实例
const apiClient: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
apiClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${userStore.token}`
}
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
apiClient.interceptors.response.use(
(response: AxiosResponse) => {
return response.data
},
(error) => {
if (error.response?.status === 401) {
const userStore = useUserStore()
userStore.logout()
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default apiClient
用户服务实现
// src/services/user.ts
import apiClient from './api'
import { User, UserForm } from '@/types/user'
export const userService = {
// 获取用户列表
getUsers(params?: any) {
return apiClient.get('/users', { params })
},
// 获取用户详情
getUserById(id: number) {
return apiClient.get(`/users/${id}`)
},
// 创建用户
createUser(data: UserForm) {
return apiClient.post('/users', data)
},
// 更新用户
updateUser(id: number, data: UserForm) {
return apiClient.put(`/users/${id}`, data)
},
// 删除用户
deleteUser(id: number) {
return apiClient.delete(`/users/${id}`)
},
// 用户登录
login(credentials: { email: string; password: string }) {
return apiClient.post('/auth/login', credentials)
}
}
类型定义设计
用户类型定义
// src/types/user.ts
export interface User {
id: number
name: string
email: string
phone: string
role: string
createdAt: string
updatedAt: string
}
export interface UserForm {
name: string
email: string
phone: string
role: string
}
export interface LoginCredentials {
email: string
password: string
}
export interface LoginResponse {
token: string
user: User
}
响应类型定义
// src/types/response.ts
export interface ApiResponse<T> {
code: number
message: string
data: T
timestamp: number
}
export interface PaginatedResponse<T> {
data: T[]
total: number
page: number
pageSize: number
}
组件化开发实践
表单组件封装
// src/components/UserForm.vue
<template>
<el-form
:model="form"
:rules="rules"
ref="formRef"
label-width="100px"
>
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入姓名"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱"></el-input>
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入电话"></el-input>
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="form.role" placeholder="请选择角色">
<el-option label="管理员" value="admin"></el-option>
<el-option label="普通用户" value="user"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { UserForm } from '@/types/user'
interface FormEmits {
(e: 'submit', data: UserForm): void
(e: 'reset'): void
}
const emit = defineEmits<FormEmits>()
const formRef = ref()
const form = reactive<UserForm>({
name: '',
email: '',
phone: '',
role: ''
})
const rules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入电话', trigger: 'blur' }
]
}
const submitForm = () => {
formRef.value.validate((valid: boolean) => {
if (valid) {
emit('submit', form)
}
})
}
const resetForm = () => {
formRef.value.resetFields()
emit('reset')
}
</script>
页面组件实现
// src/views/users/Users.vue
<template>
<div class="user-page">
<el-card class="box-card">
<div class="header">
<h2>用户管理</h2>
<el-button type="primary" @click="openCreateDialog">
新增用户
</el-button>
</div>
<el-table
:data="users"
:loading="loading"
border
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column>
<el-table-column prop="phone" label="电话"></el-table-column>
<el-table-column prop="role" label="角色"></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="openEditDialog(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</el-card>
<el-dialog
v-model="dialogVisible"
:title="currentUserId ? '编辑用户' : '新增用户'"
width="500px"
>
<UserForm
@submit="handleSubmit"
@reset="dialogVisible = false"
/>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUser } from '@/composables/useUser'
const {
loading,
users,
dialogVisible,
fetchUsers,
openEditDialog,
openCreateDialog,
handleSubmit
} = useUser()
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)
onMounted(() => {
fetchUsers()
})
const handleSizeChange = (size: number) => {
pageSize.value = size
fetchUsers()
}
const handleCurrentChange = (page: number) => {
currentPage.value = page
fetchUsers()
}
</script>
<style scoped>
.user-page {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
</style>
性能优化策略
组件懒加载
// src/router/index.ts
const routes: RouteRecordRawWithMeta[] = [
{
path: '/users',
name: 'Users',
component: () => import('@/views/users/Users.vue'),
meta: { title: '用户管理', icon: 'el-icon-user' }
},
{
path: '/products',
name: 'Products',
component: () => import('@/views/products/Products.vue'),
meta: { title: '产品管理', icon: 'el-icon-goods' }
}
]
数据缓存机制
// src/composables/useCache.ts
import { ref, watch } from 'vue'
export function useCache<T>(key: string, initialValue: T) {
const cachedValue = ref<T>(initialValue)
// 从localStorage获取缓存
const loadFromCache = () => {
const cached = localStorage.getItem(key)
if (cached) {
try {
cachedValue.value = JSON.parse(cached)
} catch (error) {
console.error('缓存解析失败:', error)
}
}
}
// 保存到localStorage
const saveToCache = (value: T) => {
cachedValue.value = value
localStorage.setItem(key, JSON.stringify(value))
}
// 监听值变化并保存
watch(cachedValue, (newValue) => {
saveToCache(newValue)
}, { deep: true })
loadFromCache()
return {
value: cachedValue,
save: saveToCache,
load: loadFromCache
}
}
错误处理与日志记录
全局错误处理
// src/utils/errorHandler.ts
import { ElMessage, ElMessageBox } from 'element-plus'
export function handleError(error: any, context?: string) {
console.error('错误发生:', { error, context })
if (error.response) {
// 服务器响应错误
const { status, data } = error.response
switch (status) {
case 401:
ElMessage.error('登录已过期,请重新登录')
// 跳转到登录页
window.location.href = '/login'
break
case 403:
ElMessage.error('权限不足')
break
case 404:
ElMessage.error('请求的资源不存在')
break
default:
ElMessage.error(data.message || '请求失败')
}
} else if (error.request) {
// 网络错误
ElMessage.error('网络连接失败,请检查网络')
} else {
// 其他错误
ElMessage.error(error.message || '未知错误')
}
// 发送错误到监控系统(可选)
// sendErrorToMonitoring(error, context)
}
请求拦截器增强
// src/services/api.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { handleError } from '@/utils/errorHandler'
// 添加请求拦截器
apiClient.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 添加请求时间戳
config.params = {
...config.params,
_t: Date.now()
}
// 添加请求标识
const requestId = Math.random().toString(36).substr(2, 9)
config.headers = {
...config.headers,
'X-Request-ID': requestId
}
return config
},
(error) => {
handleError(error, '请求拦截器错误')
return Promise.reject(error)
}
)
// 添加响应拦截器
apiClient.interceptors.response.use(
(response: AxiosResponse) => {
// 可以在这里处理统一的响应格式
return response.data
},
(error) => {
handleError(error, '响应拦截器错误')
return Promise.reject(error)
}
)
测试策略
单元测试示例
// src/composables/__tests__/useUser.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { useUser } from '../useUser'
describe('useUser composable', () => {
it('should initialize with correct default values', () => {
const { loading, users, dialogVisible } = useUser()
expect(loading.value).toBe(false)
expect(users.value).toEqual([])
expect(dialogVisible.value).toBe(false)
})
it('should handle loading state correctly', async () => {
const { loading, fetchUsers } = useUser()
expect(loading.value).toBe(false)
// 模拟异步操作
const mockUsers = [{ id: 1, name: 'Test User' }]
// 这里可以添加mock的实现来测试loading状态
// 由于是组合式API,需要更复杂的测试环境
})
})
部署与构建优化
构建配置
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue(),
vueJsx()
],
resolve: {
alias: {
'@': resolve(__dirname, './src')
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'vuex', 'element-plus'],
api: ['axios'],
utils: ['lodash',
评论 (0)