feat: add batch user role setting functionality and update related UI components
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
Typos Check / Spell Check with Typos (push) Has been cancelled

This commit is contained in:
wxg0103 2025-12-26 18:27:33 +08:00
parent 132d6d78e8
commit 3600985fe6
11 changed files with 362 additions and 54 deletions

View File

@ -8894,5 +8894,11 @@ msgstr ""
msgid "Resources mapping"
msgstr ""
msgid "Batch set user roles"
msgstr ""
msgid "Role Setting cannot be empty"
msgstr ""
msgid "View related resources"
msgstr ""

View File

@ -8361,7 +8361,7 @@ msgstr "更新平台状态"
#: apps/xpack/views/resource_chat_user.py:28
#: apps/xpack/views/resource_chat_user.py:29
msgid "Get Resource chat user List"
msgstr "获取资源聊天用户列表"
msgstr "获取资源对话用户列表"
#: apps/xpack/views/resource_chat_user.py:32
#: apps/xpack/views/resource_chat_user.py:54
@ -8370,19 +8370,19 @@ msgstr "获取资源聊天用户列表"
#: apps/xpack/views/system_chat_user_group.py:45
#: apps/xpack/views/system_chat_user_group.py:67
msgid "Chat user"
msgstr "聊天用户"
msgstr "对话用户"
#: apps/xpack/views/resource_chat_user.py:48
#: apps/xpack/views/resource_chat_user.py:49
#: apps/xpack/views/resource_chat_user.py:50
msgid "Edit Resource chat user List"
msgstr "编辑资源聊天用户列表"
msgstr "编辑资源对话用户列表"
#: apps/xpack/views/resource_chat_user.py:72
#: apps/xpack/views/resource_chat_user.py:73
#: apps/xpack/views/resource_chat_user.py:74
msgid "Get Resource chat user page List"
msgstr "获取资源聊天用户分页列表"
msgstr "获取资源对话用户分页列表"
#: apps/xpack/views/system_api_key.py:19 apps/xpack/views/system_api_key.py:20
#: apps/xpack/views/system_api_key.py:21
@ -8446,22 +8446,22 @@ msgstr "系统/用户组"
#: apps/xpack/views/system_chat_user_group.py:20
#: apps/xpack/views/system_chat_user_group.py:21
msgid "Get Resource chat user group List"
msgstr "获取资源聊天用户组列表"
msgstr "获取资源对话用户组列表"
#: apps/xpack/views/system_chat_user_group.py:39
#: apps/xpack/views/system_chat_user_group.py:40
#: apps/xpack/views/system_chat_user_group.py:41
msgid "Edit Resource chat user group List"
msgstr "编辑资源聊天用户组列表"
msgstr "编辑资源对话用户组列表"
#: apps/xpack/views/system_chat_user_group.py:62
#: apps/xpack/views/system_chat_user_group.py:64
msgid "Get Resource chat user group page List"
msgstr "获取资源聊天用户组分页列表"
msgstr "获取资源对话用户组分页列表"
#: apps/xpack/views/system_chat_user_group.py:63
msgid "Get Resource chat user page group List"
msgstr "获取资源聊天用户分页组列表"
msgstr "获取资源对话用户分页组列表"
#: apps/xpack/views/system_params.py:22 apps/xpack/views/system_params.py:23
#: apps/xpack/views/system_params.py:24
@ -8546,7 +8546,7 @@ msgid "Paragraph"
msgstr "段落"
msgid "User group"
msgstr "用组"
msgstr "用组"
msgid "Remove member from user group"
msgstr "从用户组中移除成员"
@ -9020,5 +9020,11 @@ msgstr "获取资源的关系分页列表"
msgid "Resources mapping"
msgstr "资源映射"
msgid "Batch set user roles"
msgstr "批量设置用户角色"
msgid "Role Setting cannot be empty"
msgstr "角色设置不能为空"
msgid "View related resources"
msgstr "查看关联资源"

View File

@ -9020,5 +9020,11 @@ msgstr "獲取資源的關係分頁清單"
msgid "Resources mapping"
msgstr "資源映射"
msgid "Batch set user roles"
msgstr "批量設置用戶角色"
msgid "Role Setting cannot be empty"
msgstr "角色設置不能為空"
msgid "View related resources"
msgstr "查看關聯資源"

View File

@ -42,6 +42,7 @@ def get_user_operation_object(user_id):
return {}
def get_re_password_details(request):
path = request.path
body = request.data
@ -245,7 +246,7 @@ class UserManage(APIView):
responses=DefaultModelResponse.get_response())
@has_permissions(PermissionConstants.USER_DELETE, RoleConstants.ADMIN)
@log(menu='User management', operate='Batch delete user',
get_operation_object=lambda r, k: get_user_operation_object(k.get('user_id')))
get_operation_object=lambda r, k: get_user_operation_object(r.data.get('ids', [])))
def post(self, request: Request):
return result.success(UserManageSerializer.BatchDelete({'ids': request.data}).batch_delete(with_valid=True))

View File

@ -96,6 +96,19 @@ const batchDelete: (
return post(`/user_manage/batch_delete`, ids, {}, loading)
}
const batchSetRolePE: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
data,
loading,
) => {
return post(`/user_manage/batch/add_role`, data, undefined, loading)
}
const batchSetRoleEE: (data: any, loading?: Ref<boolean>) => Promise<Result<any>> = (
data,
loading,
) => {
return post(`/user_manage/batch/add_role_ee`, data, undefined, loading)
}
export default {
getUserManage,
putUserManage,
@ -104,5 +117,7 @@ export default {
putUserManagePassword,
getSystemDefaultPassword,
getValid,
batchDelete
batchDelete,
batchSetRolePE,
batchSetRoleEE
}

View File

@ -42,4 +42,5 @@ export default {
lark: 'Lark',
dingtalk: 'DingTalk',
},
settingRole: 'Set Role',
}

View File

@ -40,4 +40,5 @@ export default {
lark: '飞书',
dingtalk: '钉钉',
},
settingRole: '设置角色',
}

View File

@ -41,4 +41,5 @@ export default {
lark: '飛書',
dingtalk: '釘釘',
},
settingRole: '設定角色',
}

View File

@ -57,7 +57,7 @@
</div>
</el-scrollbar>
<!-- 添加按钮 -->
<el-button type="primary" text class="mt-2" @click="handleAdd">
<el-button type="primary" text class="mt-2" @click="handleAdd" v-if="needAddButton">
<AppIcon iconName="app-add-outlined" class="mr-4"></AppIcon>
{{ props.addText ?? $t('views.role.member.add') }}
</el-button>
@ -65,15 +65,19 @@
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
import type { FormItemModel } from '@/api/type/role'
import {ref, watch, computed} from 'vue'
import type {FormItemModel} from '@/api/type/role'
const props = defineProps<{
const props = withDefaults(defineProps<{
models: FormItemModel[]
addText?: string
keepOneLine?: boolean //
keepOneLine?: boolean
deleteButtonDisabled?: (model: any) => boolean
}>()
needAddButton?: boolean
}>(), {
needAddButton: true
})
const formRef = ref()
const formItem: Record<string, any> = {}
@ -86,7 +90,7 @@ const selectedRoles = computed(() => {
})
function handleAdd() {
form.value.push({ ...formItem })
form.value.push({...formItem})
}
watch(
@ -96,7 +100,7 @@ watch(
formItem[e.path] = []
})
},
{ immediate: true },
{immediate: true},
)
function handleDelete(index: number) {
@ -115,5 +119,5 @@ const resetValidation = () => {
formRef.value.clearValidate()
}
}
defineExpose({ validate, resetValidation })
defineExpose({validate, resetValidation})
</script>

View File

@ -0,0 +1,215 @@
<template>
<el-dialog width="600" :title="$t('views.userManage.settingRole')" 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.chatUser.settingMethod')">
<el-radio-group v-model="form.is_append">
<el-radio :value="true">{{ $t('views.chatUser.append') }}</el-radio>
<el-radio :value="false">{{
$t('views.applicationOverview.SettingDisplayDialog.replace')
}}
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<MemberFormContent
ref="memberFormContentRef"
:models="formItemModel"
v-model:form="list"
v-loading="memberFormContentLoading"
keepOneLine
:need-add-button="!user.isPE()"
:addText="$t('views.userManage.addRole')"
v-if="user.isEE() || user.isPE()"
/>
</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">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import {ref, reactive, onBeforeMount} from 'vue'
import type {FormInstance} from 'element-plus'
import {t} from '@/locales'
const memberFormContentLoading = ref(false)
import userManageApi from '@/api/system/user-manage'
import MemberFormContent from "@/views/system/role/component/MemberFormContent.vue";
import type {FormItemModel} from "@/api/type/role.ts";
import WorkspaceApi from "@/api/workspace/workspace.ts";
import useStore from "@/stores";
import {RoleTypeEnum} from "@/enums/system.ts";
import {loadPermissionApi} from "@/utils/dynamics-api/permission-api.ts";
import {MsgSuccess} from "@/utils/message.ts";
const list = ref<any[]>([])
const formItemModel = ref<FormItemModel[]>([])
const {user, common} = useStore()
const workspaceFormItem = ref<FormItemModel[]>([])
const roleFormItem = ref<FormItemModel[]>([])
const emit = defineEmits<{
(e: 'refresh'): void;
}>();
const dialogVisible = ref<boolean>(false)
const defaultForm = {
role_ids: [],
is_append: true,
ids: []
}
const form = ref<{
ids: string[], role_ids: string[], is_append: boolean
}>({
...defaultForm,
})
function open(ids: string[]) {
form.value = {...defaultForm, ids}
list.value = [{role_id: '', workspace_ids: []}]
dialogVisible.value = true
}
const formRef = ref<FormInstance>();
const adminRoleList = ref<any[]>([])
const rules = reactive({
is_append: [{required: true, message: t('common.selectPlaceholder'), trigger: 'blur'}],
})
const loading = ref<boolean>(false)
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (user.isPE()) {
const data = {
is_append: form.value.is_append,
ids: form.value.ids,
role_ids: list.value[0].role_id,
}
userManageApi.batchSetRolePE(data, loading).then(() => {
MsgSuccess(t('common.settingSuccess'))
emit('refresh')
dialogVisible.value = false
})
}
if (user.isEE()) {
list.value = list.value.map((item) => {
const isAdminRole = adminRoleList.value.find((item1) => item1.id === item.role_id)
// ['None']
if (isAdminRole) {
return {...item, workspace_ids: ['None']}
}
return item
})
const data = {
is_append: form.value.is_append,
ids: form.value.ids,
role_setting: list.value,
}
userManageApi.batchSetRoleEE(data, loading).then(() => {
MsgSuccess(t('common.settingSuccess'))
emit('refresh')
dialogVisible.value = false
})
}
}
})
}
async function getRoleFormItem() {
try {
const res = await WorkspaceApi.getWorkspaceRoleList(memberFormContentLoading)
roleFormItem.value = [
{
path: 'role_id',
label: t('views.role.member.role'),
rules: [
{
required: true,
message: `${t('common.selectPlaceholder')}${t('views.role.member.role')}`,
},
],
selectProps: {
options:
res.data?.map((item) => ({
label: item.name,
value: item.id,
})) || [],
placeholder: `${t('common.selectPlaceholder')}${t('views.role.member.role')}`,
multiple: !!user.isPE(),
},
},
]
adminRoleList.value = res.data.filter((item) => item.type === RoleTypeEnum.ADMIN)
} catch (e) {
console.error(e)
}
}
async function getWorkspaceFormItem() {
try {
const res = await WorkspaceApi.getWorkspaceList(memberFormContentLoading)
workspaceFormItem.value = [
{
path: 'workspace_ids',
label: t('views.role.member.workspace'),
hidden: (e) => adminRoleList.value.find((item) => item.id === e.role_id),
rules: [
{
validator: (rule, value, callback) => {
const match = rule.field?.match(/\[(\d+)\]/)
const isAdmin = adminRoleList.value.some(
(role) => role.id === list.value[parseInt(match?.[1] ?? '', 10)].role_id,
)
if (!isAdmin && (!value || value.length === 0)) {
callback(
new Error(`${t('common.selectPlaceholder')}${t('views.role.member.workspace')}`),
)
} else {
callback()
}
},
trigger: 'blur',
},
],
selectProps: {
options:
res.data?.map((item) => ({
label: item.name,
value: item.id,
})) || [],
placeholder: `${t('common.selectPlaceholder')}${t('views.role.member.workspace')}`,
},
},
]
} catch (e) {
console.error(e)
}
}
onBeforeMount(async () => {
if (user.isEE() || user.isPE()) {
await getRoleFormItem()
if (user.isEE()) {
await getWorkspaceFormItem()
}
formItemModel.value = [...roleFormItem.value, ...workspaceFormItem.value]
}
list.value = [{role_id: '', workspace_ids: []}]
})
defineExpose({open})
</script>

View File

@ -3,12 +3,29 @@
<h2 class="mb-16">{{ $t('views.userManage.title') }}</h2>
<el-card class="main-calc-height">
<div class="flex-between mb-16">
<el-button
type="primary"
@click="createUser"
v-hasPermission="[RoleConst.ADMIN, PermissionConst.USER_CREATE]"
<div>
<el-button
type="primary"
@click="createUser"
v-hasPermission="[RoleConst.ADMIN, PermissionConst.USER_CREATE]"
>{{ $t('views.userManage.createUser') }}
</el-button>
</el-button>
<el-button
v-if="user.isPE() || user.isEE()"
:disabled="multipleSelection.length === 0"
@click="setUserRoles"
v-hasPermission="[RoleConst.ADMIN, PermissionConst.USER_EDIT]"
>
{{ $t('views.userManage.settingRole') }}
</el-button>
<el-button
:disabled="multipleSelection.length === 0"
@click="handleBatchDelete"
v-hasPermission="[RoleConst.ADMIN, PermissionConst.USER_DELETE]"
>
{{ $t('common.delete') }}
</el-button>
</div>
<div class="flex-between complex-search">
<el-select
class="complex-search__left"
@ -16,10 +33,10 @@
style="width: 120px"
@change="search_type_change"
>
<el-option :label="$t('views.login.loginForm.username.label')" value="username" />
<el-option :label="$t('views.userManage.userForm.nick_name.label')" value="nick_name" />
<el-option :label="$t('views.login.loginForm.email.label')" value="email" />
<el-option :label="$t('common.status.label')" value="is_active" />
<el-option :label="$t('views.login.loginForm.username.label')" value="username"/>
<el-option :label="$t('views.userManage.userForm.nick_name.label')" value="nick_name"/>
<el-option :label="$t('views.login.loginForm.email.label')" value="email"/>
<el-option :label="$t('common.status.label')" value="is_active"/>
<el-option
v-if="user.isEE() || user.isPE()"
:label="$t('views.userManage.source.label')"
@ -57,8 +74,8 @@
clearable
style="width: 220px"
>
<el-option :label="$t('common.status.enabled')" :value="true" />
<el-option :label="$t('common.status.disabled')" :value="false" />
<el-option :label="$t('common.status.enabled')" :value="true"/>
<el-option :label="$t('common.status.disabled')" :value="false"/>
</el-select>
<el-select
v-else-if="search_type === 'source'"
@ -68,14 +85,14 @@
clearable
:placeholder="$t('common.inputPlaceholder')"
>
<el-option :label="$t('views.userManage.source.local')" value="LOCAL" />
<el-option label="CAS" value="CAS" />
<el-option label="LDAP" value="LDAP" />
<el-option label="OIDC" value="OIDC" />
<el-option label="OAuth2" value="OAuth2" />
<el-option :label="$t('views.userManage.source.wecom')" value="wecom" />
<el-option :label="$t('views.userManage.source.lark')" value="lark" />
<el-option :label="$t('views.userManage.source.dingtalk')" value="dingtalk" />
<el-option :label="$t('views.userManage.source.local')" value="LOCAL"/>
<el-option label="CAS" value="CAS"/>
<el-option label="LDAP" value="LDAP"/>
<el-option label="OIDC" value="OIDC"/>
<el-option label="OAuth2" value="OAuth2"/>
<el-option :label="$t('views.userManage.source.wecom')" value="wecom"/>
<el-option :label="$t('views.userManage.source.lark')" value="lark"/>
<el-option :label="$t('views.userManage.source.dingtalk')" value="dingtalk"/>
</el-select>
</div>
</div>
@ -86,8 +103,10 @@
@sizeChange="handleSizeChange"
@changePage="getList"
v-loading="loading"
@selection-change="handleSelectionChange"
:maxTableHeight="280"
>
<el-table-column type="selection" width="55"/>
<el-table-column
prop="nick_name"
:label="$t('views.userManage.userForm.nick_name.label')"
@ -104,7 +123,7 @@
<template #default="{ row }">
<div v-if="row.is_active" class="flex align-center">
<el-icon class="color-success mr-8" style="font-size: 16px">
<SuccessFilled />
<SuccessFilled/>
</el-icon>
<span class="color-text-primary">
{{ $t('common.status.enabled') }}
@ -200,7 +219,7 @@
v-if="hasPermission([RoleConst.ADMIN, PermissionConst.USER_EDIT], 'OR')"
/>
</span>
<el-divider direction="vertical" />
<el-divider direction="vertical"/>
<el-tooltip effect="dark" :content="$t('common.edit')" placement="top">
<span class="mr-8">
<el-button
@ -247,25 +266,29 @@
</el-table-column>
</app-table>
</el-card>
<UserDrawer :title="title" ref="UserDrawerRef" @refresh="refresh" />
<UserPwdDialog ref="UserPwdDialogRef" @refresh="refresh" />
<UserDrawer :title="title" ref="UserDrawerRef" @refresh="refresh"/>
<UserPwdDialog ref="UserPwdDialogRef" @refresh="refresh"/>
<SetUserRoleDialog
ref="setUserRoleRef"
@refresh="refresh"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, reactive, watch } from 'vue'
import {onMounted, ref, reactive, watch, computed, onBeforeMount} from 'vue'
import UserDrawer from './component/UserDrawer.vue'
import UserPwdDialog from './component/UserPwdDialog.vue'
import SetUserRoleDialog from './component/SetUserRoleDialog.vue'
import userManageApi from '@/api/system/user-manage'
import { datetimeFormat } from '@/utils/time'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { t } from '@/locales'
import { ValidCount, ValidType } from '@/enums/common.ts'
import {datetimeFormat} from '@/utils/time'
import {MsgSuccess, MsgConfirm} from '@/utils/message'
import {t} from '@/locales'
import useStore from '@/stores'
import { PermissionConst, RoleConst } from '@/utils/permission/data'
import { hasPermission } from '@/utils/permission/index'
import {PermissionConst, RoleConst} from '@/utils/permission/data'
import {hasPermission} from '@/utils/permission/index'
const { user, common } = useStore()
const {user, common} = useStore()
const search_type = ref('username')
const search_form = ref<{
username: string
@ -290,11 +313,10 @@ const paginationConfig = reactive({
page_size: 20,
total: 0,
})
const userTableData = ref<any[]>([])
const search_type_change = () => {
search_form.value = { username: '', nick_name: '', email: '', is_active: null }
search_form.value = {username: '', nick_name: '', email: '', is_active: null}
}
function handleSizeChange() {
@ -366,7 +388,8 @@ function deleteUserManage(row: any) {
getList()
})
})
.catch(() => {})
.catch(() => {
})
}
function editPwdUser(row: any) {
@ -377,6 +400,35 @@ function refresh() {
getList()
}
const multipleSelection = ref<any[]>([])
function handleSelectionChange(val: any[]) {
multipleSelection.value = val
}
function handleBatchDelete() {
MsgConfirm(t('views.chatUser.batchDeleteUser', {count: multipleSelection.value.length}), '', {
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger',
})
.then(() => {
userManageApi.batchDelete(multipleSelection.value.map((item) => item.id), loading).then(
async () => {
MsgSuccess(t('common.deleteSuccess'))
await getList()
})
})
.catch(() => {
})
}
const setUserRoleRef = ref<InstanceType<typeof SetUserRoleDialog>>()
function setUserRoles() {
setUserRoleRef.value?.open(multipleSelection.value.map((item) => item.id))
}
onMounted(() => {
getList()
})