561 lines
17 KiB
Vue
561 lines
17 KiB
Vue
<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>
|