feat: 新增数字人训练提交和审核功能
This commit is contained in:
476
components/DigitalHumanTrainCreator.vue
Normal file
476
components/DigitalHumanTrainCreator.vue
Normal file
@@ -0,0 +1,476 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { object, string } from 'yup'
|
||||||
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
|
const isOpen = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单状态
|
||||||
|
const formState = reactive({
|
||||||
|
dh_name: '',
|
||||||
|
organization: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 文件上传状态
|
||||||
|
const videoFile = ref<File | null>(null)
|
||||||
|
const authVideoFile = ref<File | null>(null)
|
||||||
|
const isSubmitting = ref(false)
|
||||||
|
|
||||||
|
// 上传进度状态
|
||||||
|
const uploadProgress = reactive({
|
||||||
|
step: 0,
|
||||||
|
total: 3,
|
||||||
|
message: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证
|
||||||
|
const schema = object({
|
||||||
|
dh_name: string()
|
||||||
|
.required('请输入数字人名称')
|
||||||
|
.max(50, '数字人名称不能超过50个字符'),
|
||||||
|
organization: string()
|
||||||
|
.required('请输入单位名称')
|
||||||
|
.max(100, '单位名称不能超过100个字符'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新上传进度
|
||||||
|
const updateProgress = (step: number, message: string) => {
|
||||||
|
uploadProgress.step = step
|
||||||
|
uploadProgress.message = message
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数字人视频上传
|
||||||
|
const handleVideoUpload = (files: FileList) => {
|
||||||
|
const file = files[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
if (!['video/mp4', 'video/mov'].includes(file.type)) {
|
||||||
|
toast.add({
|
||||||
|
title: '文件格式错误',
|
||||||
|
description: '仅支持MP4和MOV格式的视频文件',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小 (1GB)
|
||||||
|
if (file.size > 1024 * 1024 * 1024) {
|
||||||
|
toast.add({
|
||||||
|
title: '文件过大',
|
||||||
|
description: '视频文件大小不能超过1GB',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
videoFile.value = file
|
||||||
|
toast.add({
|
||||||
|
title: '文件上传成功',
|
||||||
|
description: '数字人视频已选择',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-tabler-check',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理授权视频上传
|
||||||
|
const handleAuthVideoUpload = (files: FileList) => {
|
||||||
|
const file = files[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
if (!['video/mp4', 'video/mov'].includes(file.type)) {
|
||||||
|
toast.add({
|
||||||
|
title: '文件格式错误',
|
||||||
|
description: '仅支持MP4和MOV格式的视频文件',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小
|
||||||
|
if (file.size > 1024 * 1024 * 1024) {
|
||||||
|
toast.add({
|
||||||
|
title: '文件过大',
|
||||||
|
description: '视频文件大小不能超过1GB',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authVideoFile.value = file
|
||||||
|
toast.add({
|
||||||
|
title: '文件上传成功',
|
||||||
|
description: '授权视频已选择',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-tabler-check',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const onSubmit = async (event: FormSubmitEvent<typeof formState>) => {
|
||||||
|
// 验证文件是否已上传
|
||||||
|
if (!videoFile.value) {
|
||||||
|
toast.add({
|
||||||
|
title: '请上传数字人视频素材',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authVideoFile.value) {
|
||||||
|
toast.add({
|
||||||
|
title: '请上传形象授权视频',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSubmitting.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
isSubmitting.value = true
|
||||||
|
updateProgress(0, '开始创建数字人...')
|
||||||
|
|
||||||
|
// 上传数字人视频素材
|
||||||
|
updateProgress(1, '上传数字人视频素材...')
|
||||||
|
const videoUrl = await useFileGo(videoFile.value, 'material')
|
||||||
|
|
||||||
|
// 上传形象授权视频
|
||||||
|
updateProgress(2, '上传形象授权视频...')
|
||||||
|
const authVideoUrl = await useFileGo(authVideoFile.value, 'material')
|
||||||
|
|
||||||
|
// 创建数字人定制记录
|
||||||
|
updateProgress(3, '创建数字人定制记录...')
|
||||||
|
const response = await useFetchWrapped<
|
||||||
|
{
|
||||||
|
user_id: number
|
||||||
|
dh_name: string
|
||||||
|
organization: string
|
||||||
|
video_url: string
|
||||||
|
auth_video_url: string
|
||||||
|
} & AuthedRequest,
|
||||||
|
BaseResponse<{ train_id: number }>
|
||||||
|
>('App.Digital_Train.Create', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
dh_name: event.data.dh_name,
|
||||||
|
organization: event.data.organization,
|
||||||
|
video_url: videoUrl,
|
||||||
|
auth_video_url: authVideoUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ret === 200 && response.data.train_id) {
|
||||||
|
toast.add({
|
||||||
|
title: '数字人定制提交成功',
|
||||||
|
description: '您的数字人定制请求已提交,请等待管理员处理',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-tabler-check',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
formState.dh_name = ''
|
||||||
|
formState.organization = ''
|
||||||
|
videoFile.value = null
|
||||||
|
authVideoFile.value = null
|
||||||
|
uploadProgress.step = 0
|
||||||
|
uploadProgress.message = ''
|
||||||
|
|
||||||
|
// 关闭弹窗
|
||||||
|
isOpen.value = false
|
||||||
|
} else {
|
||||||
|
throw new Error(response.msg || '创建失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数字人定制失败:', error)
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '数字人定制失败,请重试'
|
||||||
|
toast.add({
|
||||||
|
title: '提交失败',
|
||||||
|
description: errorMessage,
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false
|
||||||
|
uploadProgress.step = 0
|
||||||
|
uploadProgress.message = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
formState.dh_name = ''
|
||||||
|
formState.organization = ''
|
||||||
|
videoFile.value = null
|
||||||
|
authVideoFile.value = null
|
||||||
|
uploadProgress.step = 0
|
||||||
|
uploadProgress.message = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听弹窗关闭事件,重置表单
|
||||||
|
watch(isOpen, (newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 显示授权文案弹窗
|
||||||
|
const showAuthModal = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UModal v-model="isOpen" :ui="{ width: 'sm:max-w-6xl' }">
|
||||||
|
<UCard
|
||||||
|
:ui="{
|
||||||
|
ring: '',
|
||||||
|
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
数字人定制
|
||||||
|
</h3>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
|
class="-my-1"
|
||||||
|
@click="isOpen = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7 gap-6">
|
||||||
|
<!-- 左侧表单 -->
|
||||||
|
<div class="col-span-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-800">
|
||||||
|
<UForm
|
||||||
|
:schema="schema"
|
||||||
|
:state="formState"
|
||||||
|
class="space-y-4"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<!-- 数字人视频素材 -->
|
||||||
|
<UFormGroup label="数字人视频素材" required>
|
||||||
|
<UniFileDnD
|
||||||
|
accept="video/mp4,video/mov"
|
||||||
|
class="h-36"
|
||||||
|
@change="handleVideoUpload"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div class="text-center">
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-video-camera"
|
||||||
|
class="mx-auto h-12 w-12 text-gray-400"
|
||||||
|
/>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ videoFile ? videoFile.name : '点击或拖拽上传视频' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
小于 1GB 的 mov/mp4 格式,比例 9:16,帧率 25FPS,分辨率 1080P,时长 3-6 分钟
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UniFileDnD>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<!-- 数字人名称 -->
|
||||||
|
<UFormGroup label="数字人名称" name="dh_name" required>
|
||||||
|
<UInput
|
||||||
|
v-model="formState.dh_name"
|
||||||
|
placeholder="请输入数字人名称"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<!-- 单位名称 -->
|
||||||
|
<UFormGroup label="单位名称" name="organization" required>
|
||||||
|
<UInput
|
||||||
|
v-model="formState.organization"
|
||||||
|
placeholder="请输入单位名称"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<!-- 形象授权视频 -->
|
||||||
|
<UFormGroup label="形象授权视频" required>
|
||||||
|
<template #description>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-xs text-gray-500">
|
||||||
|
请确保本人进行形象授权视频录制,否则脸部比对将不通过导致制作失败
|
||||||
|
</span>
|
||||||
|
<UButton
|
||||||
|
variant="link"
|
||||||
|
size="xs"
|
||||||
|
icon="i-heroicons-document-text"
|
||||||
|
@click="showAuthModal = true"
|
||||||
|
>
|
||||||
|
授权文案
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<UniFileDnD
|
||||||
|
accept="video/mp4,video/mov"
|
||||||
|
class="h-36"
|
||||||
|
@change="handleAuthVideoUpload"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div class="text-center">
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-shield-check"
|
||||||
|
class="mx-auto h-12 w-12 text-gray-400"
|
||||||
|
/>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ authVideoFile ? authVideoFile.name : '点击或拖拽上传授权视频' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UniFileDnD>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
class="w-full"
|
||||||
|
:loading="isSubmitting"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ isSubmitting ? '提交中...' : '确认提交' }}
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<!-- 上传进度 -->
|
||||||
|
<div v-if="isSubmitting" class="mt-4 space-y-2">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span>{{ uploadProgress.message }}</span>
|
||||||
|
<span>{{ uploadProgress.step }}/{{ uploadProgress.total }}</span>
|
||||||
|
</div>
|
||||||
|
<UProgress
|
||||||
|
:value="(uploadProgress.step / uploadProgress.total) * 100"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UForm>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧教程和提示 -->
|
||||||
|
<div class="col-span-4 p-4 rounded-lg border dark:border-gray-700">
|
||||||
|
<div class="flex flex-col h-full gap-6">
|
||||||
|
<!-- 教程视频 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-lg font-semibold mb-3 text-gray-800 dark:text-white flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-video-camera" class="h-5 w-5" />
|
||||||
|
视频录制教程
|
||||||
|
</h3>
|
||||||
|
<div class="w-full aspect-video border rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
|
||||||
|
<UIcon name="i-heroicons-video-camera" class="h-12 w-12 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 联系方式 -->
|
||||||
|
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4 border border-blue-200 dark:border-blue-700">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-blue-100 dark:bg-blue-900 p-2 rounded-lg">
|
||||||
|
<UIcon name="i-heroicons-chat-bubble-left-right" class="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-800 dark:text-white">需要帮助?</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
客服微信:<span class="font-mono text-blue-600 dark:text-blue-400">xxxxxx</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 录制指南 -->
|
||||||
|
<div class="bg-amber-50 dark:bg-amber-900/20 rounded-lg p-4 border border-amber-200 dark:border-amber-700">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="bg-amber-100 dark:bg-amber-900 p-2 rounded-lg mt-0.5">
|
||||||
|
<UIcon name="i-heroicons-light-bulb" class="h-5 w-5 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-800 dark:text-white mb-3">录制注意事项</h4>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-sun" class="h-4 w-4 text-amber-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span class="text-xs text-gray-600 dark:text-gray-300">确保光线充足,避免背光</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-speaker-wave" class="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span class="text-xs text-gray-600 dark:text-gray-300">选择安静环境,减少噪音干扰</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-viewfinder-circle" class="h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span class="text-xs text-gray-600 dark:text-gray-300">人脸占画面比例控制在 1/4 以内</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-heroicons-face-smile" class="h-4 w-4 text-purple-500 mt-0.5 flex-shrink-0" />
|
||||||
|
<span class="text-xs text-gray-600 dark:text-gray-300">保持自然表情,使用恰当手势</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<!-- 授权文案弹窗 -->
|
||||||
|
<UModal v-model="showAuthModal">
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
授权视频文案
|
||||||
|
</h3>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-x-mark-20-solid"
|
||||||
|
class="-my-1"
|
||||||
|
@click="showAuthModal = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
请确保您是视频中人物的合法授权人,在授权视频中朗读以下文案:
|
||||||
|
</p>
|
||||||
|
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-4">
|
||||||
|
<p class="text-sm leading-relaxed text-gray-800 dark:text-gray-200">
|
||||||
|
我是在"AI智慧职教平台"定制上传视频的模特本人,我承诺已经按照平台规则进行合法授权,特此承诺。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</UModal>
|
||||||
|
</UModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -39,9 +39,9 @@ const navList = ref<
|
|||||||
to: '/generation/ppt-templates',
|
to: '/generation/ppt-templates',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '用户管理',
|
label: '管理中心',
|
||||||
icon: 'tabler:users',
|
icon: 'tabler:home-cog',
|
||||||
to: '/generation/admin/users',
|
to: '/generation/admin',
|
||||||
admin: true,
|
admin: true,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
574
pages/generation/admin/digital-human-train.vue
Normal file
574
pages/generation/admin/digital-human-train.vue
Normal file
@@ -0,0 +1,574 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { object, string, number } from 'yup'
|
||||||
|
import type { FormSubmitEvent } from '#ui/types'
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: '数字人定制管理 | 管理员',
|
||||||
|
})
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
|
// 定制记录列表
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: trainListResp,
|
||||||
|
status: trainListStatus,
|
||||||
|
refresh: refreshTrainList,
|
||||||
|
} = useAsyncData(
|
||||||
|
'digital-train-list',
|
||||||
|
() =>
|
||||||
|
useFetchWrapped<
|
||||||
|
PagedDataRequest & AuthedRequest,
|
||||||
|
BaseResponse<PagedData<DigitalHumanTrainItem>>
|
||||||
|
>('App.Digital_Train.GetList', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
to_user_id: loginState.user.id,
|
||||||
|
page: pagination.page,
|
||||||
|
perpage: pagination.pageSize,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
watch: [pagination],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const trainList = computed(() => trainListResp.value?.data.items || [])
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dh_name',
|
||||||
|
label: '数字人名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'organization',
|
||||||
|
label: '单位名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'user_id',
|
||||||
|
label: '用户ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'create_time',
|
||||||
|
label: '创建时间',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'video_url',
|
||||||
|
label: '数字人视频',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'auth_video_url',
|
||||||
|
label: '授权视频',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions',
|
||||||
|
label: '操作',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 录入数字人相关状态
|
||||||
|
const isProcessModalOpen = ref(false)
|
||||||
|
const currentTrainItem = ref<DigitalHumanTrainItem | null>(null)
|
||||||
|
|
||||||
|
const processFormState = reactive({
|
||||||
|
name: '',
|
||||||
|
model_id: undefined as number | undefined,
|
||||||
|
description: '',
|
||||||
|
type: 2, // 默认为XSH自有
|
||||||
|
})
|
||||||
|
|
||||||
|
const processFormSchema = object({
|
||||||
|
name: string().required('请输入名称'),
|
||||||
|
model_id: number().required('请输入数字人ID'),
|
||||||
|
description: string().required('请输入描述'),
|
||||||
|
type: number().required('请选择类型'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const sourceTypeList = [
|
||||||
|
{ label: 'xsh_wm', value: 1, color: 'blue' }, // 万木(腾讯)
|
||||||
|
{ label: 'xsh_zy', value: 2, color: 'green' }, // XSH 自有
|
||||||
|
{ label: 'xsh_fh', value: 3, color: 'purple' }, // 硅基(泛化数字人)
|
||||||
|
{ label: 'xsh_bb', value: 4, color: 'indigo' }, // 百度小冰
|
||||||
|
]
|
||||||
|
|
||||||
|
const avatarFile = ref<File | null>(null)
|
||||||
|
const isProcessing = ref(false)
|
||||||
|
|
||||||
|
// 处理训练素材:录入系统数字人并分配给用户
|
||||||
|
const handleProcessTrain = (item: DigitalHumanTrainItem) => {
|
||||||
|
currentTrainItem.value = item
|
||||||
|
|
||||||
|
// 预填充表单数据
|
||||||
|
processFormState.name = item.dh_name
|
||||||
|
processFormState.model_id = undefined
|
||||||
|
processFormState.description = `基于${item.organization}提交的训练素材创建`
|
||||||
|
processFormState.type = 2
|
||||||
|
|
||||||
|
isProcessModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件上传
|
||||||
|
const handleAvatarUpload = (files: FileList) => {
|
||||||
|
const file = files[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
toast.add({
|
||||||
|
title: '文件格式错误',
|
||||||
|
description: '请上传图片文件',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小 (10MB)
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
|
toast.add({
|
||||||
|
title: '文件过大',
|
||||||
|
description: '图片文件大小不能超过10MB',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交录入表单
|
||||||
|
const onProcessSubmit = async (event: FormSubmitEvent<typeof processFormState>) => {
|
||||||
|
if (!currentTrainItem.value) return
|
||||||
|
|
||||||
|
if (!avatarFile.value) {
|
||||||
|
toast.add({
|
||||||
|
title: '请上传数字人预览图',
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isProcessing.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
isProcessing.value = true
|
||||||
|
|
||||||
|
// 1. 上传预览图
|
||||||
|
const avatarUrl = await useFileGo(avatarFile.value, 'material')
|
||||||
|
|
||||||
|
// 2. 创建系统数字人
|
||||||
|
const createSystemResult = await useFetchWrapped<
|
||||||
|
{
|
||||||
|
name: string
|
||||||
|
model_id: number
|
||||||
|
type: number
|
||||||
|
description: string
|
||||||
|
avatar: string
|
||||||
|
} & AuthedRequest,
|
||||||
|
BaseResponse<{ digital_human_id: number }>
|
||||||
|
>('App.Digital_Human.Create', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
name: event.data.name,
|
||||||
|
model_id: event.data.model_id!,
|
||||||
|
type: event.data.type,
|
||||||
|
description: event.data.description,
|
||||||
|
avatar: avatarUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (createSystemResult.ret !== 200 || !createSystemResult.data.digital_human_id) {
|
||||||
|
throw new Error(createSystemResult.msg || '创建系统数字人失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 分配数字人给提交素材的用户
|
||||||
|
const createUserResult = await 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: currentTrainItem.value.user_id,
|
||||||
|
digital_human_array: [createSystemResult.data.digital_human_id],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (createUserResult.ret !== 200 || createUserResult.data.success === 0) {
|
||||||
|
throw new Error(createUserResult.msg || '分配用户数字人失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: '录入成功',
|
||||||
|
description: `数字人"${event.data.name}"已成功录入并分配给用户 ${currentTrainItem.value.user_id}${
|
||||||
|
createUserResult.data.failed ? `,失败 ${createUserResult.data.failed} 个` : ''
|
||||||
|
}`,
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-tabler-check',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除定制记录
|
||||||
|
await handleDeleteTrain(currentTrainItem.value)
|
||||||
|
|
||||||
|
// 重置表单和状态
|
||||||
|
processFormState.name = ''
|
||||||
|
processFormState.model_id = undefined
|
||||||
|
processFormState.description = ''
|
||||||
|
processFormState.type = 2
|
||||||
|
avatarFile.value = null
|
||||||
|
currentTrainItem.value = null
|
||||||
|
isProcessModalOpen.value = false
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
await refreshTrainList()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('录入数字人失败:', error)
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '录入失败,请重试'
|
||||||
|
toast.add({
|
||||||
|
title: '录入失败',
|
||||||
|
description: errorMessage,
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除定制记录
|
||||||
|
const handleDeleteTrain = async (item: DigitalHumanTrainItem) => {
|
||||||
|
try {
|
||||||
|
const result = await useFetchWrapped<
|
||||||
|
{ train_id: number } & AuthedRequest,
|
||||||
|
BaseResponse<{ code: 0 | 1 }>
|
||||||
|
>('App.Digital_Train.Delete', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
train_id: item.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.ret === 200 && result.data.code === 1) {
|
||||||
|
toast.add({
|
||||||
|
title: '删除成功',
|
||||||
|
description: '定制记录已删除',
|
||||||
|
color: 'green',
|
||||||
|
icon: 'i-tabler-check',
|
||||||
|
})
|
||||||
|
await refreshTrainList()
|
||||||
|
} else {
|
||||||
|
throw new Error(result.msg || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除定制记录失败:', error)
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '删除失败,请重试'
|
||||||
|
toast.add({
|
||||||
|
title: '删除失败',
|
||||||
|
description: errorMessage,
|
||||||
|
color: 'red',
|
||||||
|
icon: 'i-tabler-alert-triangle',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timestamp: number) => {
|
||||||
|
return new Date(timestamp * 1000).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览视频
|
||||||
|
const previewVideo = (videoUrl: string, title: string) => {
|
||||||
|
// 创建一个简单的视频预览弹窗
|
||||||
|
const videoModal = document.createElement('div')
|
||||||
|
videoModal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'
|
||||||
|
|
||||||
|
const videoContainer = document.createElement('div')
|
||||||
|
videoContainer.className = 'bg-white dark:bg-gray-800 rounded-lg p-4 max-w-4xl max-h-[80vh] overflow-auto'
|
||||||
|
|
||||||
|
const titleElement = document.createElement('h3')
|
||||||
|
titleElement.textContent = title
|
||||||
|
titleElement.className = 'text-lg font-semibold mb-4 text-gray-900 dark:text-white'
|
||||||
|
|
||||||
|
const video = document.createElement('video')
|
||||||
|
video.src = videoUrl
|
||||||
|
video.controls = true
|
||||||
|
video.className = 'w-full max-h-[60vh]'
|
||||||
|
|
||||||
|
const closeButton = document.createElement('button')
|
||||||
|
closeButton.textContent = '关闭'
|
||||||
|
closeButton.className = 'mt-4 px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600'
|
||||||
|
closeButton.onclick = () => {
|
||||||
|
document.body.removeChild(videoModal)
|
||||||
|
}
|
||||||
|
|
||||||
|
videoContainer.appendChild(titleElement)
|
||||||
|
videoContainer.appendChild(video)
|
||||||
|
videoContainer.appendChild(closeButton)
|
||||||
|
videoModal.appendChild(videoContainer)
|
||||||
|
|
||||||
|
videoModal.onclick = (e) => {
|
||||||
|
if (e.target === videoModal) {
|
||||||
|
document.body.removeChild(videoModal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(videoModal)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="p-4 pb-0">
|
||||||
|
<BubbleTitle
|
||||||
|
title="数字人定制管理"
|
||||||
|
subtitle="Digital Human Training Management"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<UButton
|
||||||
|
color="gray"
|
||||||
|
variant="soft"
|
||||||
|
icon="i-tabler-refresh"
|
||||||
|
label="刷新"
|
||||||
|
@click="refreshTrainList"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</BubbleTitle>
|
||||||
|
<GradientDivider />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<UAlert
|
||||||
|
v-if="loginState.user.auth_code === 2"
|
||||||
|
icon="i-tabler-user-shield"
|
||||||
|
title="管理员功能"
|
||||||
|
description="当前正在管理用户提交的数字人定制请求,仅管理员可见"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<UTable
|
||||||
|
:rows="trainList"
|
||||||
|
:columns="columns"
|
||||||
|
:loading="trainListStatus === 'pending'"
|
||||||
|
:progress="{ color: 'amber', animation: 'carousel' }"
|
||||||
|
class="border dark:border-neutral-800 rounded-md"
|
||||||
|
>
|
||||||
|
<template #create_time-data="{ row }">
|
||||||
|
<span class="text-sm">{{ formatTime(row.create_time) }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #video_url-data="{ row }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton
|
||||||
|
color="blue"
|
||||||
|
variant="soft"
|
||||||
|
size="xs"
|
||||||
|
icon="i-tabler-download"
|
||||||
|
:to="row.video_url"
|
||||||
|
target="_blank"
|
||||||
|
label="下载"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
color="green"
|
||||||
|
variant="soft"
|
||||||
|
size="xs"
|
||||||
|
icon="i-tabler-eye"
|
||||||
|
label="预览"
|
||||||
|
@click="previewVideo(row.video_url, '数字人视频')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #auth_video_url-data="{ row }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton
|
||||||
|
color="blue"
|
||||||
|
variant="soft"
|
||||||
|
size="xs"
|
||||||
|
icon="i-tabler-download"
|
||||||
|
:to="row.auth_video_url"
|
||||||
|
target="_blank"
|
||||||
|
label="下载"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
color="green"
|
||||||
|
variant="soft"
|
||||||
|
size="xs"
|
||||||
|
icon="i-tabler-eye"
|
||||||
|
label="预览"
|
||||||
|
@click="previewVideo(row.auth_video_url, '授权视频')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #actions-data="{ row }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton
|
||||||
|
color="amber"
|
||||||
|
variant="soft"
|
||||||
|
size="xs"
|
||||||
|
icon="i-tabler-user-cog"
|
||||||
|
label="录入"
|
||||||
|
@click="handleProcessTrain(row)"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
color="red"
|
||||||
|
variant="soft"
|
||||||
|
size="xs"
|
||||||
|
icon="i-tabler-trash"
|
||||||
|
label="删除"
|
||||||
|
@click="handleDeleteTrain(row)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTable>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<UPagination
|
||||||
|
v-model="pagination.page"
|
||||||
|
:max="9"
|
||||||
|
:page-count="pagination.pageSize"
|
||||||
|
:total="trainListResp?.data.total || 0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 录入数字人弹窗 -->
|
||||||
|
<USlideover v-model="isProcessModalOpen">
|
||||||
|
<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="i-tabler-x"
|
||||||
|
padded
|
||||||
|
size="sm"
|
||||||
|
square
|
||||||
|
variant="ghost"
|
||||||
|
@click="isProcessModalOpen = false"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold">录入数字人</h3>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
|
为"{{ currentTrainItem?.dh_name }}"创建系统数字人并分配给用户 {{ currentTrainItem?.user_id }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UForm
|
||||||
|
class="space-y-4"
|
||||||
|
:schema="processFormSchema"
|
||||||
|
:state="processFormState"
|
||||||
|
@submit="onProcessSubmit"
|
||||||
|
>
|
||||||
|
<UFormGroup
|
||||||
|
label="名称"
|
||||||
|
name="name"
|
||||||
|
>
|
||||||
|
<UInput v-model="processFormState.name" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup
|
||||||
|
label="数字人ID"
|
||||||
|
name="model_id"
|
||||||
|
description="请输入五位数字人ID"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="processFormState.model_id"
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入数字人ID"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup
|
||||||
|
label="描述"
|
||||||
|
name="description"
|
||||||
|
>
|
||||||
|
<UTextarea
|
||||||
|
v-model="processFormState.description"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup
|
||||||
|
label="供应商类型"
|
||||||
|
name="type"
|
||||||
|
>
|
||||||
|
<USelectMenu
|
||||||
|
v-model="processFormState.type"
|
||||||
|
value-attribute="value"
|
||||||
|
:options="sourceTypeList"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup
|
||||||
|
label="数字人预览图"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<UniFileDnD
|
||||||
|
accept="image/png,image/jpeg,image/jpg"
|
||||||
|
@change="handleAvatarUpload"
|
||||||
|
>
|
||||||
|
<template #default>
|
||||||
|
<div class="text-center">
|
||||||
|
<UIcon
|
||||||
|
name="i-heroicons-photo"
|
||||||
|
class="mx-auto h-12 w-12 text-gray-400"
|
||||||
|
/>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ avatarFile ? avatarFile.name : '点击或拖拽上传图片' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UniFileDnD>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<div class="flex justify-end gap-2 pt-4">
|
||||||
|
<UButton
|
||||||
|
type="button"
|
||||||
|
color="gray"
|
||||||
|
variant="soft"
|
||||||
|
@click="isProcessModalOpen = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="isProcessing"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
>
|
||||||
|
{{ isProcessing ? '录入中...' : '录入并分配' }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</UForm>
|
||||||
|
</UCard>
|
||||||
|
</USlideover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
101
pages/generation/admin/index.vue
Normal file
101
pages/generation/admin/index.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
const router = useRouter()
|
||||||
|
const loginState = useLoginState()
|
||||||
|
|
||||||
|
// 检查用户权限
|
||||||
|
if (loginState.user.auth_code !== 2) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: '无权访问管理页面',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: '管理中心',
|
||||||
|
})
|
||||||
|
|
||||||
|
const adminPages = [
|
||||||
|
{
|
||||||
|
title: '用户管理',
|
||||||
|
description: '管理系统用户、权限和服务配额',
|
||||||
|
icon: 'i-tabler-users',
|
||||||
|
path: '/generation/admin/users',
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数字人定制管理',
|
||||||
|
description: '管理用户提交的数字人定制请求',
|
||||||
|
icon: 'i-tabler-user-cog',
|
||||||
|
path: '/generation/admin/digital-human-train',
|
||||||
|
color: 'amber',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const navigateToPage = (path: string) => {
|
||||||
|
router.push(path)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="p-4 pb-0">
|
||||||
|
<BubbleTitle
|
||||||
|
title="管理中心"
|
||||||
|
subtitle="Administrator Panel"
|
||||||
|
/>
|
||||||
|
<GradientDivider />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="page in adminPages"
|
||||||
|
:key="page.path"
|
||||||
|
class="group cursor-pointer"
|
||||||
|
@click="navigateToPage(page.path)"
|
||||||
|
>
|
||||||
|
<UCard
|
||||||
|
class="hover:shadow-lg transition-all duration-200 group-hover:scale-105"
|
||||||
|
:ui="{
|
||||||
|
ring: 'ring-1 ring-gray-200 dark:ring-gray-700 group-hover:ring-gray-300 dark:group-hover:ring-gray-600',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center text-center p-6">
|
||||||
|
<div
|
||||||
|
class="w-16 h-16 rounded-full flex items-center justify-center mb-4 transition-colors"
|
||||||
|
:class="{
|
||||||
|
'bg-blue-100 dark:bg-blue-900/30': page.color === 'blue',
|
||||||
|
'bg-amber-100 dark:bg-amber-900/30': page.color === 'amber',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
:name="page.icon"
|
||||||
|
class="w-8 h-8"
|
||||||
|
:class="{
|
||||||
|
'text-blue-600 dark:text-blue-400': page.color === 'blue',
|
||||||
|
'text-amber-600 dark:text-amber-400': page.color === 'amber',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
{{ page.title }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||||
|
{{ page.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center text-sm text-gray-500 dark:text-gray-400 group-hover:text-gray-700 dark:group-hover:text-gray-300 transition-colors">
|
||||||
|
<span>进入管理</span>
|
||||||
|
<UIcon name="i-heroicons-arrow-right" class="ml-1 w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -226,7 +226,7 @@ const onDigitalHumansSelected = (digitalHumans: DigitalHumanItem[]) => {
|
|||||||
user_id: loginState.user.id!,
|
user_id: loginState.user.id!,
|
||||||
to_user_id: viewingUser.value?.id || 0,
|
to_user_id: viewingUser.value?.id || 0,
|
||||||
digital_human_array: digitalHumans.map(
|
digital_human_array: digitalHumans.map(
|
||||||
(row) => row.id || row.digital_human_id
|
(row) => row.id || row.digital_human_id || 0
|
||||||
),
|
),
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res.ret === 200) {
|
if (res.ret === 200) {
|
||||||
|
|||||||
@@ -61,6 +61,25 @@ const {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: avatarTrainList,
|
||||||
|
status: avatarTrainStatus,
|
||||||
|
refresh: refreshAvatarTrainList,
|
||||||
|
} = useAsyncData(
|
||||||
|
() =>
|
||||||
|
useFetchWrapped<
|
||||||
|
PagedDataRequest & AuthedRequest,
|
||||||
|
BaseResponse<PagedData<DigitalHumanTrainItem>>
|
||||||
|
>('App.Digital_Train.GetList', {
|
||||||
|
token: loginState.token!,
|
||||||
|
user_id: loginState.user.id,
|
||||||
|
to_user_id: loginState.user.id,
|
||||||
|
page: 1,
|
||||||
|
perpage: 20,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
|
||||||
const onSystemAvatarDelete = (row: DigitalHumanItem) => {
|
const onSystemAvatarDelete = (row: DigitalHumanItem) => {
|
||||||
useFetchWrapped<
|
useFetchWrapped<
|
||||||
{ digital_human_id: number } & AuthedRequest,
|
{ digital_human_id: number } & AuthedRequest,
|
||||||
@@ -125,13 +144,14 @@ const columns = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const sourceTypeList = [
|
const sourceTypeList = [
|
||||||
{ label: 'xsh_wm', value: 1, color: 'blue' }, // 万木(腾讯)
|
{ label: 'xsh_wm', value: 1, color: 'blue' }, // 万木(腾讯)
|
||||||
{ label: 'xsh_zy', value: 2, color: 'green' }, // XSH 自有
|
{ label: 'xsh_zy', value: 2, color: 'green' }, // XSH 自有
|
||||||
{ label: 'xsh_fh', value: 3, color: 'purple' }, // 硅基(泛化数字人)
|
{ label: 'xsh_fh', value: 3, color: 'purple' }, // 硅基(泛化数字人)
|
||||||
{ label: 'xsh_bb', value: 4, color: 'indigo' }, // 百度小冰
|
{ label: 'xsh_bb', value: 4, color: 'indigo' }, // 百度小冰
|
||||||
]
|
]
|
||||||
|
|
||||||
const isCreateSlideOpen = ref(false)
|
const isCreateSlideOpen = ref(false)
|
||||||
|
const isTrainCreatorOpen = ref(false)
|
||||||
|
|
||||||
const createAvatarState = reactive({
|
const createAvatarState = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -230,13 +250,6 @@ const onAvatarUpload = async (files: FileList) => {
|
|||||||
:label="showSystemAvatar ? '显示用户数字人' : '显示系统数字人'"
|
:label="showSystemAvatar ? '显示用户数字人' : '显示系统数字人'"
|
||||||
@click="showSystemAvatar = !showSystemAvatar"
|
@click="showSystemAvatar = !showSystemAvatar"
|
||||||
/>
|
/>
|
||||||
<UButton
|
|
||||||
v-if="loginState.user.auth_code === 2"
|
|
||||||
color="amber"
|
|
||||||
variant="soft"
|
|
||||||
label="创建数字人"
|
|
||||||
@click="isCreateSlideOpen = true"
|
|
||||||
/>
|
|
||||||
<UButton
|
<UButton
|
||||||
:icon="
|
:icon="
|
||||||
data_layout === 'grid'
|
data_layout === 'grid'
|
||||||
@@ -248,6 +261,29 @@ const onAvatarUpload = async (files: FileList) => {
|
|||||||
@click="data_layout = data_layout === 'grid' ? 'list' : 'grid'"
|
@click="data_layout = data_layout === 'grid' ? 'list' : 'grid'"
|
||||||
:label="data_layout === 'grid' ? '列表视图' : '宫格视图'"
|
:label="data_layout === 'grid' ? '列表视图' : '宫格视图'"
|
||||||
/>
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="loginState.user.auth_code === 2"
|
||||||
|
color="amber"
|
||||||
|
variant="soft"
|
||||||
|
icon="tabler:user-cog"
|
||||||
|
label="定制管理"
|
||||||
|
:to="'/generation/admin/digital-human-train'"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
color="blue"
|
||||||
|
variant="soft"
|
||||||
|
icon="tabler:user-plus"
|
||||||
|
label="定制数字人"
|
||||||
|
@click="isTrainCreatorOpen = true"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
v-if="loginState.user.auth_code === 2"
|
||||||
|
color="amber"
|
||||||
|
variant="soft"
|
||||||
|
icon="tabler:plus"
|
||||||
|
label="创建数字人"
|
||||||
|
@click="isCreateSlideOpen = true"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</BubbleTitle>
|
</BubbleTitle>
|
||||||
<GradientDivider />
|
<GradientDivider />
|
||||||
@@ -473,6 +509,9 @@ const onAvatarUpload = async (files: FileList) => {
|
|||||||
</UForm>
|
</UForm>
|
||||||
</UCard>
|
</UCard>
|
||||||
</USlideover>
|
</USlideover>
|
||||||
|
|
||||||
|
<!-- 数字人定制对话框 -->
|
||||||
|
<DigitalHumanTrainCreator v-model="isTrainCreatorOpen" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
22
typings/types.d.ts
vendored
22
typings/types.d.ts
vendored
@@ -87,6 +87,20 @@ interface DigitalHumanItem {
|
|||||||
digital_human_id?: number
|
digital_human_id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数字人定制训练记录
|
||||||
|
*/
|
||||||
|
interface DigitalHumanTrainItem {
|
||||||
|
id: number
|
||||||
|
user_id: number
|
||||||
|
dh_name: string
|
||||||
|
organization: string
|
||||||
|
video_url: string
|
||||||
|
auth_video_url: string
|
||||||
|
create_time: number
|
||||||
|
status?: number // 0: 待处理, 1: 已处理
|
||||||
|
}
|
||||||
|
|
||||||
interface GBVideoItem {
|
interface GBVideoItem {
|
||||||
id: number
|
id: number
|
||||||
user_id: number
|
user_id: number
|
||||||
@@ -233,6 +247,14 @@ namespace req {
|
|||||||
source_type?: number
|
source_type?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AvatarTrainCreate {
|
||||||
|
user_id: number
|
||||||
|
dh_name: string
|
||||||
|
organization: string
|
||||||
|
video_url: string
|
||||||
|
auth_video_url: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param title 任务标题筛选
|
* @param title 任务标题筛选
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user