476 lines
15 KiB
Vue
476 lines
15 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: '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> |