feat: 用户服务和用量管理

This commit is contained in:
2025-01-21 03:48:56 +08:00
parent 0ad6179b81
commit 6e9b5de8d1
6 changed files with 723 additions and 84 deletions

88
components/DatePicker.vue Normal file
View File

@@ -0,0 +1,88 @@
<script setup lang="ts">
import { DatePicker as VCalendarDatePicker } from 'v-calendar'
// @ts-ignore
import type { DatePickerDate, DatePickerRangeObject } from 'v-calendar/dist/types/src/use/datePicker'
import 'v-calendar/dist/style.css'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: {
type: [Date, Object] as PropType<DatePickerDate | DatePickerRangeObject | null>,
default: null
}
})
const emit = defineEmits(['update:model-value', 'close'])
const date = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:model-value', value)
emit('close')
}
})
const attrs = {
'transparent': true,
'borderless': true,
'color': 'primary',
'is-dark': { selector: 'html', darkClass: 'dark' },
'first-day-of-week': 2
}
function onDayClick(_: any, event: MouseEvent): void {
const target = event.target as HTMLElement
target.blur()
}
</script>
<template>
<VCalendarDatePicker
v-if="date && (date as DatePickerRangeObject)?.start && (date as DatePickerRangeObject)?.end"
v-model.range="date"
:columns="2"
v-bind="{ ...attrs, ...$attrs }"
@dayclick="onDayClick"
/>
<VCalendarDatePicker
v-else
v-model="date"
v-bind="{ ...attrs, ...$attrs }"
@dayclick="onDayClick"
/>
</template>
<style>
:root {
--vc-gray-50: rgb(var(--color-gray-50));
--vc-gray-100: rgb(var(--color-gray-100));
--vc-gray-200: rgb(var(--color-gray-200));
--vc-gray-300: rgb(var(--color-gray-300));
--vc-gray-400: rgb(var(--color-gray-400));
--vc-gray-500: rgb(var(--color-gray-500));
--vc-gray-600: rgb(var(--color-gray-600));
--vc-gray-700: rgb(var(--color-gray-700));
--vc-gray-800: rgb(var(--color-gray-800));
--vc-gray-900: rgb(var(--color-gray-900));
}
.vc-primary {
--vc-accent-50: rgb(var(--color-primary-50));
--vc-accent-100: rgb(var(--color-primary-100));
--vc-accent-200: rgb(var(--color-primary-200));
--vc-accent-300: rgb(var(--color-primary-300));
--vc-accent-400: rgb(var(--color-primary-400));
--vc-accent-500: rgb(var(--color-primary-500));
--vc-accent-600: rgb(var(--color-primary-600));
--vc-accent-700: rgb(var(--color-primary-700));
--vc-accent-800: rgb(var(--color-primary-800));
--vc-accent-900: rgb(var(--color-primary-900));
}
.vc-container .vc-weekday-1, .vc-container .vc-weekday-7 {
@apply text-primary;
}
</style>

View File

@@ -101,11 +101,11 @@ const onClick = () => {
</li>
<li class="">
<h2 class="text-2xs font-medium text-primary-500">完成时间</h2>
<p class="text-xs line-clamp-1">{{ dayjs(video.complete_time * 1000).format('YYYY-MM-DD HH:mm:ss') }}</p>
<p class="text-xs line-clamp-1">{{ video.complete_time ? dayjs(video.complete_time * 1000).format('YYYY-MM-DD HH:mm:ss') : '进行中' }}</p>
</li>
<li class="">
<h2 class="text-2xs font-medium text-primary-500">生成耗时</h2>
<p class="text-xs line-clamp-1">{{ dayjs.duration(video.duration || 0).format('HH:mm:ss') }}</p>
<p class="text-xs line-clamp-1">{{ video.duration ? dayjs.duration(video.duration || 0).format('HH:mm:ss') : '进行中' }}</p>
</li>
<li class="col-span-2 cursor-pointer" @click="isFullContentOpen = true">
<h2 class="text-2xs font-medium text-primary-500">驱动文本</h2>
@@ -136,6 +136,7 @@ const onClick = () => {
<UButton
:label="downloadingState.subtitle > 0 && downloadingState.subtitle < 100 ? `${downloadingState.subtitle.toFixed(0)}%` : '字幕'"
:loading="downloadingState.subtitle > 0 && downloadingState.subtitle < 100"
:disabled="!video.subtitle"
color="primary"
leading-icon="i-tabler-file-download"
variant="soft"
@@ -144,6 +145,7 @@ const onClick = () => {
<UButton
:label="downloadingState.video > 0 && downloadingState.video < 100 ? `${downloadingState.video.toFixed(0)}%` : '视频'"
:loading="downloadingState.video > 0 && downloadingState.video < 100"
:disabled="!video.video_url"
color="primary"
leading-icon="i-tabler-download"
variant="soft"

View File

@@ -20,6 +20,7 @@
"@uniiem/object-trim": "^0.2.0",
"@uniiem/uuid": "^0.2.1",
"@webav/av-cliper": "^1.0.10",
"date-fns": "^4.1.0",
"events": "^3.3.0",
"gsap": "^3.12.5",
"highlight.js": "^11.10.0",
@@ -28,6 +29,7 @@
"nuxt": "^3.12.4",
"nuxt-driver.js": "^0.0.11",
"radix-vue": "^1.9.2",
"v-calendar": "^3.1.2",
"vue": "^3.4.34",
"vue-router": "^4.4.0",
"yup": "^1.4.0"

View File

@@ -1,9 +1,39 @@
<script lang="ts" setup>
import ModalDigitalHumanSelect from '~/components/ModalDigitalHumanSelect.vue'
const toast = useToast()
const dayjs = useDayjs()
const loginState = useLoginState()
const systemServices: ServiceType[] = [
{
tag: 'PPTToVideo',
name: '微课视频(PPT驱动)',
},
{
tag: 'TextToVideo',
name: '绿幕视频(文字驱动)',
},
{
tag: 'SparkChat15',
name: '讯飞星火大模型 1.5',
},
{
tag: 'SparkChat30',
name: '讯飞星火大模型 3.0',
},
{
tag: 'SparkChat35',
name: '讯飞星火大模型 3.5',
},
{
tag: 'TenImgToImg',
name: '腾讯混元 - 图生图',
},
{
tag: 'TenTextToImg',
name: '腾讯混元 - 文生图',
},
]
const columns = [
{
key: 'id',
@@ -17,7 +47,7 @@ const columns = [
},
{
key: 'username',
label: '用户名',
label: '账户名/姓名',
disabled: true,
},
{
@@ -28,14 +58,6 @@ const columns = [
key: 'mobile',
label: '手机号',
},
{
key: 'email',
label: '电子邮箱',
},
{
key: 'nickname',
label: '昵称',
},
{
key: 'sex',
label: '性别',
@@ -50,9 +72,11 @@ const columns = [
},
]
const selectedColumns = ref([...columns.filter(row => {
return !['nickname', 'email', 'auth_code'].includes(row.key)
})])
const selectedColumns = ref([
...columns.filter((row) => {
return !['auth_code'].includes(row.key)
}),
])
const page = ref(1)
const pageCount = ref(15)
const state_filter = ref<'verified' | 'unverified'>('verified')
@@ -60,11 +84,14 @@ const is_verified = ref(true)
const viewingUser = ref<UserSchema | null>(null)
const isSlideOpen = computed({
get: () => !!viewingUser.value,
set: () => viewingUser.value = null,
set: () => (viewingUser.value = null),
})
watch([is_verified, pageCount], () => page.value = 1)
watch(state_filter, () => is_verified.value = state_filter.value === 'verified')
watch([is_verified, pageCount], () => (page.value = 1))
watch(
state_filter,
() => (is_verified.value = state_filter.value === 'verified')
)
const {
data: usersData,
@@ -72,7 +99,8 @@ const {
status: usersDataStatus,
} = useAsyncData(
'systemUsers',
() => useFetchWrapped<
() =>
useFetchWrapped<
req.user.UserList & AuthedRequest,
BaseResponse<PagedData<UserSchema>>
>('App.User_User.ListUser', {
@@ -84,14 +112,18 @@ const {
}),
{
watch: [page, pageCount, is_verified],
transform: (res) => {
res.data.items.unshift(loginState.user)
return res
},
}
)
const isDigitalSelectorOpen = ref(false)
const dhPage = ref(1)
const dhPageCount = ref(10)
watch(dhPageCount, () => dhPage.value = 1)
watch(dhPageCount, () => (dhPage.value = 1))
const {
data: digitalHumansData,
@@ -99,7 +131,8 @@ const {
status: digitalHumansDataStatus,
} = useAsyncData(
'currentUserDigitalHumans',
() => useFetchWrapped<
() =>
useFetchWrapped<
PagedDataRequest & AuthedRequest,
BaseResponse<PagedData<DigitalHumanItem>>
>('App.User_UserDigital.GetList', {
@@ -111,14 +144,43 @@ const {
}),
{
watch: [viewingUser, dhPage, dhPageCount],
},
}
)
const {
data: userBalances,
status: userBalancesStatus,
refresh: refreshUserBalances,
} = useAsyncData(
'userServiceBalances',
() =>
useFetchWrapped<
PagedDataRequest & AuthedRequest,
BaseResponse<PagedData<ServiceBalance>>
>('App.User_UserBalance.GetList', {
token: loginState.token!,
user_id: loginState.user.id,
to_user_id: viewingUser.value?.id || 0,
page: 1,
perpage: 20,
}),
{
watch: [viewingUser],
}
)
const getBalanceByTag = (tag: ServiceTag): ServiceBalance | null => {
return (
userBalances?.value?.data.items.find((row) => row.request_type === tag) ||
null
)
}
const items = (row: UserSchema) => [
[
{
label: '数字人授权',
icon: 'tabler:user-cog',
label: '服务和用量',
icon: 'tabler:server-cog',
click: () => openSlide(row),
disabled: row.auth_code === 0,
},
@@ -155,12 +217,16 @@ const onDigitalHumansSelected = (digitalHumans: DigitalHumanItem[]) => {
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 => {
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 }` : '' }`,
description: `成功授权 ${res.data.success} 个数字人${
res.data.failed ? `,失败 ${res.data.failed}` : ''
}`,
color: 'green',
icon: 'tabler:check',
})
@@ -190,7 +256,7 @@ const revokeDigitalHuman = (uid: number, digitalHumanId: number) => {
user_id: loginState.user.id!,
to_user_id: uid,
digital_human_id: digitalHumanId,
}).then(res => {
}).then((res) => {
if (res.ret === 200 && res.data.code === 1) {
toast.add({
title: '撤销成功',
@@ -224,7 +290,7 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
user_id: loginState.user.id!,
to_user_id: uid,
is_verify: is_verified,
}).then(res => {
}).then((res) => {
if (res.ret === 200 && res.data.status === 1) {
toast.add({
title: '操作成功',
@@ -243,21 +309,91 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
}
})
}
const userBalanceEditing = ref(false)
const userBalanceState = reactive({
expire_time: dayjs().add(1, 'month').unix() * 1000,
remain_count: 1800,
request_type: '',
})
const isActivateBalance = ref(false)
const isBalanceEditModalOpen = computed({
get: () => !!viewingUser && userBalanceEditing.value,
set: (val) => {
userBalanceEditing.value = val
if (!val) {
userBalanceState.expire_time = dayjs().add(1, 'month').unix() * 1000
userBalanceState.remain_count = 1800
}
},
})
const udpateBalance = (tag: ServiceTag, isActivate: boolean = false) => {
useFetchWrapped<
{
to_user_id: number
request_type: ServiceTag
expire_time: number
remain_count: number
} & AuthedRequest,
BaseResponse<{
code: number // 1: success, 0: failed
data_id?: number
}>
>(
isActivate ? 'App.User_UserBalance.Insert' : 'App.User_UserBalance.Update',
{
token: loginState.token!,
user_id: loginState.user.id!,
to_user_id: viewingUser.value?.id || 0,
request_type: tag,
expire_time: userBalanceState.expire_time / 1000,
remain_count: userBalanceState.remain_count,
}
)
.then((res) => {
if (res.ret === 200 && (res.data.code === 1 || !!res.data.data_id)) {
toast.add({
title: '操作成功',
description: `${isActivate ? '开通' : '更新'}服务`,
color: 'green',
icon: 'tabler:check',
})
} else {
toast.add({
title: '操作失败',
description: res.msg || '未知错误',
color: 'red',
icon: 'tabler:alert-triangle',
})
}
})
.catch((err) => {
toast.add({
title: '操作失败',
description: err.message || '未知错误',
color: 'red',
icon: 'tabler:alert-triangle',
})
})
.finally(() => {
refreshUserBalances()
isBalanceEditModalOpen.value = false
})
}
</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)"
<BubbleTitle
bubble-color="amber-500"
subtitle="User Management"
title="用户管理"
>
本账号数字人
</UButton>
<template #action>
<UButton
color="amber"
icon="tabler:reload"
@@ -268,7 +404,10 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
</UButton>
</template>
</BubbleTitle>
<GradientDivider line-gradient-from="amber" line-gradient-to="amber"/>
<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">
@@ -285,17 +424,31 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
<USelectMenu
v-model="state_filter"
:options="[
{label: '正常账号', value: 'verified', icon: 'tabler:user-check'},
{label: '停用账号', value: 'unverified', icon: 'tabler:user-cancel'},
{
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'}}}"
: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'}}}"
: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
@@ -315,15 +468,35 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
:rows="usersData?.data.items"
class="border dark:border-neutral-800 rounded-md"
>
<template #username-data="{ row }">
<span
:class="{
'font-semibold text-amber-500': row.id === loginState.user.id,
}"
>
{{ row.username }}
<span class="text-xs">
{{ row.id === loginState.user.id ? ' (本账号)' : '' }}
</span>
</span>
</template>
<template #avatar-data="{ row }">
<UAvatar :alt="row.username.toUpperCase()" :src="row.avatar" size="sm"/>
<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"/>
<UButton
color="gray"
icon="tabler:dots"
variant="ghost"
/>
</UDropdown>
</template>
</UTable>
@@ -337,9 +510,16 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
</div>
</div>
</div>
<USlideover v-model="isSlideOpen">
<USlideover
v-model="isSlideOpen"
:ui="{ width: 'w-screen max-w-3xl' }"
>
<UCard
:ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }"
:ui="{
body: { base: 'flex-1' },
ring: '',
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
}"
class="flex flex-col flex-1"
>
<template #header>
@@ -353,11 +533,282 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
variant="ghost"
@click="closeSlide"
/>
数字人授权管理
<p class="text-sm font-medium text-primary">{{ viewingUser?.username }} (UID:{{ viewingUser?.id }})</p>
服务和用量管理
<p class="text-sm font-medium text-primary">
{{ viewingUser?.username }} (UID:{{ viewingUser?.id }})
</p>
</template>
<div class="flex w-full justify-end pb-4">
<UDivider
label="服务用量管理"
class="mb-4"
:ui="{ label: 'text-primary-500 dark:text-primary-400' }"
/>
<div class="border dark:border-neutral-700 rounded-md">
<UTable
:columns="[
{ key: 'service', label: '服务' },
{ key: 'status', label: '状态' },
{ key: 'create_time', label: '开通时间' },
{ key: 'expire_time', label: '过期时间' },
{ key: 'remain_count', label: '余量(秒)' },
{ key: 'actions' },
]"
:loading="userBalancesStatus === 'pending'"
:rows="[
...systemServices,
// 如果 userBalances?.data.items 具有 systemServices 中没有的服务,则添加到列表中
...(userBalances?.data.items
.filter(
(row) =>
!systemServices.find((s) => s.tag === row.request_type)
)
.map((row) => ({
tag: row.request_type,
name: row.request_type,
})) || []),
]"
>
<template #service-data="{ row }: { row: ServiceType }">
{{
systemServices.find((s) => s.tag === row.tag)?.name || row.tag
}}
</template>
<template #status-data="{ row }">
<UBadge
v-if="!getBalanceByTag(row.tag)"
color="gray"
variant="solid"
size="xs"
>
未开通
</UBadge>
<UBadge
v-else-if="
getBalanceByTag(row.tag)!.expire_time < dayjs().unix()
"
color="red"
variant="subtle"
size="xs"
>
已过期
</UBadge>
<UBadge
v-else
color="green"
variant="subtle"
size="xs"
>
生效中
</UBadge>
</template>
<template #create_time-data="{ row }">
<span class="text-xs">
{{
!!getBalanceByTag(row.tag)
? dayjs(
getBalanceByTag(row.tag)!.create_time * 1000
).format('YYYY-MM-DD HH:mm:ss')
: '未开通'
}}
</span>
</template>
<template #expire_time-data="{ row }">
<span class="text-xs">
{{
!!getBalanceByTag(row.tag)
? dayjs(
getBalanceByTag(row.tag)!.expire_time * 1000
).format('YYYY-MM-DD HH:mm:ss')
: '未开通'
}}
</span>
</template>
<template #remain_count-data="{ row }">
<span class="text-sm">
{{ getBalanceByTag(row.tag)?.remain_count || 0 }}
</span>
</template>
<template #actions-data="{ row }">
<UButton
v-if="!getBalanceByTag(row.tag)"
color="green"
icon="tabler:clock-check"
size="xs"
variant="soft"
@click="
() => {
isActivateBalance = true
userBalanceEditing = true
userBalanceState.request_type = row.tag
}
"
>
开通
</UButton>
<UButton
v-else-if="getBalanceByTag(row.tag)!.expire_time < dayjs().unix()"
color="teal"
icon="tabler:clock-plus"
size="xs"
variant="soft"
@click="
() => {
isActivateBalance = false
userBalanceEditing = true
userBalanceState.request_type = row.tag
}
"
>
延续
</UButton>
<UButton
v-else
color="sky"
icon="tabler:rotate-clockwise"
size="xs"
variant="soft"
@click="
() => {
isActivateBalance = false
userBalanceEditing = true
userBalanceState.request_type = row.tag
userBalanceState.expire_time =
getBalanceByTag(row.tag)!.expire_time * 1000
userBalanceState.remain_count = getBalanceByTag(row.tag)!.remain_count
}
"
>
更新
</UButton>
</template>
</UTable>
<UModal
v-model="isBalanceEditModalOpen"
:ui="{ width: 'w-xl' }"
>
<UCard
:ui="{
ring: '',
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
}"
>
<template #header>
<!-- 为 {{ viewingUser!.username }}
<span class="text-primary">{{ isActivateBalance ? '开通' : '续期' }}</span>
服务:
{{
systemServices.find(
(s) => s.tag === userBalanceState.request_type
)?.name || userBalanceState.request_type
}} -->
<div class="flex justify-between items-center gap-1">
<h1 class="text-sm font-medium">
{{ isActivateBalance ? '开通' : '续期' }}
{{
systemServices.find(
(s) => s.tag === userBalanceState.request_type
)?.name || userBalanceState.request_type
}}
服务
</h1>
<p
class="text-xs text-indigo-500 inline-flex items-center gap-1"
>
<UIcon
name="tabler:user-circle"
class="text-base"
/>
{{ viewingUser!.username }}
</p>
</div>
</template>
<UForm class="flex justify-between gap-4">
<UFormGroup
label="到期时间"
class="flex-1"
>
<!-- <UInput
v-model="userBalanceState.expire_time"
type="datetime"
/> -->
<UPopover :popper="{ placement: 'bottom-start' }">
<UButton
block
variant="soft"
icon="i-heroicons-calendar-days-20-solid"
:label="
dayjs(userBalanceState.expire_time).format(
'YYYY-MM-DD HH:mm'
)
"
/>
<template #panel="{ close }">
<DatePicker
v-model="userBalanceState.expire_time"
locale="cn"
mode="dateTime"
title-position="left"
is-required
is24hr
@close="close"
/>
</template>
</UPopover>
</UFormGroup>
<UFormGroup
label="服务时长(秒)"
class="flex-1"
>
<UInput v-model="userBalanceState.remain_count" />
</UFormGroup>
</UForm>
<template #footer>
<div class="flex items-center justify-end gap-2">
<UButton
color="gray"
size="sm"
variant="ghost"
@click="isBalanceEditModalOpen = false"
>
取消
</UButton>
<UButton
color="primary"
icon="tabler:check"
size="sm"
@click="
udpateBalance(
userBalanceState.request_type as ServiceTag,
isActivateBalance
)
"
>
{{ isActivateBalance ? '开通' : '续期' }}
</UButton>
</div>
</template>
</UCard>
</UModal>
</div>
<UDivider
label="数字人权限管理"
class="my-4"
:ui="{ label: 'text-primary-500 dark:text-primary-400' }"
/>
<!-- <div class="flex w-full justify-end pb-4">
<UButton
icon="tabler:plus"
size="xs"
@@ -365,12 +816,12 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
>
新增授权
</UButton>
</div>
</div> -->
<div class="border dark:border-neutral-700 rounded-md">
<UTable
:columns="[
{ key: 'name', label: '名称' },
{key: 'digital_human_id', label: '平台ID'},
{ key: 'digital_human_id', label: '本地ID' },
{ key: 'model_id', label: '上游ID' },
{ key: 'actions' },
]"
@@ -383,14 +834,23 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
icon="tabler:cancel"
size="xs"
variant="ghost"
@click="revokeDigitalHuman(viewingUser?.id || 0, row.digital_human_id)"
@click="
revokeDigitalHuman(viewingUser?.id || 0, row.digital_human_id)
"
>
撤销授权
</UButton>
</template>
</UTable>
</div>
<div class="flex justify-end py-3.5">
<div class="flex justify-between py-3.5">
<UButton
icon="tabler:plus"
size="xs"
@click="isDigitalSelectorOpen = true"
>
新增授权
</UButton>
<UPagination
v-if="(digitalHumansData?.data.total || -1) > 0"
v-model="dhPage"
@@ -400,7 +860,9 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
</div>
</UCard>
<ModalDigitalHumanSelect
:disabled-digital-human-ids="digitalHumansData?.data.items.map(d => d.model_id)"
:disabled-digital-human-ids="
digitalHumansData?.data.items.map((d) => d.model_id)
"
:is-open="isDigitalSelectorOpen"
default-tab="system"
multiple
@@ -411,6 +873,4 @@ const setUserStatus = (uid: number, is_verified: boolean) => {
</LoginNeededContent>
</template>
<style scoped>
</style>
<style scoped></style>

64
pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ importers:
'@webav/av-cliper':
specifier: ^1.0.10
version: 1.0.10
date-fns:
specifier: ^4.1.0
version: 4.1.0
dayjs:
specifier: ^1.11.12
version: 1.11.12
@@ -65,6 +68,9 @@ importers:
tailwindcss:
specifier: ^3.4.7
version: 3.4.17
v-calendar:
specifier: ^3.1.2
version: 3.1.2(@popperjs/core@2.11.8)(vue@3.4.34)
vue:
specifier: ^3.4.34
version: 3.4.34
@@ -1782,6 +1788,9 @@ packages:
'@types/linkify-it@3.0.5':
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==}
'@types/lodash@4.17.14':
resolution: {integrity: sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==}
'@types/markdown-it@13.0.9':
resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
@@ -1791,6 +1800,9 @@ packages:
'@types/node@20.14.12':
resolution: {integrity: sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==}
'@types/resize-observer-browser@0.1.11':
resolution: {integrity: sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -2579,6 +2591,18 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
date-fns-tz@2.0.1:
resolution: {integrity: sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA==}
peerDependencies:
date-fns: 2.x
date-fns@2.30.0:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dayjs-nuxt@2.1.9:
resolution: {integrity: sha512-7L1X8Wm+O2s2o+YK8RH1GK0SUUsh7l0lnIzsjaLGvyu64asLs2KVNoh25J4tQurdILp2TyrFaTo/k3k/ZPub9Q==}
@@ -5136,6 +5160,12 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
v-calendar@3.1.2:
resolution: {integrity: sha512-QDWrnp4PWCpzUblctgo4T558PrHgHzDtQnTeUNzKxfNf29FkCeFpwGd9bKjAqktaa2aJLcyRl45T5ln1ku34kg==}
peerDependencies:
'@popperjs/core': ^2.0.0
vue: ^3.2.0
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -5285,6 +5315,11 @@ packages:
peerDependencies:
vue: ^3.2.0
vue-screen-utils@1.0.0-beta.13:
resolution: {integrity: sha512-EJ/8TANKhFj+LefDuOvZykwMr3rrLFPLNb++lNBqPOpVigT2ActRg6icH9RFQVm4nHwlHIHSGm5OY/Clar9yIg==}
peerDependencies:
vue: ^3.2.0
vue@3.4.34:
resolution: {integrity: sha512-VZze05HWlA3ItreQ/ka7Sx7PoD0/3St8FEiSlSTVgb6l4hL+RjtP2/8g5WQBzZgyf8WG2f+g1bXzC7zggLhAJA==}
peerDependencies:
@@ -7472,6 +7507,8 @@ snapshots:
'@types/linkify-it@3.0.5': {}
'@types/lodash@4.17.14': {}
'@types/markdown-it@13.0.9':
dependencies:
'@types/linkify-it': 3.0.5
@@ -7483,6 +7520,8 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/resize-observer-browser@0.1.11': {}
'@types/resolve@1.20.2': {}
'@types/trusted-types@2.0.7': {}
@@ -8454,6 +8493,16 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
date-fns-tz@2.0.1(date-fns@2.30.0):
dependencies:
date-fns: 2.30.0
date-fns@2.30.0:
dependencies:
'@babel/runtime': 7.26.0
date-fns@4.1.0: {}
dayjs-nuxt@2.1.9(magicast@0.3.4)(rollup@4.19.1):
dependencies:
'@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.1)
@@ -11479,6 +11528,17 @@ snapshots:
util-deprecate@1.0.2: {}
v-calendar@3.1.2(@popperjs/core@2.11.8)(vue@3.4.34):
dependencies:
'@popperjs/core': 2.11.8
'@types/lodash': 4.17.14
'@types/resize-observer-browser': 0.1.11
date-fns: 2.30.0
date-fns-tz: 2.0.1(date-fns@2.30.0)
lodash: 4.17.21
vue: 3.4.34
vue-screen-utils: 1.0.0-beta.13(vue@3.4.34)
vary@1.1.2: {}
vite-hot-client@0.2.3(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)):
@@ -11613,6 +11673,10 @@ snapshots:
'@vue/devtools-api': 6.6.3
vue: 3.4.34
vue-screen-utils@1.0.0-beta.13(vue@3.4.34):
dependencies:
vue: 3.4.34
vue@3.4.34:
dependencies:
'@vue/compiler-dom': 3.4.34

23
typings/types.d.ts vendored
View File

@@ -35,6 +35,29 @@ interface UserSchema {
company?: string
}
type ServiceTag =
| 'PPTToVideo'
| 'TextToVideo'
| 'SparkChat15'
| 'SparkChat30'
| 'SparkChat35'
| 'TenTextToImg'
| 'TenImgToImg'
interface ServiceType {
name: string
tag: ServiceTag
}
interface ServiceBalance {
id: number
user_id: number
create_time: number
expire_time: number
remain_count: number
request_type: ServiceTag
}
interface DigitalHumanItem {
user_id: number
create_time: number