416 lines
11 KiB
Vue
416 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 state_filter = ref<'verified' | 'unverified'>('verified')
|
|
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)
|
|
watch(state_filter, () => is_verified.value = state_filter.value === 'verified')
|
|
|
|
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="state_filter"
|
|
:options="[
|
|
{label: '正常账号', value: 'verified', icon: 'tabler:user-check'},
|
|
{label: '停用账号', value: 'unverified', 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> |