后台管理系统是前端工程师最常接触的项目类型之一,其典型需求包括:根据用户角色动态生成菜单和路由、精确到按钮级别的权限控制、带搜索与分页的数据表格、以及表单字段之间的联动校验。下面以 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 页面骨架。实际项目中,你可以将这些模块抽离为通用组件或组合式函数,逐步沉淀出适合自己团队的“业务组件库”。