人人都会AI编程

29.1 后台管理系统:动态路由、权限控制、表格分页、表单联动

更新时间:2026-07-11

后台管理系统是前端工程师最常接触的项目类型之一,其典型需求包括:根据用户角色动态生成菜单和路由、精确到按钮级别的权限控制、带搜索与分页的数据表格、以及表单字段之间的联动校验。下面以 Vue 3 + Element Plus 为例,给出可直接落地的实现方案。


动态路由与权限控制

目标:用户登录后,后端返回该用户的权限标识(如角色编码 ['admin', 'editor'] 或权限点 ['user:add', 'order:view']),前端根据权限动态注册路由,并渲染侧边栏菜单。

1. 路由的模块化定义

将所有路由按业务模块拆分,并为每个路由设置 meta.roles 字段,表示哪些角色才能访问。

// router/modules/user.js
export default {
  path: '/user',
  name: 'User',
  component: () => import('@/views/user/index.vue'),
  meta: { title: '用户管理', icon: 'User', roles: ['admin'] }
}

2. 登录后动态添加路由

在登录成功的回调中,根据后端返回的角色信息过滤路由,并通过 router.addRoute() 动态注册。

// store/userStore.js (Pinia)
import { defineStore } from 'pinia'
import { asyncRoutes } from '@/router' // 异步路由配置

export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    roles: []
  }),
  actions: {
    async login(loginForm) {
      // 调用登录接口,获取 token 和 roles
      const { token, roles } = await loginApi(loginForm)
      this.token = token
      this.roles = roles
      // 动态生成路由
      const accessRoutes = filterAsyncRoutes(asyncRoutes, roles)
      accessRoutes.forEach(route => router.addRoute(route))
      // 将路由存入状态,供侧边栏渲染
      this.routes = accessRoutes
    }
  }
})

3. 过滤算法

function filterAsyncRoutes(routes, roles) {
  return routes.filter(route => {
    if (hasPermission(route.meta?.roles, roles)) {
      if (route.children) {
        route.children = filterAsyncRoutes(route.children, roles)
      }
      return true
    }
    return false
  })
}

function hasPermission(routeRoles, userRoles) {
  if (!routeRoles) return true // 无限制
  return userRoles.some(role => routeRoles.includes(role))
}

4. 菜单渲染 + 按钮级权限

侧边栏递归生成菜单,按钮权限通过自定义指令 v-permission 实现。

<!-- 菜单组件 -->
<template>
  <el-menu>
    <template v-for="route in routes" :key="route.path">
      <el-sub-menu v-if="route.children">
        <span>{{ route.meta.title }}</span>
        <menu-item :routes="route.children" />
      </el-sub-menu>
      <el-menu-item v-else :index="route.path">
        {{ route.meta.title }}
      </el-menu-item>
    </template>
  </el-menu>
</template>
// 按钮权限指令
app.directive('permission', {
  mounted(el, binding) {
    const { value } = binding  // ['user:add']
    const store = useUserStore()
    if (!store.permissions.includes(value)) {
      el.parentNode?.removeChild(el)
    }
  }
})
// 使用:<el-button v-permission="'user:add'">新增</el-button>

表格分页(带搜索和重置)

典型场景:列表页顶部的搜索表单 + 中部表格 + 底部分页器。推荐把搜索条件统一放在一个对象中,通过 watch 或在查询方法中统一处理。

1. 数据结构与状态

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getTableData } from '@/api/table'

const queryParams = reactive({
  keyword: '',
  status: '',
  pageNum: 1,
  pageSize: 10
})
const tableData = ref([])
const total = ref(0)

const fetchData = async () => {
  const { data } = await getTableData(queryParams)
  tableData.value = data.list
  total.value = data.total
}

const handleSearch = () => {
  queryParams.pageNum = 1   // 重置页码
  fetchData()
}
const handleReset = () => {
  Object.assign(queryParams, { keyword: '', status: '', pageNum: 1, pageSize: 10 })
  fetchData()
}
const handlePageChange = (page) => {
  queryParams.pageNum = page
  fetchData()
}
onMounted(() => fetchData())
</script>

2. 模板部分

<template>
  <div class="page-container">
    <!-- 搜索栏 -->
    <el-form :model="queryParams" inline>
      <el-form-item label="关键词">
        <el-input v-model="queryParams.keyword" placeholder="请输入" />
      </el-form-item>
      <el-form-item label="状态">
        <el-select v-model="queryParams.status">
          <el-option label="全部" value="" />
          <el-option label="启用" value="1" />
          <el-option label="禁用" value="0" />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleSearch">查询</el-button>
        <el-button @click="handleReset">重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 表格 -->
    <el-table :data="tableData" border>
      <el-table-column prop="name" label="名称" />
      <el-table-column prop="status" label="状态" />
    </el-table>

    <!-- 分页 -->
    <el-pagination
      v-model:current-page="queryParams.pageNum"
      v-model:page-size="queryParams.pageSize"
      :total="total"
      @current-change="handlePageChange"
      @size-change="handleSearch"
    />
  </div>
</template>

真实项目中的注意点

  • 分页器切换页码和每页条数时,应当复用同一个查询方法,保证状态同步。
  • 搜索条件较多时可以将 queryParams 抽取为组合式函数复用。
  • 如果多个页面表格结构一致,可封装一个通用的表格分页组件,内部自动处理 loading、空状态等,传参即可用。

表单联动

表单联动指一个字段的值改变时,影响另一个字段的选项、校验规则或可见性。常见如:选择省后加载市,选择商品分类后筛选规格。

实现方式:利用 watch 监听联动源数据的变化,动态修改目标字段的选项或清空已选值。

<script setup>
import { ref, watch } from 'vue'
import { getCities, getDistricts } from '@/api/region'

const form = reactive({ province: '', city: '', district: '' })
const cityOptions = ref([])
const districtOptions = ref([])

watch(() => form.province, async (newVal) => {
  if (!newVal) {
    cityOptions.value = []
    form.city = ''
    return
  }
  cityOptions.value = await getCities(newVal)
  form.city = '' // 清空已选城市
})

watch(() => form.city, async (newVal) => {
  if (!newVal) {
    districtOptions.value = []
    form.district = ''
    return
  }
  districtOptions.value = await getDistricts(newVal)
  form.district = ''
})
</script>

当选项数据量较大时,可加入防抖避免频繁请求。如果联动逻辑在多处复用,可以将它封装成自定义 Hook useCascader(province, city),返回响应式选项和加载状态,提高代码复用度。


以上方案覆盖了后台管理系统最核心的四个功能点,它们共同构成了一套可维护、可扩展的 CRUD 页面骨架。实际项目中,你可以将这些模块抽离为通用组件或组合式函数,逐步沉淀出适合自己团队的“业务组件库”。