Skip to content

Vue3 后台管理系统完整开发指南

引言

本指南将详细介绍如何使用Vue3创建一个完整的后台管理系统,包括登录注册、用户管理、角色管理、菜单管理等功能,并对接后端接口。我们将基于Vue3 + Vite + TypeScript + Element Plus + Pinia等主流技术栈进行开发,确保系统具有高性能和可维护性。

项目初始化

环境准备

首先,我们需要确保环境已经配置好:

  • Node.js 16.18.0 以上版本
  • pnpm 8.6.0 以上版本(推荐使用pnpm)

初始化项目

使用Vite初始化一个Vue3项目:

bash
npm init vite@latest vue3-admin --template vue-ts

这将创建一个基于Vue3和TypeScript的项目模板。

项目结构

项目的基本结构如下:

src/
├── api/                 # API请求
├── assets/              # 资源文件
├── components/          # 公共组件
├── hooks/               # 组合式函数
├── layout/              # 布局组件
├── models/              # TypeScript类型定义
├── router/              # 路由配置
├── store/               # Pinia状态管理
├── styles/              # 全局样式
├── utils/               # 工具函数
└── views/               # 页面组件

技术栈选择

我们将使用以下技术栈:

  • Vue3:渐进式JavaScript框架
  • Vite:开发和构建工具
  • TypeScript:JavaScript的超集
  • Element Plus:基于Vue3的组件库
  • Pinia:Vue的状态管理库
  • Axios:HTTP客户端
  • UnoCSS:原子CSS库

项目配置

路径别名配置

为了简化路径引用,我们在vite.config.ts中配置路径别名:

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': '/src'
    }
  }
})

TypeScript配置

tsconfig.json中添加路径映射:

json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

自动导入配置

使用unplugin实现API和组件的自动导入:

bash
npm install -D unplugin-auto-import unplugin-vue-components

vite.config.ts中添加自动导入配置:

typescript
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
  plugins: [
    AutoImport({
      imports: ['vue', 'vue-router', '@/store', '@/api'],
      eslintrc: {
        enabled: true,
        filepath: './.eslintrc-auto-import.json'
      }
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ]
})

集成Element Plus

Element Plus是一个功能丰富的UI组件库,我们将使用它来构建管理界面。 安装Element Plus:

bash
npm install element-plus

main.ts中引入Element Plus:

typescript
import 'element-plus'

路由配置

使用Vue Router实现动态路由。我们需要定义静态路由和异步路由。 静态路由(不需要权限的路由):

typescript
// src/router/constantRoutes.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const LAYOUT = () => import('@/layout/index.vue')
export const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    component: () => import('@/views/login/index.vue'),
    meta: { hidden: true }
  },
  {
    path: '/',
    component: LAYOUT,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        component: () => import('@/views/dashboard/index.vue'),
        name: 'Dashboard',
        meta: { title: '仪表盘', icon: 'dashboard', affix: true }
      }
    ]
  }
]

异步路由(需要权限的路由):

typescript
// src/router/dynamicRoutes.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
const LAYOUT = () => import('@/layout/index.vue')
export const dynamicRoutes: RouteRecordRaw[] = [
  {
    name: 'System',
    path: '/system',
    component: LAYOUT,
    meta: { title: '系统管理', icon: 'system' },
    children: [
      {
        name: 'User',
        path: '/system/user',
        component: () => import('@/views/system/user/index.vue'),
        meta: { title: '用户管理', icon: 'user' }
      },
      {
        name: 'Role',
        path: '/system/role',
        component: () => import('@/views/system/role/index.vue'),
        meta: { title: '角色管理', icon: 'role' }
      },
      {
        name: 'Menu',
        path: '/system/menu',
        component: () => import('@/views/system/menu/index.vue'),
        meta: { title: '菜单管理', icon: 'menu' }
      }
    ]
  }
]

Pinia状态管理

Pinia是Vue3的状态管理库,我们将使用它来管理应用程序的状态。 定义用户Store:

typescript
// src/store/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: null as any
  }),
  actions: {
    setToken(token: string) {
      this.token = token
    },
    setUserInfo(userInfo: any) {
      this.userInfo = userInfo
    }
  }
})

定义权限Store:

typescript
// src/store/permission.ts
import { defineStore } from 'pinia'
export const usePermissionStore = defineStore('permission', {
  state: () => ({
    routes: [] as RouteRecordRaw[],
    buttons: [] as string[]
  }),
  actions: {
    setAuth({ routes, buttons }: { routes: string[], buttons: string[] }) {
      this.routes = routes
      this.buttons = buttons
    }
  }
})

登录注册功能

登录页面

创建登录页面组件:

vue
<!-- src/views/login/index.vue -->
<template>
  <div class="login-container">
    <el-form ref="formEl" :model="ruleForm" :rules="rules">
      <el-form-item prop="username">
        <el-input v-model="ruleForm.username" placeholder="用户名" />
      </el-form-item>
      <el-form-item prop="password">
        <el-input v-model="ruleForm.password" type="password" placeholder="密码" />
      </el-form-item>
      <el-button type="primary" @click="submitForm(formEl)">登录</el-button>
    </el-form>
  </div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/user'
import { useRouter } from 'vue-router'
import { login } from '@/api/auth'
const userStore = useUserStore()
const router = useRouter()
const ruleForm = ref({
  username: '',
  password: ''
})
const rules = {
  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
const formEl = ref<FormInstance>()
const submitForm = async (formEl: FormInstance | undefined) => {
  if (!formEl) return
  try {
    const { data } = await login(ruleForm.value)
    userStore.setToken(data.token)
    router.replace('/')
  } catch (error) {
    console.error(error)
  }
}
</script>

注册页面

创建注册页面组件:

vue
<!-- src/views/register/index.vue -->
<template>
  <div class="register-container">
    <el-form ref="formEl" :model="ruleForm" :rules="rules">
      <el-form-item prop="username">
        <el-input v-model="ruleForm.username" placeholder="用户名" />
      </el-form-item>
      <el-form-item prop="password">
        <el-input v-model="ruleForm.password" type="password" placeholder="密码" />
      </el-form-item>
      <el-form-item prop="confirmPassword">
        <el-input v-model="ruleForm.confirmPassword" type="password" placeholder="确认密码" />
      </el-form-item>
      <el-button type="primary" @click="submitForm(formEl)">注册</el-button>
    </el-form>
  </div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store/user'
import { useRouter } from 'vue-router'
import { register } from '@/api/auth'
const userStore = useUserStore()
const router = useRouter()
const ruleForm = ref({
  username: '',
  password: '',
  confirmPassword: ''
})
const rules = {
  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
  confirmPassword: [
    { required: true, message: '请输入确认密码', trigger: 'blur' },
    { validator: (rule, value) => value === ruleForm.value.password, message: '两次输入的密码不一致', trigger: 'blur' }
  ]
}
const formEl = ref<FormInstance>()
const submitForm = async (formEl: FormInstance | undefined) => {
  if (!formEl) return
  try {
    await register(ruleForm.value)
    router.replace('/login')
  } catch (error) {
    console.error(error)
  }
}
</script>

权限管理

路由权限控制

router/index.ts中添加路由守卫:

typescript
// src/router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/store/user'
import { usePermissionStore } from '@/store/permission'
import { login } from '@/api/auth'
import { constantRoutes, dynamicRoutes } from './routes'
const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes as RouteRecordRaw[],
  scrollBehavior: () => ({ left: 0, top: 0 })
})
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const permissionStore = usePermissionStore()
  if (to.path === '/login') {
    if (userStore.token) {
      next('/')
    } else {
      next()
    }
    return
  }
  if (!userStore.token) {
    next({ path: '/login', query: { redirect: to.fullPath } })
    return
  }
  if (!permissionStore.routes.length) {
    try {
      await initDynamicRouter()
      next({ ...to, replace: true })
    } catch (error) {
      console.error(error)
      next('/')
    }
    return
  }
  next()
})
async function initDynamicRouter() {
  const userStore = useUserStore()
  const permissionStore = usePermissionStore()
  try {
    const { data } = await login()
    userStore.setToken(data.token)
    userStore.setUserInfo(data.userInfo)
    const routes = filterAsyncRoutes(dynamicRoutes, data.userInfo.routes)
    router.addRoute('System', routes)
    permissionStore.setAuth({ routes: data.userInfo.routes, buttons: data.userInfo.buttons })
  } catch (error) {
    console.error(error)
    throw error
  }
}
function filterAsyncRoutes(asyncRoutes: RouteRecordRaw[], authRoutes: string[]) {
  return asyncRoutes.filter(route => {
    if (!authRoutes.includes(route.name as string)) return false
    if (route.children) {
      route.children = filterAsyncRoutes(route.children, authRoutes)
    }
    return true
  })
}
export default router

按钮权限控制

创建按钮权限指令:

typescript
// src/directives/auth.ts
import { Directive, DirectiveBinding } from 'vue'
import { usePermissionStore } from '@/store/permission'
export const auth: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const { value } = binding
    const permissionStore = usePermissionStore()
    const buttons = permissionStore.buttons
    if (value instanceof Array && value.length) {
      const hasPermission = value.every(item => buttons.includes(item))
      if (!hasPermission) {
        el.parentNode?.removeChild(el)
      }
    } else {
      if (!buttons.includes(value as string)) {
        el.parentNode?.removeChild(el)
      }
    }
  }
}

main.ts中注册指令:

typescript
// src/main.ts
import App from './App.vue'
import router from './router'
import store from './store'
import directives from './directives'
createApp(App)
  .use(router)
  .use(store)
  .use(directives)
  .mount('#app')

用户管理

用户列表页面

创建用户列表页面:

vue
<!-- src/views/system/user/index.vue -->
<template>
  <div class="app-container">
    <el-button type="primary" @click="handleCreate">新增用户</el-button>
    <el-table :data="users" border>
      <el-table-column prop="username" label="用户名" />
      <el-table-column prop="email" label="邮箱" />
      <el-table-column prop="role" label="角色" />
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button type="primary" @click="handleEdit(row)">编辑</el-button>
          <el-button type="danger" @click="handleDelete(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script setup lang="ts">
import { userList, userCreate, userUpdate, userDelete } from '@/api/system/user'
const users = ref([])
async function fetchUsers() {
  const { data } = await userList()
  users.value = data
}
async function handleCreate() {
  await userCreate()
  await fetchUsers()
}
async function handleEdit(row: any) {
  await userUpdate(row.id)
  await fetchUsers()
}
async function handleDelete(row: any) {
  await userDelete(row.id)
  await fetchUsers()
}
fetchUsers()
</script>

用户详情页面

创建用户详情页面:

vue
<!-- src/views/system/user/detail.vue -->
<template>
  <div class="app-container">
    <el-form ref="formEl" :model="user" :rules="rules">
      <el-form-item label="用户名" prop="username">
        <el-input v-model="user.username" />
      </el-form-item>
      <el-form-item label="邮箱" prop="email">
        <el-input v-model="user.email" />
      </el-form-item>
      <el-form-item label="角色" prop="role">
        <el-select v-model="user.role">
          <el-option label="管理员" value="admin" />
          <el-option label="普通用户" value="user" />
        </el-select>
      </el-form-item>
      <el-button type="primary" @click="submitForm(formEl)">保存</el-button>
    </el-form>
  </div>
</template>
<script setup lang="ts">
import { defineProps, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { userDetail, userUpdate } from '@/api/system/user'
const props = defineProps({
  id: {
    type: String,
    required: true
  }
})
const route = useRoute()
const router = useRouter()
const user = ref({})
const rules = {
  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  email: [{ required: true, message: '请输入邮箱', trigger: 'blur' }],
  role: [{ required: true, message: '请选择角色', trigger: 'change' }]
}
const formEl = ref<FormInstance>()
async function fetchUser() {
  const { data } = await userDetail(props.id)
  user.value = data
}
async function submitForm(formEl: FormInstance | undefined) {
  if (!formEl) return
  try {
    await userUpdate(props.id, user.value)
    router.push('/system/user')
  } catch (error) {
    console.error(error)
  }
}
fetchUser()
</script>

角色管理

角色列表页面

创建角色列表页面:

vue
<!-- src/views/system/role/index.vue -->
<template>
  <div class="app-container">
    <el-button type="primary" @click="handleCreate">新增角色</el-button>
    <el-table :data="roles" border>
      <el-table-column prop="name" label="角色名称" />
      <el-table-column prop="description" label="描述" />
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button type="primary" @click="handleEdit(row)">编辑</el-button>
          <el-button type="danger" @click="handleDelete(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script setup lang="ts">
import { roleList, roleCreate, roleUpdate, roleDelete } from '@/api/system/role'
const roles = ref([])
async function fetchRoles() {
  const { data } = await roleList()
  roles.value = data
}
async function handleCreate() {
  await roleCreate()
  await fetchRoles()
}
async function handleEdit(row: any) {
  await roleUpdate(row.id)
  await fetchRoles()
}
async function handleDelete(row: any) {
  await roleDelete(row.id)
  await fetchRoles()
}
fetchRoles()
</script>

角色详情页面

创建角色详情页面:

vue
<!-- src/views/system/role/detail.vue -->
<template>
  <div class="app-container">
    <el-form ref="formEl" :model="role" :rules="rules">
      <el-form-item label="角色名称" prop="name">
        <el-input v-model="role.name" />
      </el-form-item>
      <el-form-item label="描述" prop="description">
        <el-input v-model="role.description" />
      </el-form-item>
      <el-button type="primary" @click="submitForm(formEl)">保存</el-button>
    </el-form>
  </div>
</template>
<script setup lang="ts">
import { defineProps, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { roleDetail, roleUpdate } from '@/api/system/role'
const props = defineProps({
  id: {
    type: String,
    required: true
  }
})
const route = useRoute()
const router = useRouter()
const role = ref({})
const rules = {
  name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }]
}
const formEl = ref<FormInstance>()
async function fetchRole() {
  const { data } = await roleDetail(props.id)
  role.value = data
}
async function submitForm(formEl: FormInstance | undefined) {
  if (!formEl) return
  try {
    await roleUpdate(props.id, role.value)
    router.push('/system/role')
  } catch (error) {
    console.error(error)
  }
}
fetchRole()
</script>

菜单管理

菜单列表页面

创建菜单列表页面:

vue
<!-- src/views/system/menu/index.vue -->
<template>
  <div class="app-container">
    <el-button type="primary" @click="handleCreate">新增菜单</el-button>
    <el-tree :data="menus" :props="{ children: 'children', label: 'title' }" />
  </div>
</template>
<script setup lang="ts">
import { menuList, menuCreate, menuUpdate, menuDelete } from '@/api/system/menu'
const menus = ref([])
async function fetchMenus() {
  const { data } = await menuList()
  menus.value = data
}
async function handleCreate() {
  await menuCreate()
  await fetchMenus()
}
async function handleEdit(row: any) {
  await menuUpdate(row.id)
  await fetchMenus()
}
async function handleDelete(row: any) {
  await menuDelete(row.id)
  await fetchMenus()
}
fetchMenus()
</script>

菜单详情页面

创建菜单详情页面:

vue
<!-- src/views/system/menu/detail.vue -->
<template>
  <div class="app-container">
    <el-form ref="formEl" :model="menu" :rules="rules">
      <el-form-item label="菜单名称" prop="name">
        <el-input v-model="menu.name" />
      </el-form-item>
      <el-form-item label="路由路径" prop="path">
        <el-input v-model="menu.path" />
      </el-form-item>
      <el-form-item label="组件路径" prop="component">
        <el-input v-model="menu.component" />
      </el-form-item>
      <el-form-item label="图标" prop="icon">
        <el-input v-model="menu.icon" />
      </el-form-item>
      <el-button type="primary" @click="submitForm(formEl)">保存</el-button>
    </el-form>
  </div>
</template>
<script setup lang="ts">
import { defineProps, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { menuDetail, menuUpdate } from '@/api/system/menu'
const props = defineProps({
  id: {
    type: String,
    required: true
  }
})
const route = useRoute()
const router = useRouter()
const menu = ref({})
const rules = {
  name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
  path: [{ required: true, message: '请输入路由路径', trigger: 'blur' }],
  component: [{ required: true, message: '请输入组件路径', trigger: 'blur' }]
}
const formEl = ref<FormInstance>()
async function fetchMenu() {
  const { data } = await menuDetail(props.id)
  menu.value = data
}
async function submitForm(formEl: FormInstance | undefined) {
  if (!formEl) return
  try {
    await menuUpdate(props.id, menu.value)
    router.push('/system/menu')
  } catch (error) {
    console.error(error)
  }
}
fetchMenu()
</script>

对接后端接口

API请求配置

src/api目录下创建API请求配置。例如,src/api/auth.ts

typescript
import { axios } from '@/utils/request'
export async function login() {
  return axios.get('/api/login')
}
export async function register() {
  return axios.post('/api/register')
}

接口文档

使用Swagger或OpenAPI工具创建接口文档,确保前后端接口一致。

总结

通过以上步骤,我们已经创建了一个完整的基于Vue3的后台管理系统,包括登录注册、用户管理、角色管理、菜单管理等功能,并对接了后端接口。这个系统使用了Vue3 + Vite + TypeScript + Element Plus + Pinia等技术栈,具有高性能和可维护性。 希望这个指南对你有所帮助!如果你有任何问题或建议,请随时反馈。

更新时间:

指尖飞舞,键盘敲击,思维激荡,创新不断。