摘要:Vue3作为前端主流框架,结合Vite的快速构建能力与Pinia的状态管理优势,已成为企业级后台管理系统的首选技术栈。本文基于Vue3+Vite+Pinia+Element Plus,实现一个完整的企业级后台管理系统,包含用户登录、权限控制、数据表格、表单提交、图表展示等核心功能,附完整源码与部署教程,兼顾美观与实用性,适合前端开发者快速上手Vue3项目。
一、前言:Vue3技术栈的优势与项目选型
传统Vue2项目面临构建速度慢、状态管理繁琐(Vuex)、TypeScript支持不佳等问题,而Vue3+Vite+Pinia技术栈完美解决了这些痛点:Vite基于ESModule实现快速热更新,构建速度比Webpack快5-10倍;Pinia替代Vuex,API更简洁、支持TypeScript、无需 mutations,状态管理更高效;Element Plus作为Vue3生态的UI组件库,组件丰富、适配Vue3,可快速搭建美观的后台界面。
本文将实现一个企业级后台管理系统(用户管理、角色管理、菜单管理、数据统计),覆盖后台系统的核心场景,同时讲解Vue3的Composition API、自定义Hook、路由守卫、权限控制等核心知识点,帮助开发者快速掌握Vue3实战技巧。
二、环境搭建:Vue3+Vite+Pinia项目初始化
本次实战基于Node.js 18、Vue3.4、Vite5.0、Pinia 2.1、Element Plus 2.7,建议使用npm或pnpm初始化项目,避免版本冲突。
bash
# 1. 初始化Vite+Vue3项目
npm create vite@latest admin-system -- --template vue-ts
# 2. 进入项目目录
cd admin-system
# 3. 安装核心依赖
npm install pinia element-plus @element-plus/icons-vue axios vue-router
# 4. 启动开发服务器
npm run dev
项目目录结构设计(规范清晰,便于维护):
text
admin-system/
├── src/
│ ├── api/ # 接口请求封装
│ ├── assets/ # 静态资源(图片、样式)
│ ├── components/ # 公共组件
│ ├── hooks/ # 自定义Hook
│ ├── router/ # 路由配置
│ ├── store/ # Pinia状态管理
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ ├── main.ts # 入口文件
│ └── vite-env.d.ts # 类型声明
├── vite.config.ts # Vite配置
└── package.json # 依赖配置
三、核心功能实现(分模块讲解+源码)
3.1 路由配置与权限控制
核心逻辑:使用vue-router实现路由配置,通过路由守卫判断用户是否登录、是否拥有对应权限,未登录用户跳转至登录页,无权限用户跳转至403页面。
typescript
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/store/userStore'
// 公开路由(无需登录)
const publicRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue')
},
{
path: '/403',
name: 'Forbidden',
component: () => import('@/views/403.vue')
}
]
// 私有路由(需要登录+权限)
const privateRoutes: RouteRecordRaw[] = [
{
path: '/',
name: 'Layout',
component: () => import('@/components/Layout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '数据面板', permission: ['admin', 'user'] }
},
{
path: 'user',
name: 'User',
component: () => import('@/views/User.vue'),
meta: { title: '用户管理', permission: ['admin'] }
},
{
path: 'role',
name: 'Role',
component: () => import('@/views/Role.vue'),
meta: { title: '角色管理', permission: ['admin'] }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes: [...publicRoutes, ...privateRoutes]
})
// 路由守卫(权限控制)
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
const token = userStore.token
// 未登录用户,只能访问公开路由
if (!token && to.name !== 'Login') {
next({ name: 'Login' })
return
}
// 已登录用户,禁止访问登录页
if (token && to.name === 'Login') {
next({ name: 'Dashboard' })
return
}
// 权限判断(私有路由需要对应权限)
if (to.meta.permission && token) {
const permission = to.meta.permission as string[]
if (!permission.includes(userStore.role)) {
next({ name: 'Forbidden' })
return
}
}
next()
})
export default router
3.2 Pinia状态管理(用户+权限)
核心逻辑:使用Pinia管理用户状态(token、用户名、角色),实现登录、登出、状态持久化(localStorage),避免页面刷新后状态丢失。
typescript
// src/store/userStore.ts
import { defineStore } from 'pinia'
import { loginApi, logoutApi } from '@/api/userApi'
interface UserState {
token: string | null
username: string | null
role: string | null // admin/user
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
token: localStorage.getItem('token'),
username: localStorage.getItem('username'),
role: localStorage.getItem('role')
}),
actions: {
// 登录
async login(data: { username: string; password: string }) {
const res = await loginApi(data)
const { token, username, role } = res.data
// 存储状态
this.token = token
this.username = username
this.role = role
// 持久化到localStorage
localStorage.setItem('token', token)
localStorage.setItem('username', username)
localStorage.setItem('role', role)
return res
},
// 登出
async logout() {
await logoutApi()
// 清空状态
this.token = null
this.username = null
this.role = null
// 清空localStorage
localStorage.removeItem('token')
localStorage.removeItem('username')
localStorage.removeItem('role')
}
}
})
3.3 接口请求封装(Axios)
核心逻辑:封装Axios,统一处理请求拦截(添加token)、响应拦截(处理错误、token过期),简化接口调用,提升代码复用性。
typescript
// src/utils/request.ts
import axios from 'axios'
import { useUserStore } from '@/store/userStore'
import router from '@/router'
// 创建Axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量配置
timeout: 5000
})
// 请求拦截器(添加token)
request.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器(处理错误)
request.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
// token过期,清空状态并跳转登录页
if (error.response?.status === 401) {
const userStore = useUserStore()
userStore.logout()
router.push({ name: 'Login' })
}
return Promise.reject(error)
}
)
export default request
3.4 核心页面实现(用户管理+数据面板)
用户管理页面:使用Element Plus的Table组件展示用户列表,实现分页、搜索、新增、编辑、删除功能;数据面板页面:使用ECharts展示数据统计图表(折线图、饼图),直观展示系统数据。
vue
<!-- src/views/User.vue(用户管理页面) -->
<template>
<el-page-header content="用户管理" />
<el-card class="mt-4">
<div class="flex justify-between mb-4">
<el-input v-model="searchKeyword" placeholder="请输入用户名搜索" style="width: 300px" />
<el-button type="primary" @click="openAddDialog">新增用户</el-button>
</div>
<el-table :data="userList" border stripe>
<el-table-column label="ID" prop="id" align="center" />
<el-table-column label="用户名" prop="username" align="center" />
<el-table-column label="角色" prop="role" align="center">
<template #default="scope">
<el-tag :type="scope.row.role === 'admin' ? 'primary' : 'success'">
{{ scope.row.role === 'admin' ? '管理员' : '普通用户' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="text" @click="openEditDialog(scope.row)">编辑</el-button>
<el-button type="text" danger @click="deleteUser(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="mt-4"
:total="total"
:page-size="pageSize"
:current-page="currentPage"
@current-change="handlePageChange"
layout="total, prev, pager, next"
/>
</el-card>
<!-- 新增/编辑用户弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="isAdd">
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item label="角色" prop="role">
<el-select v-model="form.role" placeholder="请选择角色">
<el-option label="管理员" value="admin" />
<el-option label="普通用户" value="user" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getUserListApi, addUserApi, editUserApi, deleteUserApi } from '@/api/userApi'
// 搜索与分页
const searchKeyword = ref('')
const total = ref(0)
const pageSize = ref(10)
const currentPage = ref(1)
const userList = ref([])
// 弹窗相关
const dialogVisible = ref(false)
const isAdd = ref(true)
const dialogTitle = ref('新增用户')
const formRef = ref()
const form = reactive({
username: '',
password: '',
role: 'user'
})
const rules = reactive({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
})
// 获取用户列表
const getUserList = async () => {
const res = await getUserListApi({
keyword: searchKeyword.value,
page: currentPage.value,
size: pageSize.value
})
userList.value = res.data.list
total.value = res.data.total
}
// 分页切换
const handlePageChange = (page: number) => {
currentPage.value = page
getUserList()
}
// 新增用户弹窗
const openAddDialog = () => {
isAdd.value = true
dialogTitle.value = '新增用户'
form.username = ''
form.password = ''
form.role = 'user'
dialogVisible.value = true
}
// 编辑用户弹窗
const openEditDialog = (row: any) => {
isAdd.value = false
dialogTitle.value = '编辑用户'
form.username = row.username
form.role = row.role
dialogVisible.value = true
}
// 提交表单(新增/编辑)
const submitForm = async () => {
await formRef.value.validate()
if (isAdd.value) {
await addUserApi(form)
ElMessage.success('新增用户成功')
} else {
await editUserApi(form)
ElMessage.success('编辑用户成功')
}
dialogVisible.value = false
getUserList()
}
// 删除用户
const deleteUser = async (id: number) => {
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
type: 'warning'
})
await deleteUserApi(id)
ElMessage.success('删除用户成功')
getUserList()
}
// 页面加载时获取用户列表
onMounted(() => {
getUserList()
})
</script>
四、项目优化与部署
4.1 项目优化策略
-
按需引入:Element Plus使用按需引入,减少打包体积;
-
路由懒加载:使用动态import()实现路由懒加载,提升首屏加载速度;
-
自定义Hook:封装常用Hook(如useRequest、usePermission),提升代码复用性;
-
样式优化:使用Scoped CSS避免样式污染,结合Tailwind CSS简化样式编写;
-
性能优化:使用v-memo缓存组件,避免不必要的重渲染;图片使用懒加载。
4.2 项目部署(Nginx)
bash
# 1. 打包项目
npm run build
# 2. 部署到Nginx(修改nginx.conf配置)
server {
listen 80;
server_name localhost;
location / {
root /usr/local/nginx/html/admin-system; # 打包后的dist目录路径
index index.html;
try_files $uri $uri/ /index.html; # 解决Vue路由刷新404问题
}
# 接口代理(解决跨域)
location /api {
proxy_pass http://localhost:8080; # 后端接口地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
五、总结与延伸
本文完整实现了企业级后台管理系统的核心功能,掌握了Vue3+Vite+Pinia+Element Plus的实战用法,同时学习了路由守卫、权限控制、接口封装、项目优化与部署等核心知识点。该项目可直接作为企业级后台系统的模板,根据业务需求扩展功能。
延伸学习:可深入研究Vue3的自定义组件开发、Vue3+TS的类型约束、ECharts复杂图表的实现;同时学习前端工程化(ESLint、Prettier、Husky),规范代码风格,提升开发效率。