Merge pull request '🧮优化账号角色权限' (#143) from KaneLeung/Admin.NET.Pro:main into main

Reviewed-on: http://101.43.53.74:3000/Admin.NET/Admin.NET.Pro/pulls/143
This commit is contained in:
zuohuaijun 2024-09-30 18:42:39 +08:00
commit 120a544001
7 changed files with 355 additions and 22 deletions

View File

@ -25,6 +25,11 @@ public class RoleOutput
/// 编码
/// </summary>
public string Code { get; set; }
/// <summary>
/// 是否禁用
/// </summary>
public bool Disabled { get; set; } = true;
}
/// <summary>
@ -36,4 +41,19 @@ public class PageRoleOutput : SysRole
/// 租户名称
/// </summary>
public string TenantName { get; set; }
}
/// <summary>
/// 角色已分配可分配输出参数
/// </summary>
public class GrantRoleOutput
{
/// <summary>
/// 以分配
/// </summary>
public IEnumerable<RoleOutput> Granted { get; set; }
/// <summary>
/// 可分配
/// </summary>
public IEnumerable<RoleOutput> Available { get; set; }
}

View File

@ -79,7 +79,7 @@ public class SysRoleService : IDynamicApiController, ITransient
return await _sysRoleRep.AsQueryable()
.WhereIF(!_userManager.SuperAdmin, u => u.TenantId == _userManager.TenantId) // 若非超管,则只能操作本租户的角色
.WhereIF(!_userManager.SuperAdmin && !_userManager.SysAdmin, u => u.CreateUserId == _userManager.UserId || roleIdList.Contains(u.Id)) // 若非超管且非系统管理员,则只显示自己创建和已拥有的角色
.OrderBy(u => new { u.OrderNo, u.Id }).Select<RoleOutput>().ToListAsync();
.OrderBy(u => new { u.OrderNo, u.Id }).Select(u => new RoleOutput { Disabled = false }, true).ToListAsync();
}
/// <summary>

View File

@ -83,6 +83,19 @@ public class SysUserRoleService : ITransient
.Where(u => u.UserId == userId).Select(u => u.RoleId).ToListAsync();
}
/// <summary>
/// 根据用户Id获取角色集合
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<List<RoleOutput>> GetUserRoleInfoList(long userId)
{
return await _sysUserRoleRep.AsQueryable().Includes(u => u.SysRole)
.Where(u => u.UserId == userId)
.Select(u => new RoleOutput { Id = u.RoleId, Code = u.SysRole.Code, Name = u.SysRole.Name })
.ToListAsync();
}
/// <summary>
/// 根据角色Id获取用户Id集合
/// </summary>

View File

@ -15,6 +15,7 @@ public class SysUserService : IDynamicApiController, ITransient
private readonly UserManager _userManager;
private readonly SysOrgService _sysOrgService;
private readonly SysUserExtOrgService _sysUserExtOrgService;
private readonly SysRoleService _sysRoleService;
private readonly SysUserRoleService _sysUserRoleService;
private readonly SysConfigService _sysConfigService;
private readonly SysOnlineUserService _sysOnlineUserService;
@ -25,6 +26,7 @@ public class SysUserService : IDynamicApiController, ITransient
public SysUserService(UserManager userManager,
SysOrgService sysOrgService,
SysUserExtOrgService sysUserExtOrgService,
SysRoleService sysRoleService,
SysUserRoleService sysUserRoleService,
SysConfigService sysConfigService,
SysOnlineUserService sysOnlineUserService,
@ -35,6 +37,7 @@ public class SysUserService : IDynamicApiController, ITransient
_userManager = userManager;
_sysOrgService = sysOrgService;
_sysUserExtOrgService = sysUserExtOrgService;
_sysRoleService = sysRoleService;
_sysUserRoleService = sysUserRoleService;
_sysConfigService = sysConfigService;
_sysOnlineUserService = sysOnlineUserService;
@ -136,7 +139,7 @@ public class SysUserService : IDynamicApiController, ITransient
// 若账号的角色和组织架构发生变化,则强制下线账号进行权限更新
var user = await _sysUserRep.AsQueryable().ClearFilter().FirstAsync(u => u.Id == input.Id);
var roleIds = await GetOwnRoleList(input.Id);
var roleIds = await _sysUserRoleService.GetUserRoleIdList(input.Id);
if (input.OrgId != user.OrgId || !input.RoleIdList.OrderBy(u => u).SequenceEqual(roleIds.OrderBy(u => u)))
await _sysOnlineUserService.ForceOffline(input.Id);
// 更新域账号
@ -353,9 +356,17 @@ public class SysUserService : IDynamicApiController, ITransient
/// <param name="userId"></param>
/// <returns></returns>
[DisplayName("获取用户拥有角色集合")]
public async Task<List<long>> GetOwnRoleList(long userId)
public async Task<GrantRoleOutput> GetOwnRoleList(long userId)
{
return await _sysUserRoleService.GetUserRoleIdList(userId);
// 获取当前分配用户的角色
var granted = (await _sysUserRoleService.GetUserRoleInfoList(userId));
// 获取当前用户的角色
var available = await _sysRoleService.GetList();
// 改变分配用户的角色可分配状态
granted.ForEach(u => u.Disabled = !available.Any(e => e.Id == u.Id));
// 排除已分配的角色
available = available.ExceptBy(granted.Select(e => e.Id), e => e.Id).ToList();
return new GrantRoleOutput { Granted = granted, Available = available };
}
/// <summary>

View File

@ -0,0 +1,264 @@
<template>
<el-row :gutter="10">
<el-col :span="10">
<div class="transfer-panel">
<p class="transfer-panel__header">
<el-checkbox v-model="state.leftAllChecked" :indeterminate="leftIndeterminate" :validate-event="false" @change="handleLeftAllChecked"> {{ props.leftTitle }} </el-checkbox>
<span>{{ state.leftChecked.length }}/{{ props.leftData.length }}</span>
</p>
<div class="transfer-panel__body">
<el-input class="transfer-panel__filter" v-model="state.leftKeyword" placeholder="搜索" :prefix-icon="Search" clearable :validate-event="false" />
<el-checkbox-group v-show="true" v-model="state.leftChecked" :validate-event="false" class="transfer-panel__list">
<el-checkbox
v-for="(i, k) in leftFilterData"
:key="k"
:value="i[props.options.value]"
:label="i[props.options.label]"
:disabled="i[props.options.disabled]"
:validate-event="false"
class="transfer-panel__item"
>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</el-col>
<el-col :span="4" class="transfer-buttons">
<div class="transfer-buttons__item">
<el-button type="primary" style="width: 100%" :icon="ArrowRight" @click="toRight">往右</el-button>
</div>
<div class="transfer-buttons__item">
<el-button type="primary" style="width: 100%" :icon="ArrowLeft" @click="toLeft">往左</el-button>
</div>
<div class="transfer-buttons__item">
<el-button type="primary" style="width: 100%" :icon="DArrowRight" @click="allToRight">全部往右</el-button>
</div>
<div class="transfer-buttons__item">
<el-button type="primary" style="width: 100%" :icon="DArrowLeft" @click="allToLeft">全部往左</el-button>
</div>
</el-col>
<el-col :span="10">
<div class="transfer-panel">
<p class="transfer-panel__header">
<el-checkbox v-model="state.rightAllChecked" :indeterminate="rightIndeterminate" :validate-event="false" @change="handleRightAllChecked"> {{ props.rightTitle }} </el-checkbox>
<span>{{ state.rightChecked.length }}/{{ props.rightData.length }}</span>
</p>
<div class="transfer-panel__body">
<el-input class="transfer-panel__filter" v-model="state.rightKeyword" placeholder="搜索" :prefix-icon="Search" clearable :validate-event="false" />
<el-checkbox-group v-show="true" v-model="state.rightChecked" :validate-event="false" class="transfer-panel__list">
<el-checkbox
v-for="(i, k) in rightFilterData"
:key="k"
:value="i[props.options.value]"
:label="i[props.options.label]"
:disabled="i[props.options.disabled]"
:validate-event="false"
class="transfer-panel__item"
>
</el-checkbox>
</el-checkbox-group>
</div>
</div>
</el-col>
</el-row>
</template>
<script lang="ts" setup name="transfer">
import { watch, reactive, computed } from 'vue';
const props = defineProps({
leftTitle: String,
rightTitle: String,
options: {
type: Object,
default: () => ({
value: 'id',
label: 'name',
disabled: 'disabled',
}),
},
leftData: { type: Array, default: () => [] }, //
rightData: { type: Array, default: () => [] }, //
});
const emits = defineEmits(['left', 'right', 'allLeft', 'allRight', 'update:leftData', 'update:rightData']);
const state = reactive({
leftAllChecked: false, //
leftKeyword: '', //
leftChecked: [], //
rightAllChecked: false, //
rightKeyword: '', //
rightChecked: [], //
});
//
const leftFilterData = computed(() => {
let result = props.leftData.filter((e) => e[props.options.label].toLowerCase().includes(state.leftKeyword.toLowerCase()));
if (state.leftChecked.length > 0) {
for (let i = state.leftChecked.length - 1; i >= 0; i--) {
const index = result.findIndex((e) => e[props.options.value] == state.leftChecked[i]);
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
if (index == -1) state.leftChecked.splice(i, 1);
}
}
return result;
});
//
const handleLeftAllChecked = (value: any) => {
state.leftChecked = value ? leftFilterData.value.filter((e) => e[props.options.disabled] == false).map((e) => e[props.options.value]) : [];
};
//
const leftIndeterminate = computed(() => {
const checkedLength = state.leftChecked.length;
const result = checkedLength > 0 && checkedLength < leftFilterData.value.filter((e) => e[props.options.disabled] == false).length;
return result;
});
watch(
() => state.leftChecked,
(val: any[]) => {
state.leftAllChecked = val.length > 0 && val.length == leftFilterData.value.filter((e) => e[props.options.disabled] == false).length;
}
);
//
const rightFilterData = computed(() => {
let result = props.rightData.filter((e) => e[props.options.label].toLowerCase().includes(state.rightKeyword.toLowerCase()));
if (state.rightChecked.length > 0) {
for (let i = state.rightChecked.length - 1; i >= 0; i--) {
const index = result.findIndex((e) => e[props.options.value] == state.rightChecked[i]);
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
if (index == -1) state.rightChecked.splice(i, 1);
}
}
return result;
});
//
const handleRightAllChecked = (value: any) => {
state.rightChecked = value ? rightFilterData.value.filter((e) => e[props.options.disabled] == false).map((e) => e[props.options.value]) : [];
};
//
const rightIndeterminate = computed(() => {
const checkedLength = state.rightChecked.length;
const result = checkedLength > 0 && checkedLength < rightFilterData.value.filter((e) => e[props.options.disabled] == false).length;
return result;
});
watch(
() => state.rightChecked,
(val: any[]) => {
state.rightAllChecked = val.length > 0 && val.length == rightFilterData.value.filter((e) => e[props.options.disabled] == false).length;
}
);
//
const toRight = () => {
if (state.leftChecked?.length > 0) {
//
let adds = props.leftData.filter((e) => state.leftChecked.some((x) => x == e[props.options.value]));
//
let cuts = props.leftData.filter((e) => state.leftChecked.every((x) => x != e[props.options.value]));
emits('update:leftData', cuts);
emits('update:rightData', props.rightData.concat(adds));
emits('right');
state.leftChecked = [];
}
};
//
const allToRight = () => {
if (leftFilterData.value?.length > 0) {
let temp = leftFilterData.value.filter((e) => e[props.options.disabled] == false);
//
let adds = props.leftData.filter((e) => temp.some((x) => x[props.options.value] == e[props.options.value]));
//
let cuts = props.leftData.filter((e) => temp.every((x) => x[props.options.value] != e[props.options.value]));
emits('update:leftData', cuts);
emits('update:rightData', props.rightData.concat(adds));
emits('allRight');
state.leftChecked = [];
}
};
//
const toLeft = () => {
if (state.rightChecked?.length > 0) {
//
let adds = props.rightData.filter((e) => state.rightChecked.some((x) => x == e[props.options.value]));
//
let cuts = props.rightData.filter((e) => state.rightChecked.every((x) => x != e[props.options.value]));
emits('update:leftData', props.leftData.concat(adds));
emits('update:rightData', cuts);
emits('left');
state.rightChecked = [];
}
};
//
const allToLeft = () => {
if (rightFilterData.value?.length > 0) {
let temp = rightFilterData.value.filter((e) => e[props.options.disabled] == false);
//
let adds = props.rightData.filter((e) => temp.some((x) => x[props.options.value] == e[props.options.value]));
//
let cuts = props.rightData.filter((e) => temp.every((x) => x[props.options.value] != e[props.options.value]));
emits('update:leftData', props.leftData.concat(adds));
emits('update:rightData', cuts);
emits('allLeft');
state.rightChecked = [];
}
};
</script>
<style lang="scss" scoped>
.transfer-panel {
overflow: hidden;
display: inline-block;
text-align: left;
vertical-align: middle;
width: 100%;
max-height: 100%;
box-sizing: border-box;
position: relative;
border: 1px solid #ebeef5;
&__header {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
background: #f5f7fa;
padding: 3px 6px;
border-bottom: 1px solid #ebeef5;
}
&__body {
height: 300px;
.transfer-panel__filter {
padding: 6px;
}
.transfer-panel__list {
overflow: auto;
height: calc(100% - 36px);
.transfer-panel__item {
display: block !important;
padding-left: 6px;
}
}
}
}
.transfer-buttons {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&__item {
padding-top: 10px;
width: 100%;
}
}
</style>

View File

@ -84,6 +84,7 @@ import { Local, Session } from '/@/utils/storage';
import { formatAxis } from '/@/utils/formatTime';
import { NextLoading } from '/@/utils/loading';
import { sm2 } from 'sm-crypto-v2';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import { storeToRefs } from 'pinia';
@ -246,10 +247,26 @@ const signInSuccess = (isNoPower: boolean | undefined) => {
let currentTimeInfo = currentTime.value;
// /
if (route.query?.redirect) {
router.push({
path: <string>route.query?.redirect,
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
});
const stores = useRoutesList();
const { routesList } = storeToRefs(stores);
const recursion = (routeList: any[], url: string): boolean | undefined => {
if (routeList && routeList.length > 0) {
for (let i = 0; i < routeList.length; i++) {
if (routeList[i].path === url) return true;
if (routeList[i]?.children.length > 0) {
let result = recursion(routeList[i]?.children, url);
if (result) return true;
}
}
}
};
let exist = recursion(routesList.value, route.query?.redirect as string);
if (exist) {
router.push({
path: <string>route.query?.redirect,
query: Object.keys(<string>route.query?.params).length > 0 ? JSON.parse(<string>route.query?.params) : '',
});
} else router.push('/');
} else {
router.push('/');
}

View File

@ -31,13 +31,6 @@
<el-input v-model="state.ruleForm.realName" placeholder="真实姓名" clearable />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="角色集合" prop="roleIdList" :rules="[{ required: true, message: '角色集合不能为空', trigger: 'blur' }]">
<el-select v-model="state.ruleForm.roleIdList" multiple value-key="id" clearable placeholder="角色集合" collapse-tags collapse-tags-tooltip class="w100" filterable>
<el-option v-for="item in state.roleData" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="账号类型" prop="accountType" :rules="[{ required: true, message: '账号类型不能为空', trigger: 'blur' }]">
<el-select v-model="state.ruleForm.accountType" placeholder="账号类型" collapse-tags collapse-tags-tooltip class="w100">
@ -130,6 +123,9 @@
</el-row>
</el-form>
</el-tab-pane>
<el-tab-pane label="角色授权" style="height: 550px; overflow-y: auto; overflow-x: hidden">
<Transfer left-title="未授权" right-title="已授权" v-model:leftData="state.available" v-model:rightData="state.granted" />
</el-tab-pane>
<el-tab-pane label="档案信息" style="height: 550px; overflow-y: auto; overflow-x: hidden">
<el-form :model="state.ruleForm" label-width="auto">
<el-row :gutter="10">
@ -253,7 +249,9 @@ import { useUserInfo } from '/@/stores/userInfo';
import { getAPI } from '/@/utils/axios-utils';
import { SysPosApi, SysRoleApi, SysUserApi } from '/@/api-services/api';
import { RoleOutput, SysOrg, PagePosOutput, UpdateUserInput } from '/@/api-services/models';
import { SysOrg, PagePosOutput, UpdateUserInput } from '/@/api-services/models';
import Transfer from '/@/components/transfer/index.vue';
import { ElMessage } from 'element-plus';
const props = defineProps({
title: String,
@ -269,7 +267,8 @@ const state = reactive({
selectedTabName: '0', // tab
ruleForm: {} as UpdateUserInput,
posData: [] as Array<PagePosOutput>, //
roleData: [] as Array<RoleOutput>, //
available: [], //
granted: [], //
});
//
const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'name', expandTrigger: 'hover' };
@ -279,8 +278,6 @@ onMounted(async () => {
state.loading = true;
const { data } = await getAPI(SysPosApi).apiSysPosListGet();
state.posData = data.result ?? [];
const { data: res } = await getAPI(SysRoleApi).apiSysRoleListGet();
state.roleData = res.result ?? [];
state.loading = false;
});
@ -291,11 +288,17 @@ const openDialog = async (row: any) => {
state.selectedTabName = '0'; // tab
state.ruleForm = JSON.parse(JSON.stringify(row));
if (row.id != undefined) {
var resRole = await getAPI(SysUserApi).apiSysUserOwnRoleListUserIdGet(row.id);
state.ruleForm.roleIdList = resRole.data.result;
const { data } = await getAPI(SysUserApi).apiSysUserOwnRoleListUserIdGet(row.id);
state.available = data.result?.available;
state.granted = data.result?.granted;
var resExtOrg = await getAPI(SysUserApi).apiSysUserOwnExtOrgListUserIdGet(row.id);
state.ruleForm.extOrgIdList = resExtOrg.data.result;
} else state.ruleForm.accountType = 777; //
} else {
state.ruleForm.accountType = 777; //
const { data } = await getAPI(SysRoleApi).apiSysRoleListGet();
state.available = data.result ?? [];
state.granted = [];
}
state.isShowDialog = true;
};
@ -314,6 +317,11 @@ const cancel = () => {
const submit = () => {
ruleFormRef.value.validate(async (valid: boolean) => {
if (!valid) return;
if (state.granted?.length > 0) state.ruleForm.roleIdList = state.granted.map((e) => e.id);
else {
ElMessage.error(`角色尚未分配`);
return;
}
if (state.ruleForm.id != undefined && state.ruleForm.id > 0) {
await getAPI(SysUserApi).apiSysUserUpdatePost(state.ruleForm);
} else {