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>
|
||||
Reference in New Issue
Block a user