在后台权限体系中,角色、菜单、接口按钮与数据范围往往被拆散在多个配置入口中维护,实际落地时极易出现按钮越权、数据范围错配、前端状态与后端存储不一致等问题。这类风险通常不源于单一逻辑错误,而是配置路径过长、默认策略缺失以及批量操作能力不足共同叠加后的结果。
本文围绕"角色权限配置"这一真实模块展开,聚焦权限配置抽屉 RoleDrawer 与接口权限面板 RoleMenuBtn 的协作方式,完整拆解从部门切换、菜单选择、按钮勾选到数据范围持久化的实现链路,重点关注参数传递、状态同步与接口封装方式。
文章目录
需求解析
该功能位于后台系统的角色管理模块中,运行在前端基于 Vue 与 Element Plus 的管理界面,后端通过统一的权限接口提供数据支撑,主要服务于角色创建或调整后的权限配置场景。使用过程中需要在一个统一界面内完成部门切换、菜单定位、接口按钮授权以及数据范围控制,且所有操作要求即时生效并持久化。


从操作链路上看,权限配置从角色列表页触发,打开权限配置抽屉后,通过部门筛选驱动菜单树刷新,菜单节点变化进一步触发接口权限面板的数据加载,按钮勾选与数据范围设置最终通过接口写入后端。
结果落库
接口
持久化
权限配置
选择
菜单
加载
接口按钮
勾选按钮
设置数据范围
上下文切换
切换
部门
加载
菜单树
入口触发
角色列表页
打开
RoleDrawer
功能实现
权限配置抽屉由 RoleDrawer.vue 承载,负责统一容纳部门切换、菜单树以及不同权限面板,通过抽屉形式避免页面跳转带来的上下文丢失。
vue
<template>
<el-drawer v-model="RoleDrawer.drawerVisible" title="权限配置" direction="rtl" size="80%" :close-on-click-modal="false"
:before-close="RoleDrawer.handleDrawerClose" :destroy-on-close="true">
<template #header>
<div style="display: flex;align-items: center;">
<el-form-item label="部门" prop="company" style="margin-top: 16px;margin-right: 12px;">
<el-select v-model="RoleDrawer.company" placeholder="请选择部门" @change="handleDeptChange"
style="width: 200px;">
<el-option v-for="item in deptAllList" :key="item.key" :label="item.name" :value="item.name" />
</el-select>
</el-form-item>
<div>当前授权角色:
<el-tag style="margin-right: 20px">{{ RoleDrawer.roleName }}</el-tag>
授权人员:
<el-button size="small" :icon="UserFilled" @click="handleUsers">{{ RoleDrawer.users.length
}}</el-button>
</div>
</div>
</template>
<splitpanes class="default-theme" style="height: 100%">
<pane min-size="20" size="22">
<div class="pane-box">
<MenuTreeCom ref="refMenuTreeCom" />
</div>
</pane>
<pane min-size="20">
<div class="pane-box">
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="接口权限" name="first">
<MenuBtnCom />
</el-tab-pane>
<el-tab-pane label="列字段权限" name="second">
<MenuFieldCom />
</el-tab-pane>
</el-tabs>
</div>
</pane>
</splitpanes>
</el-drawer>
<el-dialog v-model="dialogVisible" title="授权用户" width="700px" :close-on-click-modal="false">
<RoleUsersCom />
</el-dialog>
</template>
该组件的关键在于将部门、菜单与权限面板解耦,抽屉关闭时销毁实例,避免历史状态残留。
接口权限面板由 RoleMenuBtn.vue 实现,承担默认接口权限、按钮勾选与快捷配置能力。
vue
<template>
<div class="pccm-item" v-if="RoleMenuBtn.$state.length > 0">
<div class="menu-form-alert">
<div style="display: flex; align-items: center; white-space: nowrap; margin-bottom: 10px">
<span>默认接口权限:</span>
<el-select v-model="default_selectBtn.data_range" @change="defaulthandlePermissionRangeChange"
placeholder="请选择" style="margin-left: 5px; width: 250px; min-width: 250px">
<el-option v-for="item in dataPermissionRange" :key="item.value" :label="item.label"
:value="item.value" />
</el-select>
<el-tree-select v-show="default_selectBtn.data_range === 4" node-key="id"
v-model="default_selectBtn.dept" :props="defaultTreeProps" :data="deptData"
@change="customhandlePermissionRangeChange(default_selectBtn.dept)" placeholder="请选择自定义部门" multiple
check-strictly :render-after-expand="false" show-checkbox class="dialog-tree"
style="margin-left: 15px; width: AUTO; min-width: 250px; margin-top: 0" />
</div>
<span>配置操作功能接口权限,配置数据权限点击小齿轮</span>
</div>
<div style="margin-bottom: 20px;">
<el-button type="primary" @click="handleQuickQuery('快速查询')">快速查询</el-button>
<el-button type="primary" @click="handleQuickQuery('标准业务')">标准业务</el-button>
</div>
<el-checkbox v-for="btn in RoleMenuBtn.$state" :key="btn.id" v-model="btn.isCheck"
@change="handleCheckChange(btn)">
<div class="btn-item">
{{ btn.data_range !== null ? `${btn.name}(${formatDataRange(btn.data_range, btn.dept)})` : btn.name }}
<span v-show="btn.isCheck" @click.stop.prevent="handleSettingClick(btn)">
<el-icon>
<Setting />
</el-icon>
</span>
<span>【{{ btn.api }}】</span>
</div>
</el-checkbox>
</div>
默认接口权限在此作为批量勾选的基准,避免为每个按钮重复配置数据范围。
按钮勾选与快速配置通过统一接口完成写入,并同步更新本地状态。
ts
const handleCheckChange = async (btn: RoleMenuBtnType) => {
selectBtn.value = default_selectBtn.value;
const put_data = {
isCheck: btn.isCheck,
roleId: RoleDrawer.roleId,
menuId: RoleMenuTree.id,
btnId: btn.id,
data_range: default_selectBtn.value.data_range,
dept: default_selectBtn.value.dept,
};
const { data, msg } = await setRoleMenuBtn(put_data);
RoleMenuBtn.updateState(data);
ElMessage({ message: msg, type: 'success' });
};
该逻辑保证勾选行为与后端状态保持同步,否则会导致前端显示与实际权限不一致。
快捷配置基于后端预设返回按钮集合,再批量写入权限。
ts
const handleQuickQuery = async (type: string) => {
const { data } = await getRoleMenuBtnField({
roleId: RoleDrawer.roleId,
menuId: RoleMenuTree.id,
select: type,
});
const names = data.menu_btn.map((item: any) => item.name);
const updatedButtons = RoleMenuBtn.$state.map((btn: RoleMenuBtnType) => ({
...btn,
isCheck: names.includes(btn.name)
}));
RoleMenuBtn.updateBatchState(updatedButtons);
const promises = updatedButtons
.filter(btn => names.includes(btn.name))
.map(async (btn) => {
const put_data = {
isCheck: btn.isCheck,
roleId: RoleDrawer.roleId,
menuId: RoleMenuTree.id,
btnId: btn.id,
data_range: default_selectBtn.value.data_range,
dept: default_selectBtn.value.dept,
};
return await setRoleMenuBtn(put_data);
});
try {
await Promise.all(promises);
ElMessage({ message: '快速配置成功', type: 'success' });
} catch (error) {
ElMessage({ message: '快速配置失败', type: 'error' });
}
}
单按钮的数据范围通过弹窗单独配置,确认后立即持久化。
ts
const handleDialogConfirm = async () => {
const { data, msg } = await setRoleMenuBtnDataRange(selectBtn.value);
selectBtn.value = data;
dialogVisible.value = false;
ElMessage({ message: msg, type: 'success' });
};
接口统一封装在组件内的 api.ts,确保权限相关读写路径集中维护。
ts
export function getRoleMenu(query: object) {
return request({
url: '/api/system/role_menu_button_permission/get_role_menu/',
method: 'get',
params: query,
}).then((res: any) => {
return XEUtils.toArrayTree(res.data, { key: 'id', parentKey: 'parent', children: 'children', strict: false });
});
}
/**
* 设置 角色-菜单
*/
export function setRoleMenu(data: object) {
return request({
url: '/api/system/role_menu_button_permission/set_role_menu/',
method: 'put',
data,
});
}
角色列表页通过权限控制暴露入口,避免无权限访问配置能力。
ts
permission: {
type: 'primary',
text: '权限配置',
show: auth('role:Permission'),
click: (clickContext: any): void => {
const { row } = clickContext;
context.RoleDrawer.handleDrawerOpen(row);
context.RoleMenuBtn.setState([]);
context.RoleMenuField.setState([]);
},
},
总结
该模块通过权限配置抽屉将部门、菜单与接口权限聚合在单一操作空间内,避免多页面切换造成的状态割裂,同时以默认接口权限作为批量授权的核心锚点。在实现层面,勾选即存与弹窗即存降低了配置遗漏风险,但接口调用频繁,对后端幂等性与性能提出更高要求,若重新设计可引入批量提交或事务化策略以减少请求数量。
整体链路在权限渲染、数据一致性与维护成本之间取得平衡,为复杂业务场景下的角色权限管理提供了一套可复用的实现范式。