380 lines
12 KiB
Vue
380 lines
12 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="permission-container">
|
|||
|
|
<el-row :gutter="20">
|
|||
|
|
<!-- 左侧:用户列表 -->
|
|||
|
|
<el-col :span="8">
|
|||
|
|
<el-card class="role-card">
|
|||
|
|
<template #header>
|
|||
|
|
<div class="card-header">
|
|||
|
|
<span class="card-title">用户列表</span>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<el-table
|
|||
|
|
:data="paginatedRoleList"
|
|||
|
|
highlight-current-row
|
|||
|
|
@current-change="handleRoleChange"
|
|||
|
|
v-loading="roleLoading"
|
|||
|
|
style="width: 100%"
|
|||
|
|
max-height="500"
|
|||
|
|
>
|
|||
|
|
<el-table-column prop="name" label="用户名称" width="120" />
|
|||
|
|
<el-table-column prop="mobile" label="手机号" />
|
|||
|
|
</el-table>
|
|||
|
|
|
|||
|
|
<!-- 分页器 -->
|
|||
|
|
<div style="margin-top: 15px; text-align: center">
|
|||
|
|
<el-pagination
|
|||
|
|
v-model:current-page="pagination.currentPage"
|
|||
|
|
v-model:page-size="pagination.pageSize"
|
|||
|
|
:page-sizes="[10, 20, 50, 100]"
|
|||
|
|
:total="roleList.length"
|
|||
|
|
layout="total, sizes, prev, pager, next"
|
|||
|
|
@size-change="handleSizeChange"
|
|||
|
|
@current-change="handleCurrentChange"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</el-card>
|
|||
|
|
</el-col>
|
|||
|
|
|
|||
|
|
<!-- 右侧:菜单权限分配 -->
|
|||
|
|
<el-col :span="16">
|
|||
|
|
<el-card class="permission-card">
|
|||
|
|
<template #header>
|
|||
|
|
<div class="card-header">
|
|||
|
|
<span class="card-title">
|
|||
|
|
{{ currentRole ? `${currentRole.name} - 菜单访问权限` : '请选择角色' }}
|
|||
|
|
</span>
|
|||
|
|
<el-button
|
|||
|
|
type="primary"
|
|||
|
|
size="small"
|
|||
|
|
@click="handleSaveMenuPermissions"
|
|||
|
|
:disabled="!currentRole"
|
|||
|
|
:loading="saveLoading"
|
|||
|
|
>
|
|||
|
|
保存菜单权限
|
|||
|
|
</el-button>
|
|||
|
|
<el-button
|
|||
|
|
type="success"
|
|||
|
|
size="small"
|
|||
|
|
@click="handleQuickAssignAll"
|
|||
|
|
:disabled="!currentRole"
|
|||
|
|
:loading="quickAssignLoading"
|
|||
|
|
style="margin-left: 10px"
|
|||
|
|
>
|
|||
|
|
一键分配全部权限
|
|||
|
|
</el-button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<div v-if="currentRole" v-loading="permissionLoading">
|
|||
|
|
<el-alert
|
|||
|
|
title="提示"
|
|||
|
|
type="info"
|
|||
|
|
:closable="false"
|
|||
|
|
style="margin-bottom: 20px"
|
|||
|
|
>
|
|||
|
|
勾选菜单和按钮后,该用户登录系统时可以访问这些菜单页面和执行相应的操作
|
|||
|
|
</el-alert>
|
|||
|
|
|
|||
|
|
<el-tree
|
|||
|
|
ref="menuTreeRef"
|
|||
|
|
:data="menuTree"
|
|||
|
|
show-checkbox
|
|||
|
|
node-key="id"
|
|||
|
|
:default-expand-all="true"
|
|||
|
|
:props="treeProps"
|
|||
|
|
:check-strictly="false"
|
|||
|
|
:default-checked-keys="checkedMenuIds"
|
|||
|
|
>
|
|||
|
|
<template #default="{ node, data }">
|
|||
|
|
<span class="tree-node">
|
|||
|
|
<el-icon v-if="data.icon" style="margin-right: 5px">
|
|||
|
|
<component :is="data.icon" />
|
|||
|
|
</el-icon>
|
|||
|
|
<span>{{ node.label }}</span>
|
|||
|
|
<el-tag
|
|||
|
|
v-if="data.type === 1"
|
|||
|
|
type="success"
|
|||
|
|
size="small"
|
|||
|
|
style="margin-left: 10px"
|
|||
|
|
>
|
|||
|
|
菜单
|
|||
|
|
</el-tag>
|
|||
|
|
<el-tag
|
|||
|
|
v-if="data.type === 2"
|
|||
|
|
type="warning"
|
|||
|
|
size="small"
|
|||
|
|
style="margin-left: 10px"
|
|||
|
|
>
|
|||
|
|
按钮
|
|||
|
|
</el-tag>
|
|||
|
|
<span
|
|||
|
|
v-if="data.authority"
|
|||
|
|
style="margin-left: 10px; color: #999; font-size: 12px"
|
|||
|
|
>
|
|||
|
|
{{ data.authority }}
|
|||
|
|
</span>
|
|||
|
|
</span>
|
|||
|
|
</template>
|
|||
|
|
</el-tree>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<el-empty
|
|||
|
|
v-else
|
|||
|
|
description="请从左侧选择一个角色,为其分配菜单访问权限"
|
|||
|
|
:image-size="100"
|
|||
|
|
/>
|
|||
|
|
</el-card>
|
|||
|
|
</el-col>
|
|||
|
|
</el-row>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, reactive, onMounted, nextTick, computed } from 'vue';
|
|||
|
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
|||
|
|
import {
|
|||
|
|
getUserList,
|
|||
|
|
getMenuTree,
|
|||
|
|
getRoleMenuIds,
|
|||
|
|
assignRoleMenus,
|
|||
|
|
getMenuList,
|
|||
|
|
} from '@/api/permission.js';
|
|||
|
|
|
|||
|
|
// 角色相关数据
|
|||
|
|
const roleLoading = ref(false);
|
|||
|
|
const roleList = ref([]);
|
|||
|
|
const currentRole = ref(null);
|
|||
|
|
|
|||
|
|
// 分页数据
|
|||
|
|
const pagination = reactive({
|
|||
|
|
currentPage: 1,
|
|||
|
|
pageSize: 10,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 计算分页后的用户列表
|
|||
|
|
const paginatedRoleList = computed(() => {
|
|||
|
|
const start = (pagination.currentPage - 1) * pagination.pageSize;
|
|||
|
|
const end = start + pagination.pageSize;
|
|||
|
|
return roleList.value.slice(start, end);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 权限相关数据
|
|||
|
|
const permissionLoading = ref(false);
|
|||
|
|
const saveLoading = ref(false);
|
|||
|
|
const quickAssignLoading = ref(false);
|
|||
|
|
const menuTree = ref([]);
|
|||
|
|
const menuTreeRef = ref(null);
|
|||
|
|
const checkedMenuIds = ref([]);
|
|||
|
|
const treeProps = {
|
|||
|
|
children: 'children',
|
|||
|
|
label: 'label',
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 初始化
|
|||
|
|
onMounted(() => {
|
|||
|
|
loadRoleList();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加载用户列表
|
|||
|
|
const loadRoleList = async () => {
|
|||
|
|
roleLoading.value = true;
|
|||
|
|
try {
|
|||
|
|
const res = await getUserList();
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
roleList.value = res.data || [];
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载用户列表失败:', error);
|
|||
|
|
ElMessage.error('加载用户列表失败');
|
|||
|
|
} finally {
|
|||
|
|
roleLoading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 角色选择改变
|
|||
|
|
const handleRoleChange = async (row) => {
|
|||
|
|
if (!row) return;
|
|||
|
|
|
|||
|
|
currentRole.value = row;
|
|||
|
|
await loadMenuTree();
|
|||
|
|
await loadRoleMenus(row.roleId);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 加载菜单树(显示所有菜单,包括菜单和按钮)
|
|||
|
|
const loadMenuTree = async () => {
|
|||
|
|
permissionLoading.value = true;
|
|||
|
|
try {
|
|||
|
|
const res = await getMenuTree();
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
// 显示所有菜单和按钮,不进行过滤
|
|||
|
|
menuTree.value = res.data || [];
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载菜单树失败:', error);
|
|||
|
|
ElMessage.error('加载菜单树失败');
|
|||
|
|
} finally {
|
|||
|
|
permissionLoading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 加载角色已分配的菜单
|
|||
|
|
const loadRoleMenus = async (roleId) => {
|
|||
|
|
try {
|
|||
|
|
const res = await getRoleMenuIds(roleId);
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
checkedMenuIds.value = res.data || [];
|
|||
|
|
|
|||
|
|
await nextTick();
|
|||
|
|
if (menuTreeRef.value) {
|
|||
|
|
menuTreeRef.value.setCheckedKeys(checkedMenuIds.value);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('加载角色菜单失败:', error);
|
|||
|
|
ElMessage.error('加载角色菜单失败');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 分页处理
|
|||
|
|
const handleSizeChange = (size) => {
|
|||
|
|
pagination.pageSize = size;
|
|||
|
|
pagination.currentPage = 1;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleCurrentChange = (page) => {
|
|||
|
|
pagination.currentPage = page;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 保存菜单权限
|
|||
|
|
const handleSaveMenuPermissions = async () => {
|
|||
|
|
if (!currentRole.value) {
|
|||
|
|
ElMessage.warning('请先选择用户');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取选中的节点(包括半选中的父节点)
|
|||
|
|
const checkedKeys = menuTreeRef.value.getCheckedKeys();
|
|||
|
|
const halfCheckedKeys = menuTreeRef.value.getHalfCheckedKeys();
|
|||
|
|
const allKeys = [...checkedKeys, ...halfCheckedKeys];
|
|||
|
|
|
|||
|
|
saveLoading.value = true;
|
|||
|
|
try {
|
|||
|
|
const res = await assignRoleMenus({
|
|||
|
|
roleId: currentRole.value.roleId,
|
|||
|
|
menuIds: allKeys,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
ElMessage.success('菜单权限保存成功');
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(res.msg || '保存失败');
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('保存菜单权限失败:', error);
|
|||
|
|
ElMessage.error('保存失败');
|
|||
|
|
} finally {
|
|||
|
|
saveLoading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 一键分配全部权限
|
|||
|
|
const handleQuickAssignAll = async () => {
|
|||
|
|
if (!currentRole.value) {
|
|||
|
|
ElMessage.warning('请先选择用户');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 确认操作
|
|||
|
|
const confirmed = await ElMessageBox.confirm(
|
|||
|
|
`确定要为用户 ${currentRole.value.name} (${currentRole.value.mobile}) 分配所有菜单权限吗?`,
|
|||
|
|
'确认分配权限',
|
|||
|
|
{
|
|||
|
|
confirmButtonText: '确定',
|
|||
|
|
cancelButtonText: '取消',
|
|||
|
|
type: 'warning',
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (!confirmed) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
quickAssignLoading.value = true;
|
|||
|
|
|
|||
|
|
// 获取所有菜单
|
|||
|
|
const menuListRes = await getMenuList();
|
|||
|
|
if (menuListRes.code !== 200) {
|
|||
|
|
throw new Error('获取菜单列表失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const allMenus = menuListRes.data || [];
|
|||
|
|
const allMenuIds = allMenus.map(menu => menu.id);
|
|||
|
|
|
|||
|
|
console.log('=== 一键分配全部权限 ===', {
|
|||
|
|
user: currentRole.value,
|
|||
|
|
totalMenus: allMenus.length,
|
|||
|
|
menuIds: allMenuIds
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 分配所有菜单权限
|
|||
|
|
const res = await assignRoleMenus({
|
|||
|
|
roleId: currentRole.value.roleId,
|
|||
|
|
menuIds: allMenuIds,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (res.code === 200) {
|
|||
|
|
ElMessage.success(`成功为用户 ${currentRole.value.name} 分配了 ${allMenuIds.length} 个菜单权限`);
|
|||
|
|
|
|||
|
|
// 重新加载权限显示
|
|||
|
|
await loadRoleMenus(currentRole.value.roleId);
|
|||
|
|
} else {
|
|||
|
|
ElMessage.error(res.msg || '分配失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
if (error !== 'cancel') {
|
|||
|
|
console.error('一键分配权限失败:', error);
|
|||
|
|
ElMessage.error(`分配失败: ${error.message || error}`);
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
quickAssignLoading.value = false;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.permission-container {
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.role-card,
|
|||
|
|
.permission-card {
|
|||
|
|
border-radius: 8px;
|
|||
|
|
min-height: 600px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-title {
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tree-node {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
:deep(.el-tree-node__content) {
|
|||
|
|
height: 36px;
|
|||
|
|
}
|
|||
|
|
</style>
|