Files
xsh-assistant-next/pages/generation/admin/users.vue

414 lines
11 KiB
Vue

<script lang="ts" setup>
import ModalDigitalHumanSelect from '~/components/ModalDigitalHumanSelect.vue'
const toast = useToast()
const loginState = useLoginState()
const columns = [
{
key: 'id',
label: 'ID',
disabled: true,
},
{
key: 'avatar',
label: '头像',
disabled: true,
},
{
key: 'username',
label: '用户名',
disabled: true,
},
{
key: 'company',
label: '单位',
},
{
key: 'mobile',
label: '手机号',
},
{
key: 'email',
label: '电子邮箱',
},
{
key: 'nickname',
label: '昵称',
},
{
key: 'sex',
label: '性别',
},
{
key: 'auth_code',
label: '角色',
},
{
key: 'actions',
label: '操作',
},
]
const selectedColumns = ref([...columns.filter(row => {
return !['nickname', 'email', 'auth_code'].includes(row.key)
})])
const page = ref(1)
const pageCount = ref(15)
const is_verified = ref(true)
const viewingUser = ref<UserSchema | null>(null)
const isSlideOpen = computed({
get: () => !!viewingUser.value,
set: () => viewingUser.value = null,
})
watch([is_verified, pageCount], () => page.value = 1)
const {
data: usersData,
refresh: refreshUsersData,
status: usersDataStatus,
} = useAsyncData(
'systemUsers',
() => useFetchWrapped<
req.user.UserList & AuthedRequest,
BaseResponse<PagedData<UserSchema>>
>('App.User_User.ListUser', {
token: loginState.token!,
user_id: loginState.user.id!,
page: page.value,
perpage: pageCount.value,
is_verify: is_verified.value,
}),
{
watch: [page, pageCount, is_verified],
},
)
const isDigitalSelectorOpen = ref(false)
const dhPage = ref(1)
const dhPageCount = ref(10)
watch(dhPageCount, () => dhPage.value = 1)
const {
data: digitalHumansData,
refresh: refreshDigitalHumansData,
status: digitalHumansDataStatus,
} = useAsyncData(
'currentUserDigitalHumans',
() => useFetchWrapped<
PagedDataRequest & AuthedRequest,
BaseResponse<PagedData<DigitalHumanItem>>
>('App.User_UserDigital.GetList', {
token: loginState.token!,
user_id: loginState.user.id!,
to_user_id: viewingUser.value?.id || 0,
page: dhPage.value,
perpage: dhPageCount.value,
}),
{
watch: [viewingUser, dhPage, dhPageCount],
},
)
const items = (row: UserSchema) => [
[
{
label: '数字人授权',
icon: 'tabler:user-cog',
click: () => openSlide(row),
disabled: row.auth_code === 0,
},
],
[
{
label: row.auth_code !== 0 ? '停用账号' : '启用账号',
icon: row.auth_code !== 0 ? 'tabler:cancel' : 'tabler:shield-check',
click: () => setUserStatus(row.id, row.auth_code === 0),
},
],
]
const openSlide = (user: UserSchema) => {
viewingUser.value = user
}
const closeSlide = () => {
viewingUser.value = null
}
const onDigitalHumansSelected = (digitalHumans: DigitalHumanItem[]) => {
useFetchWrapped<
{
to_user_id: number
digital_human_array: number[]
} & AuthedRequest,
BaseResponse<{
total: number
success: number
failed: number
}>
>('App.User_UserDigital.CreateConnArr', {
token: loginState.token!,
user_id: loginState.user.id!,
to_user_id: viewingUser.value?.id || 0,
digital_human_array: digitalHumans.map(row => row.id || row.digital_human_id),
}).then(res => {
if (res.ret === 200) {
toast.add({
title: '授权成功',
description: `成功授权 ${ res.data.success } 个数字人${ res.data.failed ? `,失败 ${ res.data.failed }` : '' }`,
color: 'green',
icon: 'tabler:check',
})
refreshDigitalHumansData()
} else {
toast.add({
title: '授权失败',
description: res.msg || '未知错误',
color: 'red',
icon: 'tabler:alert-triangle',
})
}
})
}
const revokeDigitalHuman = (uid: number, digitalHumanId: number) => {
useFetchWrapped<
{
to_user_id: number
digital_human_id: number
} & AuthedRequest,
BaseResponse<{
code: number // 1: success, 0: failed
}>
>('App.User_UserDigital.DeleteConn', {
token: loginState.token!,
user_id: loginState.user.id!,
to_user_id: uid,
digital_human_id: digitalHumanId,
}).then(res => {
if (res.ret === 200 && res.data.code === 1) {
toast.add({
title: '撤销成功',
description: '已撤销数字人授权',
color: 'green',
icon: 'tabler:check',
})
refreshDigitalHumansData()
} else {
toast.add({
title: '撤销失败',
description: res.msg || '未知错误',
color: 'red',
icon: 'tabler:alert-triangle',
})
}
})
}
const setUserStatus = (uid: number, is_verified: boolean) => {
useFetchWrapped<
{
to_user_id: number
is_verify: boolean
} & AuthedRequest,
BaseResponse<{
status: number // 1: success, 0: failed
}>
>('App.User_User.SetUserVerify', {
token: loginState.token!,
user_id: loginState.user.id!,
to_user_id: uid,
is_verify: is_verified,
}).then(res => {
if (res.ret === 200 && res.data.status === 1) {
toast.add({
title: '操作成功',
description: `${ is_verified ? '启用' : '停用' }账号`,
color: 'green',
icon: is_verified ? 'tabler:shield-check' : 'tabler:cancel',
})
refreshUsersData()
} else {
toast.add({
title: '操作失败',
description: res.msg || '未知错误',
color: 'red',
icon: 'tabler:alert-triangle',
})
}
})
}
</script>
<template>
<LoginNeededContent need-admin>
<div class="p-4">
<BubbleTitle bubble-color="amber-500" subtitle="User Management" title="用户管理">
<template #action>
<UButton
color="amber"
icon="tabler:users"
variant="soft"
@click="openSlide(loginState.user)"
>
本账号数字人
</UButton>
<UButton
color="amber"
icon="tabler:reload"
variant="soft"
@click="refreshUsersData"
>
刷新
</UButton>
</template>
</BubbleTitle>
<GradientDivider line-gradient-from="amber" line-gradient-to="amber"/>
<div>
<div class="flex justify-between items-center w-full py-3">
<div class="flex items-center gap-1.5">
<span class="text-sm leading-0">每页显示:</span>
<USelect
v-model="pageCount"
:options="[5, 10, 15, 20]"
class="me-2 w-20"
size="xs"
/>
</div>
<div class="flex gap-1.5 items-center">
<USelectMenu
v-model="is_verified"
:options="[
{label: '正常账号', value: true, icon: 'tabler:user-check'},
{label: '停用账号', value: false, icon: 'tabler:user-cancel'},
]"
:ui-menu="{width: 'w-fit', option: {size: 'text-xs', icon: {base: 'w-4 h-4'}}}"
size="xs"
value-attribute="value"
/>
<USelectMenu
v-model="selectedColumns"
:options="columns.filter(row => !['actions'].includes(row.key))"
:ui-menu="{width: 'w-fit', option: {size: 'text-xs', icon: {base: 'w-4 h-4'}}}"
multiple
>
<UButton
color="gray"
icon="tabler:layout-columns"
size="xs"
>
显示列
</UButton>
</USelectMenu>
</div>
</div>
<UTable
:columns="selectedColumns"
:loading="usersDataStatus === 'pending'"
:progress="{color: 'amber', animation: 'carousel'}"
:rows="usersData?.data.items"
class="border dark:border-neutral-800 rounded-md"
>
<template #avatar-data="{ row }">
<UAvatar :alt="row.username.toUpperCase()" :src="row.avatar" size="sm"/>
</template>
<template #sex-data="{ row }">
{{ row.sex === 0 ? '' : row.sex === 1 ? '男' : '女' }}
</template>
<template #actions-data="{ row }">
<UDropdown :items="items(row)">
<UButton color="gray" icon="tabler:dots" variant="ghost"/>
</UDropdown>
</template>
</UTable>
<div class="flex justify-end py-3.5">
<UPagination
v-if="(usersData?.data.total || -1) > 0"
v-model="page"
:page-count="pageCount"
:total="usersData?.data.total || 0"
/>
</div>
</div>
</div>
<USlideover v-model="isSlideOpen">
<UCard
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
class="flex flex-col flex-1"
>
<template #header>
<UButton
class="flex absolute end-5 top-5 z-10"
color="gray"
icon="tabler:x"
padded
size="sm"
square
variant="ghost"
@click="closeSlide"
/>
数字人授权管理
<p class="text-sm font-medium text-primary">{{ viewingUser?.username }} (UID:{{ viewingUser?.id }})</p>
</template>
<div class="flex w-full justify-end pb-4">
<UButton
icon="tabler:plus"
size="xs"
@click="isDigitalSelectorOpen = true"
>
新增授权
</UButton>
</div>
<div class="border dark:border-neutral-700 rounded-md">
<UTable
:columns="[
{key: 'name', label: '名称'},
{key: 'digital_human_id', label: '平台ID'},
{key: 'model_id', label: '上游ID'},
{key: 'actions'},
]"
:loading="digitalHumansDataStatus === 'pending'"
:rows="digitalHumansData?.data.items"
>
<template #actions-data="{ row }">
<UButton
color="gray"
icon="tabler:cancel"
size="xs"
variant="ghost"
@click="revokeDigitalHuman(viewingUser?.id || 0, row.digital_human_id)"
>
撤销授权
</UButton>
</template>
</UTable>
</div>
<div class="flex justify-end py-3.5">
<UPagination
v-if="(digitalHumansData?.data.total || -1) > 0"
v-model="dhPage"
:page-count="dhPageCount"
:total="digitalHumansData?.data.total || 0"
/>
</div>
</UCard>
<ModalDigitalHumanSelect
:disabled-digital-human-ids="digitalHumansData?.data.items.map(d => d.model_id)"
:is-open="isDigitalSelectorOpen"
default-tab="system"
multiple
@close="isDigitalSelectorOpen = false"
@select="digitalHumans => onDigitalHumansSelected(digitalHumans as DigitalHumanItem[])"
/>
</USlideover>
</LoginNeededContent>
</template>
<style scoped>
</style>