feat: role

This commit is contained in:
teukkk 2025-06-10 19:25:17 +08:00 committed by 刘瑞斌
parent cfafc3f0ba
commit e0dcc4b29d
19 changed files with 746 additions and 2 deletions

43
ui/src/api/type/role.ts Normal file
View File

@ -0,0 +1,43 @@
import { RoleTypeEnum } from '@/enums/system'
interface RoleItem {
id: string,
role_name: string,
type: RoleTypeEnum,
create_user: string,
internal: boolean,
}
interface ChildrenPermissionItem {
id: string
name: string
enable: boolean
}
interface RolePermissionItem {
id: string,
name: string,
children: {
id: string,
name: string,
permission: ChildrenPermissionItem[],
enable: boolean,
}[]
}
interface RoleTableDataItem {
module: string
name: string
permission: ChildrenPermissionItem[]
enable: boolean
perChecked: string[]
indeterminate: boolean
}
interface CreateOrUpdateParams {
role_id?: string,
role_name: string,
role_type?: RoleTypeEnum,
}
export type { RoleItem, RolePermissionItem, RoleTableDataItem, CreateOrUpdateParams, ChildrenPermissionItem }

67
ui/src/api/user/role.ts Normal file
View File

@ -0,0 +1,67 @@
import { get, post, del } from '@/request/index'
import type { Ref } from 'vue'
import { Result } from '@/request/Result'
import type { RoleItem, RolePermissionItem, CreateOrUpdateParams } from '@/api/type/role'
import { RoleTypeEnum } from '@/enums/system'
const prefix = '/system/role'
/**
*
*/
const getRoleList: (loading?: Ref<boolean>) => Promise<Result<{ internal_role: RoleItem[], custom_role: RoleItem[] }>> = (loading) => {
return get(`${prefix}`, undefined, loading)
}
/**
*
*/
const getRoleTemplate: (role_type: RoleTypeEnum, loading?: Ref<boolean>) => Promise<Result<RolePermissionItem[]>> = (role_type, loading) => {
return get(`${prefix}/template/${role_type}`, undefined, loading)
}
/**
*
*/
const getRolePermissionList: (role_id: string, loading?: Ref<boolean>) => Promise<Result<RolePermissionItem[]>> = (role_id, loading) => {
return get(`${prefix}/${role_id}/permission`, undefined, loading)
}
/**
*
*/
const CreateOrUpdateRole: (
data: CreateOrUpdateParams,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (data, loading) => {
return post(`${prefix}`, data, undefined, loading)
}
/**
*
*/
const deleteRole: (role_id: string, loading?: Ref<boolean>) => Promise<Result<boolean>> = (
role_id,
loading,
) => {
return del(`${prefix}/${role_id}`, undefined, {}, loading)
}
/**
*
*/
const saveRolePermission: (
role_id: string,
data: { id: string, enable: boolean }[],
loading?: Ref<boolean>,
) => Promise<Result<any>> = (role_id, data, loading) => {
return post(`${prefix}/${role_id}/permission`, data, undefined, loading)
}
export default {
getRoleList,
getRolePermissionList,
getRoleTemplate,
CreateOrUpdateRole,
deleteRole,
saveRolePermission
}

View File

@ -5,3 +5,9 @@ export enum AuthorizationEnum {
KNOWLEDGE = 'KNOWLEDGE',
APPLICATION = 'APPLICATION',
}
export enum RoleTypeEnum {
ADMIN = 'ADMIN',
USER = 'USER',
WORKSPACE_MANAGE = 'WORKSPACE_MANAGE',
}

View File

@ -66,9 +66,11 @@ export default {
addParam: 'Add Parameter',
},
inputPlaceholder: 'Please input',
selectPlaceholder: 'Please select',
title: 'Title',
content: 'Content',
rename: 'Rename',
renameSuccess: 'Successful',
EditAvatarDialog: {
title: 'App Logo',
customizeUpload: 'Custom Upload',

View File

@ -1,5 +1,6 @@
import notFound from './404'
import application from './application'
import role from './role'
import applicationOverview from './application-overview'
import knowledge from './knowledge'
import system from './system'
@ -32,5 +33,6 @@ export default {
problem,
log,
login,
operateLog
operateLog,
role
}

View File

@ -0,0 +1,22 @@
export default {
title: 'Role management',
internalRole: 'System built-in roles',
customRole: 'Custom roles',
systemAdmin: 'System admin',
workspaceAdmin: 'Workspace admin',
user: 'Regular user',
roleName: 'Role name',
inheritingRole: 'Inherited role',
delete: {
confirmTitle: 'Confirm to delete role:',
confirmMessage: 'After deletion, all members under this role will be removed. Please proceed with caution.',
},
permission: {
title: 'Permission configuration',
operationTarget: 'Operation target',
moduleName: 'Module name'
},
member: {
title: 'Members'
}
};

View File

@ -70,9 +70,11 @@ export default {
addParam: '添加参数',
},
inputPlaceholder: '请输入',
selectPlaceholder: '请选择',
title: '标题',
content: '内容',
rename: '重命名',
renameSuccess: '重命名成功',
EditAvatarDialog: {
title: '应用头像',
customizeUpload: '自定义上传',

View File

@ -6,6 +6,7 @@ import document from './document'
import system from './system'
import userManage from './user-manage'
import resourceAuthorization from './resource-authorization'
import role from './role'
import application from './application'
import problem from './problem'
import applicationOverview from './application-overview'
@ -24,6 +25,7 @@ export default {
system,
userManage,
resourceAuthorization,
role,
application,
problem,
applicationOverview,

View File

@ -0,0 +1,22 @@
export default {
title: '角色管理',
internalRole: '系统内置角色',
customRole: '自定义角色',
systemAdmin: '系统管理员',
workspaceAdmin: '工作空间管理员',
user: '普通用户',
roleName: '角色名称',
inheritingRole: '继承角色',
delete: {
confirmTitle: '是否删除角色:',
confirmMessage: '删除后,该角色下的成员都会被移除,请谨慎操作。',
},
permission: {
title: '权限配置',
operationTarget: '操作对象',
moduleName: '模块名称'
},
member: {
title: '成员'
}
}

View File

@ -66,9 +66,11 @@ export default {
addParam: '新增參數',
},
inputPlaceholder: '請輸入',
selectPlaceholder: '請選擇',
title: '標題',
content: '内容',
rename: '重命名',
renameSuccess: '重命名成功',
EditAvatarDialog: {
title: '應用頭像',
customizeUpload: '自訂上傳',

View File

@ -1,5 +1,6 @@
import notFound from './404'
import application from './application'
import role from './role'
import applicationOverview from './application-overview'
import knowledge from './knowledge'
import system from './system'
@ -32,5 +33,6 @@ export default {
problem,
log,
login,
operateLog
operateLog,
role
}

View File

@ -0,0 +1,22 @@
export default {
title: '角色管理',
internalRole: '系統內置角色',
customRole: '自定義角色',
systemAdmin: '系統管理員',
workspaceAdmin: '工作空間管理員',
user: '普通用戶',
roleName: '角色名稱',
inheritingRole: '繼承角色',
delete: {
confirmTitle: '是否刪除角色:',
confirmMessage: '刪除後,該角色下的成員都會被移除,請謹慎操作。',
},
permission: {
title: '權限配置',
operationTarget: '操作對象',
moduleName: '模塊名稱'
},
member: {
title: '成員'
}
};

View File

@ -32,6 +32,19 @@ const systemRouter = {
},
component: () => import('@/views/resource-authorization/index.vue'),
},
{
path: '/system/role',
name: 'role',
meta: {
icon: 'app-resource-authorization', // TODO
iconActive: 'app-resource-authorization-active', // TODO
title: 'views.role.title',
activeMenu: '/system',
parentPath: '/system',
parentName: 'system',
},
component: () => import('@/views/role/index.vue'),
},
{
path:'/system/setting',
name: 'setting',

View File

@ -385,6 +385,9 @@ h5 {
.color-success {
color: var(--el-color-success);
}
.color-input-placeholder {
color: var(--app-input-color-placeholder);
}
.avatar-purple {
background: #7f3bf5;
}

View File

@ -0,0 +1,82 @@
<template>
<el-dialog :title="`${!form.role_id ? $t('common.create') : $t('common.rename')}${$t('views.role.customRole')}`"
v-model="dialogVisible" :close-on-click-modal="false" :close-on-press-escape="false" :destroy-on-close="true">
<el-form label-position="top" ref="formRef" :rules="rules" :model="form" require-asterisk-position="right">
<el-form-item :label="$t('views.role.roleName')" prop="role_name">
<el-input v-model="form.role_name" maxlength="64"
:placeholder="`${$t('common.inputPlaceholder')}${$t('views.role.roleName')}`" />
</el-form-item>
<el-form-item v-if="!form.role_id" :label="$t('views.role.inheritingRole')" prop="role_type">
<el-select v-model="form.role_type"
:placeholder="`${$t('common.selectPlaceholder')}${$t('views.role.inheritingRole')}`">
<el-option v-for="(label, value) in roleTypeMap" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(formRef)" :loading="loading">
{{ !form.role_id ? $t('common.create') : $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { FormInstance } from 'element-plus'
import { MsgSuccess } from '@/utils/message'
import { t } from '@/locales'
import type { RoleItem, CreateOrUpdateParams } from '@/api/type/role'
import RoleApi from '@/api/user/role'
import { roleTypeMap } from '../index'
const emit = defineEmits<{
(e: 'refresh', currentRole: RoleItem): void;
}>();
const dialogVisible = ref<boolean>(false)
const defaultForm = {
role_name: ''
}
const form = ref<CreateOrUpdateParams>({
...defaultForm,
})
function open(item?: RoleItem) {
if (item) {
form.value = {
role_name: item.role_name,
role_type: item.type,
role_id: item.id,
}
} else {
form.value = { ...defaultForm }
}
dialogVisible.value = true
}
const formRef = ref<FormInstance>();
const rules = reactive({
role_name: [{ required: true, message: `${t('common.inputPlaceholder')}${t('views.role.roleName')}`, trigger: 'blur' }],
role_type: [{ required: true, message: `${t('common.selectPlaceholder')}${t('views.role.inheritingRole')}`, trigger: 'blur' }]
})
const loading = ref<boolean>(false)
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
RoleApi.CreateOrUpdateRole(form.value, loading).then((res: any) => {
MsgSuccess(!form.value.role_id ? t('common.createSuccess') : t('common.renameSuccess'))
emit('refresh', res.data)
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,5 @@
<template>
<div></div>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,173 @@
<template>
<el-scrollbar v-loading="loading">
<div class="p-24 pt-0">
<el-table :data="tableData" border :span-method="objectSpanMethod">
<el-table-column prop="module" :width="130" :label="$t('views.role.permission.moduleName')" />
<el-table-column prop="name" :width="150" :label="$t('views.role.permission.operationTarget')" />
<el-table-column prop="permission" :label="$t('views.model.modelForm.permissionType.label')">
<template #default="{ row }">
<el-checkbox-group v-model="row.perChecked" @change="handleCellChange($event, row)">
<el-checkbox v-for="item in row.permission" :key="item.id" :value="item.id" :disabled="disabled">
<div class="ellipsis" style="width: 96px">{{ item.name }}</div>
</el-checkbox>
</el-checkbox-group>
</template>
</el-table-column>
<el-table-column :width="40">
<template #header>
<el-checkbox :model-value="allChecked" :indeterminate="allIndeterminate" :disabled="disabled"
@change="handleCheckAll" />
</template>
<template #default="{ row }">
<el-checkbox v-model="row.enable" :indeterminate="row.indeterminate" :disabled="disabled"
@change="(value: boolean) => handleRowChange(value, row)" />
</template>
</el-table-column>
</el-table>
</div>
</el-scrollbar>
<div v-if="!disabled" class="footer border-t">
<el-button type="primary" style="width: 80px;" :loading="loading" @click="handleSave">
{{ $t('common.save') }}
</el-button>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { RoleItem, RolePermissionItem, RoleTableDataItem, ChildrenPermissionItem } from '@/api/type/role'
import RoleApi from '@/api/user/role'
import { MsgSuccess } from '@/utils/message'
import { t } from '@/locales'
const props = defineProps<{
currentRole?: RoleItem
}>()
const loading = ref(false)
const tableData = ref<RoleTableDataItem[]>([])
const disabled = computed(() => props.currentRole?.internal) // TODO
function transformData(data: RolePermissionItem[]) {
const transformedData: RoleTableDataItem[] = []
data.forEach(module => {
module.children.forEach(feature => {
const perChecked = feature.permission
.filter(p => p.enable)
.map(p => p.id)
transformedData.push({
module: module.name,
name: feature.name,
permission: feature.permission,
enable: feature.enable,
perChecked,
indeterminate: perChecked.length > 0 && perChecked.length < feature.permission.length
})
})
})
return transformedData;
};
async function getRolePermission() {
if (!props.currentRole?.id) return
try {
tableData.value = [];
const res = await RoleApi.getRolePermissionList(props.currentRole.id, loading)
tableData.value = transformData(res.data);
} catch (error) {
console.error(error)
}
}
function handleCellChange(checkedValues: string[], row: RoleTableDataItem) {
row.enable = checkedValues.length === row.permission.length
row.indeterminate = checkedValues.length > 0 && checkedValues.length < row.permission.length
row.permission.forEach(p => {
p.enable = checkedValues.includes(p.id)
})
}
function handleRowChange(checked: boolean, row: RoleTableDataItem) {
if (checked) {
row.perChecked = row.permission.map(p => p.id)
row.permission.forEach(p => p.enable = true)
} else {
row.perChecked = []
row.permission.forEach(p => p.enable = false)
}
row.indeterminate = false
}
const allChecked = computed(() => {
return tableData.value.length > 0 && tableData.value.every(item => item.enable)
})
const allIndeterminate = computed(() => {
return !allChecked.value && tableData.value.some(item => item.enable)
})
function handleCheckAll(checked: boolean) {
tableData.value.forEach(item => {
item.enable = checked
item.perChecked = checked ? item.permission.map(p => p.id) : []
item.indeterminate = false
item.permission.forEach(p => p.enable = checked)
})
}
const objectSpanMethod = ({ row, column, rowIndex, columnIndex }: any) => {
if (columnIndex === 0) {
const sameModuleRows = tableData.value.filter(item => item.module === row.module)
const firstRowIndex = tableData.value.findIndex(item => item.module === row.module)
if (rowIndex === firstRowIndex) {
return {
rowspan: sameModuleRows.length,
colspan: 1
}
} else {
return {
rowspan: 0,
colspan: 0
}
}
}
}
watch(() => props.currentRole?.id, getRolePermission, { immediate: true })
async function handleSave() {
try {
const permissions: { id: string, enable: boolean }[] = [];
tableData.value.forEach((e) => {
e.permission?.forEach((ele: ChildrenPermissionItem) => {
permissions.push({
id: ele.id,
enable: ele.enable,
});
});
});
await RoleApi.saveRolePermission(props.currentRole?.id as string, permissions, loading);
MsgSuccess(t('common.saveSuccess'))
} catch (error) {
console.log(error);
}
}
</script>
<style lang="scss" scoped>
:deep(.el-checkbox-group) {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.footer {
width: 100%;
display: flex;
justify-content: flex-end;
padding: 16px 24px;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,8 @@
import { RoleTypeEnum } from '@/enums/system'
import { t } from '@/locales'
export const roleTypeMap: Record<RoleTypeEnum, string> = {
[RoleTypeEnum.ADMIN]: t('views.role.systemAdmin'),
[RoleTypeEnum.USER]: t('views.role.user'),
[RoleTypeEnum.WORKSPACE_MANAGE]: t('views.role.workspaceAdmin')
}

266
ui/src/views/role/index.vue Normal file
View File

@ -0,0 +1,266 @@
<template>
<div class="role">
<h2 class="mb-16">{{ $t('views.role.title') }}</h2>
<el-card style="--el-card-padding: 0" body-class="role-card">
<div class="flex h-full">
<div class="role-left border-r p-16">
<div class="p-8 pb-0">
<el-input v-model="filterText" :placeholder="$t('common.search')" prefix-icon="Search" clearable />
</div>
<div class="list-height-left mt-8">
<el-scrollbar v-loading="loading">
<div class="role-left_title color-secondary lighter">
<span>{{ $t('views.role.internalRole') }}</span>
</div>
<common-list :data="filterInternalRole" @click="clickRole" :default-active="currentRole?.id">
<template #default="{ row }">
<div class="flex-between">
<span class="mr-8">{{ row.role_name }}</span>
<el-dropdown :teleported="false">
<el-button text>
<el-icon class="color-secondary">
<MoreFilled />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu style="min-width: 80px">
<el-dropdown-item @click.stop="createOrUpdateRole(row)" class="p-8">
{{ $t('common.rename') }}
</el-dropdown-item>
<el-dropdown-item @click.stop="deleteRole(row)" class="border-t p-8">
{{ $t('common.delete') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #empty>
<span></span>
</template>
</common-list>
<div class="role-left_divider">
<el-divider />
</div>
<div class="role-left_title">
<span class="color-secondary lighter">{{ $t('views.role.customRole') }}</span>
<AppIcon iconName="app-wordspace" style="font-size: 16px" class="cursor color-primary"
@click="createOrUpdateRole()">
</AppIcon>
</div>
<common-list :data="filterCustomRole" @click="clickRole" :default-active="currentRole?.id">
<template #default="{ row }">
<div class="flex-between">
<span>
{{ row.role_name }}
<span class="color-input-placeholder ml-4">({{ roleTypeMap[row.type as RoleTypeEnum] }})</span>
</span>
<el-dropdown :teleported="false">
<el-button text>
<el-icon class="color-secondary">
<MoreFilled />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu style="min-width: 80px">
<el-dropdown-item @click.stop="createOrUpdateRole(row)" class="p-8"> {{ $t('common.rename') }}
</el-dropdown-item>
<el-dropdown-item @click.stop="deleteRole(row)" class="border-t p-8"> {{ $t('common.delete')
}}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #empty>
<span></span>
</template>
</common-list>
</el-scrollbar>
</div>
</div>
<!-- 右边 -->
<div class="role-right">
<div class="flex-between mb-16 p-24 pb-0">
<div class="flex align-center">
<span>
{{ currentRole?.role_name }}
<span v-if="currentRole?.type" class="color-input-placeholder ml-4">({{ roleTypeMap[currentRole?.type as
RoleTypeEnum] }})
</span>
</span>
<el-divider direction="vertical" class="mr-8 ml-8" />
<AppIcon iconName="app-wordspace" style="font-size: 16px" class="color-input-placeholder"></AppIcon>
<span class="color-input-placeholder ml-4">
数字
</span>
</div>
<el-radio-group v-model="currentTab">
<el-radio-button v-for="item in tabList" :key="item.value" :label="item.label" :value="item.value" />
</el-radio-group>
</div>
<PermissionConfiguration v-if="currentTab === 'permission'" :currentRole="currentRole" />
<Member v-else :currentRole="currentRole" />
</div>
</div>
</el-card>
<CreateOrUpdateRoleDialog ref="createOrUpdateRoleDialogRef" @refresh="refresh" />
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue'
import RoleApi from '@/api/user/role'
import { t } from '@/locales'
import PermissionConfiguration from './component/PermissionConfiguration.vue'
import Member from './component/Member.vue'
import CreateOrUpdateRoleDialog from './component/CreateOrUpdateRoleDialog.vue'
import type { RoleItem } from '@/api/type/role'
import { RoleTypeEnum } from '@/enums/system'
import { roleTypeMap } from './index'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
const filterText = ref('')
const loading = ref(false)
const internalRoleList = ref<RoleItem[]>([])
const filterInternalRole = ref<RoleItem[]>([]) //
const customRoleList = ref<RoleItem[]>([])
const filterCustomRole = ref<RoleItem[]>([]) //
const currentRole = ref<RoleItem>()
async function getRole() {
try {
const res = await RoleApi.getRoleList(loading)
internalRoleList.value = res.data.internal_role
customRoleList.value = res.data.custom_role
filterInternalRole.value = filter(internalRoleList.value, filterText.value)
filterCustomRole.value = filter(customRoleList.value, filterText.value)
} catch (error) {
console.error(error)
}
}
onMounted(async () => {
await getRole()
currentRole.value = internalRoleList.value[0]
})
async function refresh(role?: RoleItem) {
await getRole();
//
currentRole.value = role ? role : currentRole.value
}
function filter(list: RoleItem[], filterText: string) {
if (!filterText.length) {
return list
}
return list.filter((v: RoleItem) =>
v.role_name.toLowerCase().includes(filterText.toLowerCase()),
)
}
watch(filterText, (val: string) => {
filterInternalRole.value = filter(internalRoleList.value, val)
filterCustomRole.value = filter(customRoleList.value, val)
})
function clickRole(item: RoleItem) {
currentRole.value = item
}
const createOrUpdateRoleDialogRef = ref<InstanceType<typeof CreateOrUpdateRoleDialog>>()
function createOrUpdateRole(item?: RoleItem) {
createOrUpdateRoleDialogRef.value?.open(item);
}
function deleteRole(item: RoleItem) {
MsgConfirm(
`${t('views.role.delete.confirmTitle')}${item.role_name} ?`,
t('views.role.delete.confirmMessage'),
{
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger',
},
)
.then(() => {
RoleApi.deleteRole(item.id, loading).then(async () => {
MsgSuccess(t('common.deleteSuccess'))
await getRole()
currentRole.value = item.id === currentRole.value?.id ? internalRoleList.value[0] : currentRole.value
})
})
.catch(() => { })
}
const currentTab = ref('permission')
const tabList = [
{
value: 'permission',
label: t('views.role.permission.title'),
},
{
value: 'member',
label: t('views.role.member.title'),
},
]
</script>
<style lang="scss" scoped>
.role {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
box-sizing: border-box;
padding: 16px 24px;
:deep(.role-card) {
height: 100%;
overflow: hidden;
}
.role-left {
box-sizing: border-box;
width: var(--setting-left-width);
min-width: var(--setting-left-width);
.role-left_title {
padding: 8px;
display: flex;
justify-content: space-between;
}
.list-height-left {
height: calc(100vh - 213px);
:deep(.common-list li) {
padding-right: 4px;
padding-left: 8px;
}
}
.role-left_divider {
padding: 0 8px;
:deep(.el-divider) {
margin: 4px 0;
}
}
}
.role-right {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
</style>