更新权限

This commit is contained in:
吴红兵
2026-02-14 19:55:49 +08:00
parent 920275335f
commit f893b9efc9
3 changed files with 431 additions and 43 deletions

View File

@@ -61,6 +61,54 @@ export const delObj = (ids: Object) => {
data: ids,
});
};
/**
* 批量设置角色分组
* @param roleIds 角色ID列表
* @param roleGroup 分组名称(空表示未分组)
*/
export const batchUpdateRoleGroup = (roleIds: string[], roleGroup: string) => {
return request({
url: '/admin/role/batchGroup',
method: 'put',
data: { roleIds, roleGroup: roleGroup || '' },
});
};
/**
* 批量指定角色关联用户
* @param roleId 角色ID
* @param userIds 用户ID列表
*/
export const assignUsersToRole = (roleId: string, userIds: string[]) => {
return request({
url: '/admin/role/assignUsers',
method: 'post',
data: { roleId, userIds },
});
};
/**
* 根据角色ID查询该角色下绑定的用户列表含部门、姓名、工号
* @param roleId 角色ID
*/
export const getUsersByRoleId = (roleId: string) => {
return request({
url: '/admin/role/users/' + roleId,
method: 'get',
});
};
/**
* 解除指定用户与该角色的关联
* @param roleId 角色ID
* @param userId 用户ID
*/
export const unassignUserFromRole = (roleId: string, userId: string) => {
return request({
url: `/admin/role/users/${roleId}/${userId}`,
method: 'delete',
});
};
export const permissionUpd = (roleId: string, menuIds: string) => {
return request({

View File

@@ -7,6 +7,12 @@
<el-form-item :label="$t('sysrole.roleCode')" prop="roleCode">
<el-input :placeholder="$t('sysrole.please_enter_the_role_Code')" :disabled="form.roleId !== ''" clearable v-model="form.roleCode"></el-input>
</el-form-item>
<el-form-item label="分组" prop="roleGroup">
<el-input placeholder="用于列表树状分组展示,可留空" clearable v-model="form.roleGroup" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="排序" prop="roleSort">
<el-input-number v-model="form.roleSort" :min="0" :max="9999" placeholder="数值越小越靠前" controls-position="right" style="width: 140px" />
</el-form-item>
<el-form-item :label="$t('sysrole.roleDesc')" prop="roleDesc">
<el-input
maxlength="100"
@@ -67,6 +73,8 @@ const form = reactive({
roleId: '',
roleName: '',
roleCode: '',
roleGroup: '',
roleSort: 0,
roleDesc: '',
dsType: 0,
dsScope: '',
@@ -187,6 +195,7 @@ const getRoleData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
if (res.data.roleSort == null) form.roleSort = 0;
if (res.data.dsScope) {
dataForm.checkedDsScope = res.data.dsScope.split(',');
} else {

View File

@@ -25,6 +25,12 @@
<el-button plain :disabled="multiple" icon="Delete" type="primary" class="ml10" v-auth="'sys_user_del'" @click="handleDelete(selectObjs)">
{{ $t('common.delBtn') }}
</el-button>
<el-button plain :disabled="multiple" type="primary" class="ml10" v-auth="'sys_role_edit'" @click="showBatchGroupDialog = true">
批量指定分组
</el-button>
<el-button plain :disabled="selectObjs.length !== 1" type="primary" class="ml10" v-auth="'sys_role_edit'" @click="openAssignUserDialog">
批量指定关联用户
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'sys_role_export'"
@@ -36,36 +42,59 @@
</div>
</el-row>
<el-table
:data="state.dataList"
:data="roleTreeData"
v-loading="state.loading"
style="width: 100%"
row-key="roleId"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@selection-change="handleSelectionChange"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" :selectable="handleSelectable" width="50" align="center" />
<el-table-column type="index" :label="$t('sysrole.index')" width="80" />
<el-table-column prop="roleName" :label="$t('sysrole.roleName')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleCode" :label="$t('sysrole.roleCode')" show-overflow-tooltip></el-table-column>
<el-table-column prop="roleDesc" :label="$t('sysrole.roleDesc')" show-overflow-tooltip></el-table-column>
<el-table-column prop="data_authority" :label="$t('sysrole.data_authority')" show-overflow-tooltip>
<el-table-column type="selection" :selectable="handleSelectable" width="50" align="left" />
<el-table-column type="index" :label="$t('sysrole.index')" width="80">
<template #default="scope">
<dict-tag :options="dictType" :value="scope.row.dsType"></dict-tag>
<span v-if="scope.row._isGroup"></span>
<span v-else>{{ scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('sysrole.createTime')" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="250">
<el-table-column prop="roleName" :label="$t('sysrole.roleName')" show-overflow-tooltip min-width="140" align="left">
<template #default="scope">
<span v-if="scope.row._isGroup" class="role-group-name">{{ scope.row.roleName }}</span>
<span v-else>{{ scope.row.roleName }}</span>
</template>
</el-table-column>
<el-table-column prop="roleSort" label="排序" width="80" align="center">
<template #default="scope">{{ scope.row._isGroup ? '—' : (scope.row.roleSort ?? 0) }}</template>
</el-table-column>
<el-table-column prop="roleCode" :label="$t('sysrole.roleCode')" show-overflow-tooltip min-width="120">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.roleCode }}</template>
</el-table-column>
<el-table-column prop="roleDesc" :label="$t('sysrole.roleDesc')" show-overflow-tooltip min-width="140">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.roleDesc }}</template>
</el-table-column>
<el-table-column prop="data_authority" :label="$t('sysrole.data_authority')" show-overflow-tooltip width="100">
<template #default="scope">
<template v-if="scope.row._isGroup"></template>
<dict-tag v-else :options="dictType" :value="scope.row.dsType"></dict-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('sysrole.createTime')" show-overflow-tooltip width="165">
<template #default="scope">{{ scope.row._isGroup ? '—' : scope.row.createTime }}</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="250" fixed="right">
<template #default="scope">
<template v-if="scope.row._isGroup"></template>
<template v-else>
<el-button text type="primary" icon="edit-pen" v-auth="'sys_role_edit'" @click="roleDialogRef.openDialog(scope.row.roleId)">{{
$t('common.editBtn')
}}</el-button>
<el-button text type="primary" icon="turn-off" v-auth="'sys_role_perm'" @click="permessionRef.openDialog(scope.row)">{{
$t('sysrole.permissionTip')
}}</el-button>
<el-button text type="primary" icon="user" v-auth="'sys_role_view'" @click="openRoleUsersDialog(scope.row)">查看关联用户</el-button>
<el-tooltip :content="$t('sysrole.deleteDisabledTip')" :disabled="scope.row.roleId !== '1'" placement="top">
<span style="margin-left: 12px">
<el-button
@@ -80,11 +109,98 @@
</span>
</el-tooltip>
</template>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 批量指定分组弹窗 -->
<el-dialog v-model="showBatchGroupDialog" title="批量指定分组" width="400px" @close="batchGroupName = ''">
<el-form label-width="80px">
<el-form-item label="分组名称">
<el-input v-model="batchGroupName" placeholder="输入分组名,留空为未分组" clearable maxlength="50" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showBatchGroupDialog = false">取消</el-button>
<el-button type="primary" :loading="batchGroupLoading" @click="handleBatchGroup">确定</el-button>
</template>
</el-dialog>
<!-- 批量指定关联用户弹窗 -->
<el-dialog
v-model="showAssignUserDialog"
title="批量指定关联用户"
width="720px"
destroy-on-close
@close="assignUserKeyword = ''; assignUserType = ''; assignUserList = []; assignUserTree = []; assignSelectedIds = []">
<template v-if="assignCurrentRole">
<div class="mb12"><el-text type="info">当前角色{{ assignCurrentRole.roleName }}{{ assignCurrentRole.roleCode }}</el-text></div>
<el-form :inline="true" class="mb12">
<el-form-item label="用户类型" required>
<el-radio-group v-model="assignUserType">
<el-radio label="1">教职工</el-radio>
<el-radio label="2">学生</el-radio>
<el-radio label="3">驻校单位</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="姓名/工号">
<el-input v-model="assignUserKeyword" placeholder="姓名或工号检索" clearable style="width: 180px" @keyup.enter="loadAssignUserList" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="assignUserLoading" @click="loadAssignUserList">查询</el-button>
</el-form-item>
</el-form>
<div class="assign-user-tip mb8" v-if="!assignUserType">请先选择用户类型后再查询</div>
<el-table
v-else
:data="assignUserTree"
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
default-expand-all
v-loading="assignUserLoading"
max-height="360"
border
@selection-change="handleAssignUserSelectionChange">
<el-table-column type="selection" width="50" align="left" :selectable="(row: any) => !row._isDept" />
<el-table-column prop="label" label="部门 / 姓名" min-width="200">
<template #default="{ row }">
<span v-if="row._isDept" class="dept-row">{{ row.label }}</span>
<span v-else>{{ row.realName || row.username || row.label }}</span>
</template>
</el-table-column>
<el-table-column prop="username" label="工号" width="120">
<template #default="{ row }">{{ row._isDept ? '—' : row.username }}</template>
</el-table-column>
<el-table-column prop="deptName" label="部门" width="140">
<template #default="{ row }">{{ row._isDept ? '—' : (row.deptName || '—') }}</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<el-button @click="showAssignUserDialog = false">取消</el-button>
<el-button type="primary" :loading="assignSubmitLoading" :disabled="assignSelectedIds.length === 0" @click="handleAssignUsersSubmit">确定已选 {{ assignSelectedIds.length }} </el-button>
</template>
</el-dialog>
<!-- 查看角色关联用户弹窗 -->
<el-dialog v-model="showRoleUsersDialog" title="关联用户" width="560px" destroy-on-close>
<template v-if="currentRoleForUsers">
<div class="mb12"><el-text type="info">角色{{ currentRoleForUsers.roleName }}{{ currentRoleForUsers.roleCode }}</el-text></div>
<el-table :data="roleUsersList" v-loading="roleUsersLoading" max-height="400" border>
<el-table-column prop="deptName" label="部门" min-width="140" show-overflow-tooltip />
<el-table-column prop="realName" label="姓名" width="100" show-overflow-tooltip />
<el-table-column prop="username" label="工号" width="120" show-overflow-tooltip />
<el-table-column label="操作" width="80" fixed="right">
<template #default="{ row }">
<el-button text type="danger" icon="delete" v-auth="'sys_role_edit'" @click="handleUnassignUser(row)">解除</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt8" v-if="!roleUsersLoading && roleUsersList.length === 0"><el-text type="info">该角色下暂无关联用户</el-text></div>
</template>
</el-dialog>
<!-- 角色编辑新增 -->
<role-dialog ref="roleDialogRef" @refresh="getDataList()" />
<!-- 导入角色 -->
@@ -101,8 +217,10 @@
</template>
<script setup lang="ts" name="systemRole">
import { computed, reactive, ref } from 'vue';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { pageList, delObj } from '/@/api/admin/role';
import { list, delObj, batchUpdateRoleGroup, assignUsersToRole, getUsersByRoleId, unassignUserFromRole } from '/@/api/admin/role';
import { pageList as userPageList } from '/@/api/admin/user';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
@@ -121,15 +239,103 @@ const showSearch = ref(true);
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
// 批量指定分组
const showBatchGroupDialog = ref(false);
const batchGroupName = ref('');
const batchGroupLoading = ref(false);
// 查看角色关联用户
const showRoleUsersDialog = ref(false);
const roleUsersList = ref<any[]>([]);
const roleUsersLoading = ref(false);
const currentRoleForUsers = ref<{ roleName: string; roleCode: string; roleId: string } | null>(null);
// 批量指定关联用户
const showAssignUserDialog = ref(false);
const assignUserType = ref('');
const assignUserKeyword = ref('');
const assignUserList = ref<any[]>([]);
const assignUserLoading = ref(false);
const assignSubmitLoading = ref(false);
const assignSelectedIds = ref<string[]>([]);
const assignCurrentRole = computed(() => {
const id = selectObjs.value && selectObjs.value[0];
if (!id) return null;
return (state.dataList || []).find((r: any) => r.roleId === id) || null;
});
/** 按部门分组的用户树(用于表格树形展示) */
const assignUserTree = computed(() => {
const list = assignUserList.value || [];
const map = new Map<string, any[]>();
list.forEach((u: any) => {
const deptKey = u.deptName && String(u.deptName).trim() ? u.deptName : '未分配部门';
if (!map.has(deptKey)) map.set(deptKey, []);
map.get(deptKey)!.push({ ...u, id: u.userId, _isDept: false });
});
const result: any[] = [];
map.forEach((users, deptName) => {
result.push({
id: `dept_${deptName}`,
label: deptName,
_isDept: true,
children: users,
});
});
result.sort((a, b) => a.label.localeCompare(b.label));
return result;
});
// 列表不分页,显示全部数据
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
roleName: '',
},
pageList: pageList, // H
isPage: false,
pageList: async (params: any) => {
const res = await list(params);
let data = res?.data || [];
if (Array.isArray(data) && params?.roleName) {
const kw = String(params.roleName).trim().toLowerCase();
if (kw) {
data = data.filter(
(r: any) =>
(r.roleName && r.roleName.toLowerCase().includes(kw)) ||
(r.roleCode && r.roleCode.toLowerCase().includes(kw))
);
}
}
return { data };
},
descs: ['create_time'],
});
/** 按分组构建树形数据:分组为父节点,角色为子节点 */
const roleTreeData = computed(() => {
const list = state.dataList || [];
const groupMap = new Map<string, any[]>();
list.forEach((row: any) => {
const groupName = row.roleGroup && String(row.roleGroup).trim() ? String(row.roleGroup).trim() : '未分组';
if (!groupMap.has(groupName)) groupMap.set(groupName, []);
groupMap.get(groupName)!.push(row);
});
const result: any[] = [];
groupMap.forEach((roles, groupName) => {
result.push({
roleId: `group_${groupName}`,
roleName: groupName,
_isGroup: true,
children: roles,
});
});
// 未分组放最后,其余按分组名排序
result.sort((a, b) => {
if (a.roleName === '未分组') return 1;
if (b.roleName === '未分组') return -1;
return a.roleName.localeCompare(b.roleName);
});
return result;
});
const dictType = ref([
{
label: '全部',
@@ -153,8 +359,8 @@ const dictType = ref([
},
]);
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// table hook(无分页,不暴露 currentChangeHandle/sizeChangeHandle
const { getDataList, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
@@ -167,15 +373,15 @@ const exportExcel = () => {
downBlobFile('/admin/role/export',Object.assign(state.queryForm,{ids:selectObjs}), 'role.xlsx');
};
// 是否可以多选
// 是否可以多选(分组行不可选,管理员角色不可选)
const handleSelectable = (row: any) => {
return row.roleId !== '1';
return !row._isGroup && row.roleId !== '1';
};
// 多选事件
// 多选事件(仅角色行可选,排除分组行)
const handleSelectionChange = (objs: { roleId: string }[]) => {
selectObjs.value = objs.map(({ roleId }) => roleId);
multiple.value = !objs.length;
selectObjs.value = objs.map(({ roleId }) => roleId).filter((id: string) => !String(id).startsWith('group_'));
multiple.value = !selectObjs.value.length;
};
// 删除操作
@@ -194,4 +400,129 @@ const handleDelete = async (ids: string[]) => {
useMessage().error(err.msg);
}
};
async function loadRoleUsersList() {
const role = currentRoleForUsers.value;
if (!role) return;
roleUsersLoading.value = true;
try {
const res = await getUsersByRoleId(role.roleId);
const data = res?.data;
roleUsersList.value = Array.isArray(data) ? data : [];
} catch {
roleUsersList.value = [];
} finally {
roleUsersLoading.value = false;
}
}
async function openRoleUsersDialog(row: any) {
currentRoleForUsers.value = { roleName: row.roleName, roleCode: row.roleCode, roleId: row.roleId };
showRoleUsersDialog.value = true;
roleUsersList.value = [];
await loadRoleUsersList();
}
async function handleUnassignUser(row: any) {
const roleId = currentRoleForUsers.value?.roleId;
const userId = row.userId;
if (!roleId || !userId) return;
try {
await useMessageBox().confirm(`确定解除「${row.realName || row.username}」与该角色的关联?`);
} catch {
return;
}
try {
await unassignUserFromRole(roleId, userId);
useMessage().success('已解除关联');
await loadRoleUsersList();
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
}
}
function openAssignUserDialog() {
if (selectObjs.value.length !== 1) return;
showAssignUserDialog.value = true;
assignUserType.value = '';
assignUserKeyword.value = '';
assignUserList.value = [];
assignSelectedIds.value = [];
}
async function loadAssignUserList() {
if (!assignUserType.value) return;
assignUserLoading.value = true;
try {
const res = await userPageList({
current: 1,
size: 2000,
userType: assignUserType.value,
realName: assignUserKeyword.value ? assignUserKeyword.value.trim() : undefined,
});
const records = res?.data?.records ?? res?.records ?? (Array.isArray(res?.data) ? res.data : []);
assignUserList.value = Array.isArray(records) ? records : [];
} catch (e) {
assignUserList.value = [];
} finally {
assignUserLoading.value = false;
}
}
function handleAssignUserSelectionChange(rows: any[]) {
assignSelectedIds.value = (rows || [])
.filter((r: any) => !r._isDept && r.userId)
.map((r: any) => r.userId);
}
async function handleAssignUsersSubmit() {
const roleId = assignCurrentRole.value?.roleId;
if (!roleId || !assignSelectedIds.value.length) {
useMessage().warning('请选择要关联的用户');
return;
}
assignSubmitLoading.value = true;
try {
await assignUsersToRole(roleId, assignSelectedIds.value);
useMessage().success('已关联 ' + assignSelectedIds.value.length + ' 名用户');
showAssignUserDialog.value = false;
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
} finally {
assignSubmitLoading.value = false;
}
}
// 批量指定分组
const handleBatchGroup = async () => {
const ids = selectObjs.value || [];
if (!ids.length) {
useMessage().warning('请先勾选要设置分组的角色');
return;
}
batchGroupLoading.value = true;
try {
await batchUpdateRoleGroup(ids, batchGroupName.value || '');
useMessage().success('分组已更新');
showBatchGroupDialog.value = false;
batchGroupName.value = '';
getDataList();
} catch (err: any) {
useMessage().error(err?.msg || '操作失败');
} finally {
batchGroupLoading.value = false;
}
};
</script>
<style scoped lang="scss">
.role-group-name {
font-weight: 600;
color: var(--el-text-color-primary);
}
.mb12 { margin-bottom: 12px; }
.mb8 { margin-bottom: 8px; }
.mt8 { margin-top: 8px; }
.assign-user-tip { color: var(--el-text-color-secondary); font-size: 13px; }
.dept-row { font-weight: 600; color: var(--el-text-color-primary); }
</style>