摘要:Vue3作为前端主流框架,结合Pinia(Vue官方推荐状态管理库)、Element Plus组件库,已成为企业级后台管理系统的首选技术栈。本文基于Vue3.4、Pinia、Element Plus、Vite,详细讲解后台管理系统的核心模块开发,包括路由配置、Pinia状态管理、权限控制、表单验证、表格交互、接口请求封装等,结合真实后台管理场景(用户管理、角色管理、菜单管理),附完整源码与开发规范,适合前端开发者快速上手Vue3+Pinia,提升企业级前端项目开发能力。
一、前言:Vue3+Pinia的核心优势与项目定位
Vue3相比Vue2,带来了Composition API、Teleport、Suspense等新特性,大幅提升了代码的复用性与可维护性;Pinia作为Vuex的替代方案,简化了状态管理逻辑,支持TypeScript,无需嵌套模块,使用更简洁;Element Plus作为Vue3的官方组件库,提供了丰富的后台管理常用组件(表格、表单、弹窗等),加速项目开发。
本文开发的企业级后台管理系统,涵盖用户管理、角色管理、菜单管理、权限分配等核心功能,采用前后端分离架构,前端基于Vue3+Pinia+Element Plus,后端接口采用模拟数据(可直接对接真实后端接口),帮助开发者掌握后台管理系统的开发流程与核心技巧。
二、环境搭建:Vue3+Pinia项目初始化
2.1 项目初始化(Vite方式)
bash
# 1. 初始化Vue3项目(Vite+TypeScript)
npm create vite@latest vue3-pinia-admin -- --template vue-ts
# 2. 进入项目目录
cd vue3-pinia-admin
# 3. 安装核心依赖
npm install pinia element-plus @element-plus/icons-vue axios vue-router@4
# 4. 启动开发服务器
npm run dev
2.2 项目目录结构设计(企业级规范)
text
vue3-pinia-admin/
├── src/
│ ├── api/ # 接口请求封装(用户、角色、菜单等接口)
│ ├── assets/ # 静态资源(图片、样式、图标)
│ ├── components/ # 通用组件(布局、分页、表单等)
│ ├── router/ # 路由配置(含权限路由)
│ ├── store/ # Pinia状态管理(用户、权限、系统配置等)
│ ├── utils/ # 工具函数(权限判断、请求拦截等)
│ ├── views/ # 页面组件(用户管理、角色管理、首页等)
│ ├── App.vue # 根组件
│ ├── main.ts # 入口文件
│ └── vite-env.d.ts # 环境类型声明
├── vite.config.ts # Vite配置
└── package.json # 依赖配置
2.3 全局配置(Element Plus、Pinia、路由)
typescript
// src/main.ts(入口文件)
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入Pinia
import ElementPlus from 'element-plus' // 引入Element Plus
import 'element-plus/dist/index.css' // 引入Element Plus样式
import * as ElementPlusIconsVue from '@element-plus/icons-vue' // 引入Element Plus图标
import App from './App.vue'
import router from './router' // 引入路由
const app = createApp(App)
// 注册Pinia
app.use(createPinia())
// 注册Element Plus
app.use(ElementPlus)
// 注册Element Plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 注册路由
app.use(router)
app.mount('#app')
三、核心模块开发(实战场景)
3.1 模块1:Pinia状态管理(用户+权限)
Pinia无需创建modules,直接定义Store,支持TypeScript,简化状态管理逻辑,本文实现用户Store(管理用户信息、登录登出)和权限Store(管理菜单权限)。
typescript
// src/store/modules/userStore.ts(用户Store)
import { defineStore } from 'pinia'
import { loginApi, getUserInfoApi, logoutApi } from '../../api/userApi'
// 用户状态类型
interface UserState {
token: string | null
userInfo: {
id: number
username: string
role: string
} | null
isLogin: boolean
}
// 定义用户Store
export const useUserStore = defineStore('user', {
state: (): UserState => ({
token: localStorage.getItem('token'),
userInfo: JSON.parse(localStorage.getItem('userInfo') || 'null'),
isLogin: !!localStorage.getItem('token')
}),
actions: {
// 登录
async login(username: string, password: string) {
const res = await loginApi({ username, password })
const { token } = res.data
localStorage.setItem('token', token)
this.token = token
// 登录成功后获取用户信息
await this.getUserInfo()
},
// 获取用户信息
async getUserInfo() {
const res = await getUserInfoApi()
this.userInfo = res.data
localStorage.setItem('userInfo', JSON.stringify(res.data))
this.isLogin = true
},
// 登出
async logout() {
await logoutApi()
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
this.token = null
this.userInfo = null
this.isLogin = false
}
}
})
// src/store/modules/permissionStore.ts(权限Store)
import { defineStore } from 'pinia'
import { getMenuListApi } from '../../api/menuApi'
import router from '../../router'
// 菜单类型
interface MenuItem {
id: number
name: string
path: string
component: string
icon: string
children?: MenuItem[]
}
// 权限状态类型
interface PermissionState {
menuList: MenuItem[] // 菜单列表(含权限控制)
routes: any[] // 动态路由
}
export const usePermissionStore = defineStore('permission', {
state: (): PermissionState => ({
menuList: [],
routes: []
}),
actions: {
// 获取菜单列表(根据用户角色获取对应菜单)
async getMenuList() {
const res = await getMenuListApi()
this.menuList = res.data
// 动态添加路由
this.addDynamicRoutes()
},
// 动态添加路由
addDynamicRoutes() {
this.menuList.forEach(menu => {
if (menu.children && menu.children.length > 0) {
// 有子菜单,递归添加
menu.children.forEach(child => {
this.addRoute(child)
})
} else {
// 无子菜单,直接添加
this.addRoute(menu)
}
})
},
// 单个路由添加
addRoute(menu: MenuItem) {
const route = {
path: menu.path,
name: menu.name,
component: () => import(`../../views/${menu.component}.vue`),
meta: {
icon: menu.icon,
title: menu.name
}
}
router.addRoute('layout', route) // 添加到布局路由下
this.routes.push(route)
}
}
})
3.2 模块2:路由配置(含权限控制)
路由分为静态路由(登录页、404页)和动态路由(根据用户权限动态添加),实现权限拦截,未登录用户跳转至登录页,无权限用户跳转至404页。
typescript
// src/router/index.ts(路由配置)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useUserStore } from '../store/modules/userStore'
// 静态路由
const staticRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'login',
component: () => import('../views/Login.vue'),
meta: {
requireAuth: false // 无需登录
}
},
{
path: '/',
name: 'layout',
component: () => import('../components/Layout.vue'),
meta: {
requireAuth: true // 需要登录
},
children: [] // 动态路由将添加到这里
},
{
path: '/:pathMatch(.*)*',
name: '404',
component: () => import('../views/404.vue'),
meta: {
requireAuth: false
}
}
]
const router = createRouter({
history: createWebHistory(),
routes: staticRoutes
})
// 路由拦截(权限控制)
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
// 判断是否需要登录
if (to.meta.requireAuth) {
if (userStore.isLogin) {
// 已登录,放行
next()
} else {
// 未登录,跳转至登录页
next({ name: 'login' })
}
} else {
// 无需登录,放行
next()
}
})
export default router
3.3 模块3:接口请求封装(Axios拦截器)
封装Axios,实现请求拦截(添加Token)、响应拦截(统一错误处理),简化接口调用,提升代码复用性。
typescript
// src/utils/request.ts(Axios封装)
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '../store/modules/userStore'
// 创建Axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量(区分开发/生产环境)
timeout: 5000
})
// 请求拦截器(添加Token)
request.interceptors.request.use(
(config: AxiosRequestConfig) => {
const userStore = useUserStore()
// 已登录,添加Token到请求头
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器(统一错误处理)
request.interceptors.response.use(
(response: AxiosResponse) => {
const { code, message, data } = response.data
// 接口请求成功(code=200)
if (code === 200) {
return response.data
} else {
// 接口请求失败,提示错误信息
ElMessage.error(message || '请求失败,请稍后再试')
return Promise.reject(new Error(message || '请求失败'))
}
},
(error) => {
// 网络错误或Token过期
if (error.response?.status === 401) {
// Token过期,登出并跳转至登录页
const userStore = useUserStore()
userStore.logout()
ElMessage.error('登录已过期,请重新登录')
} else {
ElMessage.error('网络错误,请稍后再试')
}
return Promise.reject(error)
}
)
export default request
3.4 模块4:核心页面开发(用户管理)
以用户管理页面为例,实现用户列表查询、新增用户、编辑用户、删除用户、分页等核心功能,结合Element Plus组件与Pinia状态管理。
vue
// src/views/UserManage.vue(用户管理页面)
<template>
<div class="user-manage">
<el-page-header content="用户管理" />
<el-card class="mt-4">
<div class="search-bar mb-4">
<el-input v-model="searchForm.username" placeholder="请输入用户名" style="width: 200px; margin-right: 10px" />
<el-select v-model="searchForm.role" placeholder="请选择角色" style="width: 150px; margin-right: 10px">
<el-option label="管理员" value="admin" />
<el-option label="普通用户" value="user" />
</el-select>
<el-button type="primary" @click="getUserList">查询</el-button>
<el-button type="success" @click="openAddDialog" style="margin-left: 10px">新增用户</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="primary" v-if="scope.row.role === 'admin'">管理员</el-tag>
<el-tag v-else>普通用户</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="createTime" align="center" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button type="primary" size="small" @click="openEditDialog(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="deleteUser(scope.row.id)" style="margin-left: 10px">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="mt-4"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageInfo.currentPage"
:page-sizes="[10, 20, 50]"
:page-size="pageInfo.pageSize"
:total="pageInfo.total"
layout="total, sizes, prev, pager, next, jumper"
/>
</el-card>
<!-- 新增/编辑用户弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
<el-form :model="form" :rules="formRules" ref="formRef" label-width="100px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="isAdd">
<el-input v-model="form.password" type="password" placeholder="请输入密码" />
</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>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, ElMessage } from 'vue'
import { ElForm, ElFormItem, ElInput, ElSelect, ElOption, ElDialog, ElPagination, ElTable, ElTableColumn, ElTag, ElButton, ElPageHeader, ElCard } from 'element-plus'
import { getUserListApi, addUserApi, editUserApi, deleteUserApi } from '../api/userApi'
// 搜索表单
const searchForm = reactive({
username: '',
role: ''
})
// 分页信息
const pageInfo = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 用户列表
const userList = ref([])
// 弹窗相关
const dialogVisible = ref(false)
const dialogTitle = ref('新增用户')
const isAdd = ref(true)
const formRef = ref<InstanceType<typeof ElForm>>(null)
const form = reactive({
id: 0,
username: '',
password: '',
role: 'user'
})
// 表单验证规则
const formRules = reactive({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }],
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
})
// 页面加载时获取用户列表
onMounted(() => {
getUserList()
})
// 获取用户列表
const getUserList = async () => {
const res = await getUserListApi({
username: searchForm.username,
role: searchForm.role,
pageNum: pageInfo.currentPage,
pageSize: pageInfo.pageSize
})
userList.value = res.data.list
pageInfo.total = res.data.total
}
// 分页大小改变
const handleSizeChange = (val: number) => {
pageInfo.pageSize = val
getUserList()
}
// 当前页码改变
const handleCurrentChange = (val: number) => {
pageInfo.currentPage = val
getUserList()
}
// 打开新增弹窗
const openAddDialog = () => {
isAdd.value = true
dialogTitle.value = '新增用户'
// 重置表单
form.id = 0
form.username = ''
form.password = ''
form.role = 'user'
dialogVisible.value = true
}
// 打开编辑弹窗
const openEditDialog = (row: any) => {
isAdd.value = false
dialogTitle.value = '编辑用户'
// 填充表单数据
form.id = row.id
form.username = row.username
form.role = row.role
dialogVisible.value = true
}
// 提交表单(新增/编辑)
const submitForm = async () => {
if (!formRef.value) return
await formRef.value.validate()
try {
if (isAdd.value) {
// 新增用户
await addUserApi(form)
ElMessage.success('新增用户成功')
} else {
// 编辑用户
await editUserApi(form)
ElMessage.success('编辑用户成功')
}
// 关闭弹窗,重新获取用户列表
dialogVisible.value = false
getUserList()
} catch (error) {
ElMessage.error('操作失败,请稍后再试')
}
}
// 删除用户
const deleteUser = async (id: number) => {
ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await deleteUserApi(id)
ElMessage.success('删除用户成功')
getUserList()
}).catch(() => {
ElMessage.info('已取消删除')
})
}
</script>
<style scoped>
.user-manage {
padding: 20px;
}
.search-bar {
display: flex;
align-items: center;
}
</style>
四、项目优化与部署
4.1 核心优化策略
-
组件缓存:使用Vue的keep-alive缓存常用页面(如用户管理、角色管理),避免重复渲染,提升页面切换速度。
-
路由懒加载:通过import()动态导入页面组件,减小首屏加载体积,提升首屏加载速度。
-
表单防抖:对高频操作(如搜索框输入)进行防抖处理,减少接口请求次数。
-
权限精细化:基于用户角色控制菜单显示、按钮操作权限,提升系统安全性。
4.2 项目部署(Nginx)
bash
# 1. 打包项目
npm run build
# 2. 部署到Nginx(将dist目录下的文件复制到Nginx的html目录)
cp -r dist/* /usr/share/nginx/html/
# 3. 修改Nginx配置(nginx.conf)
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html; # 解决Vue路由刷新404问题
}
# 反向代理后端接口(避免跨域)
location /api {
proxy_pass http://192.168.1.100:8080; # 后端接口地址
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# 4. 重启Nginx
systemctl restart nginx
五、总结与延伸
本文结合Vue3+Pinia+Element Plus,完整实现