Files
xsh-assistant-next/app/components/DigitalHumanTrainCreator.vue

561 lines
17 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
// 验证文件大小 (1GB)
if (file.size > 1024 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '视频文件大小不能超过1GB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
videoFile.value = file
toast.add({
title: '文件上传成功',
description: '数字人视频已选择',
color: 'success',
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: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
// 验证文件大小
if (file.size > 1024 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '视频文件大小不能超过1GB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
authVideoFile.value = file
toast.add({
title: '文件上传成功',
description: '授权视频已选择',
color: 'success',
icon: 'i-tabler-check',
})
}
// 提交表单
const onSubmit = async (event: FormSubmitEvent<typeof formState>) => {
// 验证文件是否已上传
if (!videoFile.value) {
toast.add({
title: '请上传数字人视频素材',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
if (!authVideoFile.value) {
toast.add({
title: '请上传形象授权视频',
color: 'error',
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: 'success',
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: 'error',
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:open="isOpen"
:ui="{ content: 'sm:max-w-6xl' }"
>
<template #content>
<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="neutral"
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 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
<UForm
:schema="schema"
:state="formState"
class="space-y-4"
@submit="onSubmit"
>
<!-- 数字人视频素材 -->
<UFormField
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="mt-1 text-xs text-gray-500">
小于 1GB mov/mp4 格式比例 9:16帧率 25FPS分辨率
1080P时长 3-6 分钟
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
<!-- 数字人名称 -->
<UFormField
label="数字人名称"
name="dh_name"
required
>
<UInput
v-model="formState.dh_name"
placeholder="请输入数字人名称"
/>
</UFormField>
<!-- 单位名称 -->
<UFormField
label="单位名称"
name="organization"
required
>
<UInput
v-model="formState.organization"
placeholder="请输入单位名称"
/>
</UFormField>
<!-- 形象授权视频 -->
<UFormField
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>
</UFormField>
<!-- 提交按钮 -->
<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 rounded-lg border p-4 dark:border-gray-700">
<div class="flex h-full flex-col gap-6">
<!-- 教程视频 -->
<div class="flex-1">
<h3
class="mb-3 flex items-center gap-2 text-lg font-semibold text-gray-800 dark:text-white"
>
<UIcon
name="i-heroicons-video-camera"
class="h-5 w-5"
/>
视频录制教程
</h3>
<div
class="flex aspect-video w-full items-center justify-center rounded-lg border bg-gray-100 dark:bg-gray-800"
>
<UIcon
name="i-heroicons-video-camera"
class="h-12 w-12 text-gray-400"
/>
</div>
</div>
<!-- 联系方式 -->
<div
class="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-700 dark:bg-blue-900/20"
>
<div class="flex items-center gap-3">
<div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900">
<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="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-700 dark:bg-amber-900/20"
>
<div class="flex items-start gap-3">
<div
class="mt-0.5 rounded-lg bg-amber-100 p-2 dark:bg-amber-900"
>
<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="mb-3 text-sm font-semibold text-gray-800 dark:text-white"
>
录制注意事项
</h4>
<div class="space-y-2">
<div class="flex items-center gap-2">
<UIcon
name="i-heroicons-sun"
class="mt-0.5 h-4 w-4 shrink-0 text-amber-500"
/>
<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="mt-0.5 h-4 w-4 shrink-0 text-green-500"
/>
<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="mt-0.5 h-4 w-4 shrink-0 text-blue-500"
/>
<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="mt-0.5 h-4 w-4 shrink-0 text-purple-500"
/>
<span class="text-xs text-gray-600 dark:text-gray-300">
保持自然表情使用恰当手势
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</UCard>
<!-- 授权文案弹窗 -->
<UModal v-model:open="showAuthModal">
<template #content>
<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="neutral"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
@click="showAuthModal = false"
/>
</div>
</template>
<div class="p-4">
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
请确保您是视频中人物的合法授权人在授权视频中朗读以下文案
</p>
<div class="rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
<p
class="text-sm leading-relaxed text-gray-800 dark:text-gray-200"
>
我是在"AI智慧职教平台"定制上传视频的模特本人我承诺已经按照平台规则进行合法授权特此承诺
</p>
</div>
</div>
</UCard>
</template>
</UModal>
</template>
</UModal>
</template>
<style scoped></style>