Vue3\+Pinia实战:企业级后台管理系统开发(附权限控制)

摘要: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 核心优化策略

  1. 组件缓存:使用Vue的keep-alive缓存常用页面(如用户管理、角色管理),避免重复渲染,提升页面切换速度。

  2. 路由懒加载:通过import()动态导入页面组件,减小首屏加载体积,提升首屏加载速度。

  3. 表单防抖:对高频操作(如搜索框输入)进行防抖处理,减少接口请求次数。

  4. 权限精细化:基于用户角色控制菜单显示、按钮操作权限,提升系统安全性。

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,完整实现

相关推荐
空中海15 小时前
Vue 全栈开发知识体系
vue
吴声子夜歌2 天前
Vue3——Vuex状态管理
前端·vue.js·vue·es6
sp42a3 天前
NativeScript ListView 实现固定分区标题
vue·nativescript
双普拉斯3 天前
打造工业级全栈文件管理器:深度解析上传、回收站与三重下载流控技术
spring·vue·js
码界筑梦坊3 天前
356-基于Python的网易新闻数据分析系统
python·mysql·信息可视化·数据分析·django·vue·毕业设计
吴声子夜歌4 天前
Vue3——渲染函数
前端·vue.js·vue·es6
2501_913680004 天前
Vue3项目快速接入AI助手的终极方案 - 让你的应用智能升级
前端·vue.js·人工智能·ai·vue·开源软件
吕永强4 天前
基于SpringBoot+Vue校园报修系统的设计与实现(源码+论文+部署)
vue·毕业设计·springboot·毕业论文·报修系统·校园报修
阿部多瑞 ABU4 天前
《智能学号抽取系统》V5.9.5 发布:精简代码,修复移动端文件读取核心 Bug
vue·html·bug