feat: 绿幕视频创建和微课视频创建

This commit is contained in:
2024-08-17 17:56:35 +08:00
parent 24629f8720
commit e48a744f60
14 changed files with 797 additions and 41 deletions

View File

@@ -28,7 +28,7 @@ const props = defineProps({
</h1> </h1>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2.5">
<slot name="action"/> <slot name="action"/>
</div> </div>

View File

@@ -41,7 +41,6 @@ watchEffect(() => {
}) })
const onCreateCourseSubmit = async (event: FormSubmitEvent<CreateCourseSchema>) => { const onCreateCourseSubmit = async (event: FormSubmitEvent<CreateCourseSchema>) => {
console.log(event.data)
if (!selected_file.value) { if (!selected_file.value) {
toast.add({ toast.add({
title: '未选择文件', title: '未选择文件',
@@ -167,6 +166,11 @@ const onCreateCourseSubmit = async (event: FormSubmitEvent<CreateCourseSchema>)
</div> </div>
<UFormGroup label="PPT 文件" required> <UFormGroup label="PPT 文件" required>
<template #help>
<p class="text-xs text-neutral-400">
仅支持 .pptx 格式
</p>
</template>
<FileDnD <FileDnD
accept="application/vnd.openxmlformats-officedocument.presentationml.presentation" accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
@change="file => selected_file = file" @change="file => selected_file = file"

View File

@@ -0,0 +1,198 @@
<script lang="ts" setup>
import { type InferType, number, object, string } from 'yup'
import ModalDigitalHumanSelect from '~/components/ModalDigitalHumanSelect.vue'
import type { FormSubmitEvent } from '#ui/types'
import { useFetchWrapped } from '~/composables/useFetchWrapped'
const emit = defineEmits(['success'])
const slide = useSlideover()
const toast = useToast()
const loginState = useLoginState()
const creationForm = ref<HTMLFormElement>()
const creationPending = ref(false)
const isDigitalSelectorOpen = ref(false)
const createCourseSchema = object({
title: string().trim().min(4, '标题必须大于4个字符').max(20, '标题不能超过20个字符').required('请输入视频标题'),
content: string().trim().min(4, '内容必须大于4个字符').max(1000, '内容不能超过1000个字符').required('请输入驱动文本内容'),
digital_human_id: number().not([0], '请选择数字人'),
speed: number().default(1.0).min(0.5).max(1.5).required(),
})
type CreateCourseSchema = InferType<typeof createCourseSchema>
const createCourseState = reactive({
title: undefined,
content: undefined,
digital_human_id: 0,
speed: 1.0,
})
const selected_digital_human = ref<DigitalHumanItem | null>(null)
watchEffect(() => {
if (selected_digital_human.value) {
createCourseState.digital_human_id = selected_digital_human.value.model_id || selected_digital_human.value.id!
}
})
const onCreateCourseGreenSubmit = async (event: FormSubmitEvent<CreateCourseSchema>) => {
creationPending.value = true
useFetchWrapped<req.gen.GBVideoCreate & AuthedRequest, BaseResponse<resp.gen.GBVideoCreate>>('App.Digital_VideoTask.Create', {
token: loginState.token!,
user_id: loginState.user.id,
title: event.data.title,
content: event.data.content,
digital_human_id: event.data.digital_human_id,
speed: 2 - event.data.speed,
device_id: 'XSHAssistant Web',
}).then(res => {
if (!!res.data.task_id) {
toast.add({
title: '创建成功',
description: '视频已加入生成队列',
color: 'green',
icon: 'i-tabler-check',
})
emit('success')
slide.close()
} else {
toast.add({
title: '创建失败',
description: res.msg || '未知错误',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
}
creationPending.value = false
}).catch(e => {
creationPending.value = false
toast.add({
title: '创建失败',
description: e.message || '未知错误',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
})
}
</script>
<template>
<USlideover prevent-close>
<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>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
新建绿幕视频
</h3>
<UButton
class="-my-1"
color="gray"
icon="i-tabler-x"
variant="ghost"
@click="slide.close()"
/>
</div>
</template>
<UForm
ref="creationForm"
:schema="createCourseSchema"
:state="createCourseState"
class="space-y-4"
@submit="onCreateCourseGreenSubmit"
>
<div class="flex justify-between gap-2 *:flex-1">
<UFormGroup label="视频标题" name="title" required>
<UInput v-model="createCourseState.title" placeholder="请输入视频标题"/>
</UFormGroup>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<UFormGroup label="数字人" name="digital_human_id" required>
<div
:class="{'shadow-inner': !!selected_digital_human}"
class="flex items-center gap-2 bg-neutral-100 p-2 rounded-md cursor-pointer select-none transition-all"
@click="isDigitalSelectorOpen = true"
>
<div class="w-12 aspect-square border rounded-md flex justify-center items-center overflow-hidden">
<UIcon v-if="!selected_digital_human" class="text-2xl opacity-50" name="i-tabler-user-screen"/>
<NuxtImg v-else :src="selected_digital_human?.avatar"/>
</div>
<div class="flex flex-col text-neutral-400 text-sm font-medium">
<span :class="!!selected_digital_human ? 'text-neutral-600' : ''">{{
selected_digital_human?.name || '点击选择数字人'
}}</span>
<span v-if="selected_digital_human?.description" class="text-2xs">
{{ selected_digital_human?.description }}
</span>
</div>
</div>
</UFormGroup>
</div>
<UFormGroup label="驱动内容" name="content" required>
<!-- <template #help>-->
<!-- <p class="text-xs text-neutral-400">-->
<!-- 仅支持 .pptx 格式-->
<!-- </p>-->
<!-- </template>-->
<UTextarea v-model="createCourseState.content" :rows="6" autoresize placeholder="请输入驱动文本内容"/>
</UFormGroup>
<UAccordion :items="[{label: '高级选项'}]" color="gray" size="lg">
<template #item>
<div class="border rounded-lg space-y-4 p-4 pb-6">
<UFormGroup :label="`视频倍速:${createCourseState.speed}`" name="speed">
<URange
v-model="createCourseState.speed"
:max="1.5"
:min="0.5"
:step="0.1"
class="pt-4"
size="sm"
/>
</UFormGroup>
</div>
</template>
</UAccordion>
</UForm>
<template #footer>
<div class="flex justify-end space-x-4">
<UButton
color="gray"
label="取消"
size="lg"
variant="ghost"
@click="slide.close()"
/>
<UButton
:loading="creationPending"
color="primary"
label="提交"
size="lg"
variant="solid"
@click="creationForm?.submit()"
/>
</div>
</template>
</UCard>
<ModalDigitalHumanSelect
:is-open="isDigitalSelectorOpen"
@close="isDigitalSelectorOpen = false"
@select="digitalHumans => {
selected_digital_human = (digitalHumans as DigitalHumanItem)
}"
/>
</USlideover>
</template>
<style scoped>
</style>

View File

@@ -3,6 +3,7 @@ import type { PropType } from 'vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useDownload } from '~/composables/useDownload' import { useDownload } from '~/composables/useDownload'
import gsap from 'gsap' import gsap from 'gsap'
import SRTEditor from '~/components/aigc/generation/SRTEditor.vue'
const toast = useToast() const toast = useToast()
const { metaSymbol } = useShortcuts() const { metaSymbol } = useShortcuts()
@@ -284,7 +285,7 @@ const copyTaskId = (extraMessage?: string) => {
/> />
</UCard> </UCard>
</UModal> </UModal>
<LazyAigcCourseGenerateSRTEditor <SRTEditor
ref="srtEditor" ref="srtEditor"
:course="course" :course="course"
/> />

View File

@@ -0,0 +1,188 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
const props = defineProps({
video: {
type: Object as PropType<GBVideoItem>,
required: true,
},
})
const emit = defineEmits({
delete: (video: GBVideoItem) => video,
})
const dayjs = useDayjs()
const toast = useToast()
const isFullContentOpen = ref(false)
const downloadingState = reactive({
subtitle: 0,
video: 0,
})
const startDownload = (url: string, filename: string) => {
if (url.endsWith('.ass')) {
downloadingState.subtitle = 0
} else {
downloadingState.video = 0
}
const {
download,
progressEmitter,
} = useDownload(url, filename)
progressEmitter.on('progress', progress => {
if (url.endsWith('.ass')) {
downloadingState.subtitle = progress
} else {
downloadingState.video = progress
}
console.log(downloadingState)
})
progressEmitter.on('done', () => {
if (url.endsWith('.ass')) {
downloadingState.subtitle = 100
} else {
downloadingState.video = 100
}
toast.add({
title: '下载完成',
description: '资源下载已完成',
color: 'green',
icon: 'i-tabler-check',
})
})
progressEmitter.on('error', err => {
if (url.endsWith('.ass')) {
downloadingState.subtitle = 0
} else {
downloadingState.video = 0
}
toast.add({
title: '下载失败',
description: err.message || '下载失败,未知错误',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
})
download()
}
const onClick = () => {
console.log('click delete')
}
</script>
<template>
<div
class="w-full flex gap-2 rounded-xl border border-neutral-200 dark:border-neutral-700 hover:shadow transition overflow-hidden p-3"
>
<div class="flex-0 h-48 aspect-[10/16] flex flex-col items-center justify-center rounded-lg shadow overflow-hidden">
<div v-if="!video.video_cover" class="w-full h-full bg-primary flex flex-col justify-center items-center gap-2">
<UIcon class="animate-spin text-4xl text-white" name="tabler:loader"/>
<div class="flex flex-col items-center gap-0.5">
<span class="text-sm font-bold text-white/90">火速生成中</span>
<span class="text-xs font-medium text-white/50">{{ video.progress }}%</span>
</div>
</div>
<NuxtImg v-else :src="video.video_cover" class="brightness-90 object-cover"/>
</div>
<div class="flex-1 flex flex-col justify-between gap-2">
<div class="flex-1 rounded-lg bg-neutral-100 dark:bg-neutral-800 p-2 px-2.5">
<ul class="grid grid-cols-2 gap-1.5">
<li class="col-span-2">
<!-- <h2 class="text-2xs font-medium text-primary-500">标题</h2>-->
<p class="text-sm font-bold line-clamp-1">{{ video.title || '无标题' }}</p>
</li>
<li class="">
<h2 class="text-2xs font-medium text-primary-500">完成时间</h2>
<p class="text-xs line-clamp-1">{{ dayjs(video.complete_time * 1000).format('YYYY-MM-DD HH:mm:ss') }}</p>
</li>
<li class="">
<h2 class="text-2xs font-medium text-primary-500">生成耗时</h2>
<p class="text-xs line-clamp-1">{{ dayjs.duration(video.duration || 0).format('HH:mm:ss') }}</p>
</li>
<li class="col-span-2 cursor-pointer" @click="isFullContentOpen = true">
<h2 class="text-2xs font-medium text-primary-500">驱动文本</h2>
<p class="text-xs line-clamp-3 text-justify">{{ video.content }}</p>
</li>
</ul>
</div>
<div class="flex justify-end sm:justify-between items-center group flex-nowrap whitespace-nowrap">
<div
class="hidden sm:flex items-center gap-1 transition-all group-hover:opacity-0 group-hover:pointer-events-none">
<UIcon class="text-primary text-lg" name="i-tabler-user-square-rounded"/>
<p class="text-xs">数字人 {{ video.digital_human_id }}</p>
</div>
<div class="space-x-2">
<UButton
class="transition-all sm:opacity-0 sm:translate-x-4 sm:pointer-events-none group-hover:opacity-100 group-hover:translate-x-0 group-hover:pointer-events-auto"
color="red"
icon="i-tabler-trash"
size="xs"
variant="soft"
@click="emit('delete', video)"
/>
<UButtonGroup size="xs">
<UButton
:label="downloadingState.subtitle > 0 && downloadingState.subtitle < 100 ? `${downloadingState.subtitle.toFixed(0)}%` : '字幕'"
:loading="downloadingState.subtitle > 0 && downloadingState.subtitle < 100"
color="primary"
leading-icon="i-tabler-file-download"
variant="soft"
@click="startDownload(video.subtitle!, (video.title || video.task_id) + '.ass')"
/>
<UButton
:label="downloadingState.video > 0 && downloadingState.video < 100 ? `${downloadingState.video.toFixed(0)}%` : '视频'"
:loading="downloadingState.video > 0 && downloadingState.video < 100"
color="primary"
leading-icon="i-tabler-download"
variant="soft"
@click="startDownload(video.video_url!, (video.title || video.task_id) + '.mp4')"
/>
</UButtonGroup>
</div>
</div>
</div>
<!-- Full video content -->
<UModal v-model="isFullContentOpen">
<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-base font-semibold leading-6 text-gray-900 dark:text-white">
{{ video.title || '无标题' }}
<span class="block text-xs text-primary">驱动内容</span>
</h3>
<UButton
color="gray"
icon="i-tabler-x"
variant="ghost"
@click="isFullContentOpen = false"
/>
</div>
</template>
<div>
<article class="prose">
<p class="text-justify">{{ video.content }}</p>
</article>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<UButton color="primary" @click="isFullContentOpen = false">关闭</UButton>
</div>
</template>
</UCard>
</UModal>
</div>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,32 @@
export const useTourState = defineStore('tour_state', () => {
const tourState = ref<{ [key: string]: boolean }>({})
const isTourDone = (tourId: string) => tourState.value[tourId] || false
const setTourDone = (tourId: string) => {
tourState.value = {
...tourState.value,
[tourId]: true,
}
}
const autoDriveTour = (tourId: string, driver: ReturnType<typeof useDriver>) => {
if (isTourDone(tourId)) return
driver.setConfig({
...driver.getConfig(),
onDestroyed: () => setTourDone(tourId),
})
driver.drive()
}
return {
tourState,
isTourDone,
setTourDone,
autoDriveTour,
}
}, {
persist: {
key: 'xsh_assistant_tour_state',
storage: persistedState.localStorage,
paths: ['tourState'],
},
})

View File

@@ -17,6 +17,8 @@ export default defineNuxtConfig({
'@vite-pwa/nuxt', '@vite-pwa/nuxt',
'@nuxtjs/google-fonts', '@nuxtjs/google-fonts',
'@nuxt/image', '@nuxt/image',
'@vueuse/nuxt',
'nuxt-driver.js',
], ],
icon: { icon: {
@@ -30,7 +32,7 @@ export default defineNuxtConfig({
dayjs: { dayjs: {
locales: ['zh', 'en'], locales: ['zh', 'en'],
plugins: ['relativeTime', 'utc', 'timezone'], plugins: ['relativeTime', 'utc', 'timezone', 'duration'],
defaultLocale: 'zh', defaultLocale: 'zh',
defaultTimezone: 'Asia/Shanghai', defaultTimezone: 'Asia/Shanghai',
}, },

View File

@@ -25,6 +25,7 @@
"idb-keyval": "^6.2.1", "idb-keyval": "^6.2.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"nuxt": "^3.12.4", "nuxt": "^3.12.4",
"nuxt-driver.js": "^0.0.11",
"radix-vue": "^1.9.2", "radix-vue": "^1.9.2",
"vue": "^3.4.34", "vue": "^3.4.34",
"vue-router": "^4.4.0", "vue-router": "^4.4.0",
@@ -38,6 +39,8 @@
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"@types/markdown-it": "^13.0.9", "@types/markdown-it": "^13.0.9",
"@vite-pwa/nuxt": "^0.5.0", "@vite-pwa/nuxt": "^0.5.0",
"@vueuse/core": "^10.11.1",
"@vueuse/nuxt": "^10.11.1",
"dayjs-nuxt": "^2.1.9", "dayjs-nuxt": "^2.1.9",
"sass": "^1.77.8" "sass": "^1.77.8"
}, },

View File

@@ -73,4 +73,27 @@ onMounted(() => {
.subpage-leave-to { .subpage-leave-to {
@apply opacity-0 translate-x-4; @apply opacity-0 translate-x-4;
} }
.loading-screen-leave-active {
@apply transition-all duration-300;
}
.loading-screen-leave-to {
@apply opacity-0;
}
.card-move,
.card-enter-active,
.card-leave-active {
@apply transition-all duration-300;
}
.card-enter-from,
.card-leave-to {
@apply opacity-0;
}
.card-leave-active {
@apply absolute;
}
</style> </style>

View File

@@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import CGTaskCard from '~/components/aigc/course-generate/CGTaskCard.vue' import CGTaskCard from '~/components/aigc/generation/CGTaskCard.vue'
import { useFetchWrapped } from '~/composables/useFetchWrapped'
import ModalAuthentication from '~/components/ModalAuthentication.vue' import ModalAuthentication from '~/components/ModalAuthentication.vue'
import SlideCreateCourse from '~/components/SlideCreateCourse.vue' import SlideCreateCourse from '~/components/SlideCreateCourse.vue'
import { useFetchWrapped } from '~/composables/useFetchWrapped'
const toast = useToast() const toast = useToast()
const modal = useModal() const modal = useModal()
@@ -90,7 +90,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="font-sans h-full"> <div>
<div class="p-4 pb-0"> <div class="p-4 pb-0">
<BubbleTitle subtitle="VIDEOS" title="我的微课视频"> <BubbleTitle subtitle="VIDEOS" title="我的微课视频">
<template #action> <template #action>
@@ -114,8 +114,10 @@ onMounted(() => {
<GradientDivider/> <GradientDivider/>
</div> </div>
<Transition name="loading-screen"> <Transition name="loading-screen">
<div v-if="courseList?.data.items.length === 0" <div
class="w-full h-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900"> v-if="courseList?.data.items.length === 0"
class="w-full py-20 flex flex-col justify-center items-center gap-2"
>
<Icon class="text-7xl text-neutral-300 dark:text-neutral-700" name="i-tabler-photo-hexagon"/> <Icon class="text-7xl text-neutral-300 dark:text-neutral-700" name="i-tabler-photo-hexagon"/>
<p class="text-sm text-neutral-500 dark:text-neutral-400"> <p class="text-sm text-neutral-500 dark:text-neutral-400">
没有记录 没有记录
@@ -137,7 +139,7 @@ onMounted(() => {
</TransitionGroup> </TransitionGroup>
</div> </div>
<div class="flex justify-end mt-4"> <div class="flex justify-end mt-4">
<UPagination v-model="page" :page-count="16" :total="courseList?.data.total || 0"/> <UPagination v-model="page" :max="9" :page-count="16" :total="courseList?.data.total || 0"/>
</div> </div>
</div> </div>
</Transition> </Transition>
@@ -145,26 +147,5 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
.loading-screen-leave-active {
@apply transition-all duration-300;
}
.loading-screen-leave-to {
@apply opacity-0;
}
.card-move,
.card-enter-active,
.card-leave-active {
@apply transition-all duration-300;
}
.card-enter-from,
.card-leave-to {
@apply opacity-0;
}
.card-leave-active {
@apply absolute;
}
</style> </style>

View File

@@ -1,10 +1,204 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFetchWrapped } from '~/composables/useFetchWrapped'
import GBTaskCard from '~/components/aigc/generation/GBTaskCard.vue'
import { useTourState } from '~/composables/useTourState'
import SlideCreateCourseGreen from '~/components/SlideCreateCourseGreen.vue'
const route = useRoute()
const slide = useSlideover()
const toast = useToast()
const loginState = useLoginState()
const tourState = useTourState()
const page = ref(1)
const pageCount = ref(15)
const searchInput = ref('')
const debounceSearch = refDebounced(searchInput, 1000)
watch(debounceSearch, () => page.value = 1)
const {
data: videoList,
refresh: refreshVideoList,
} = useAsyncData(
() => useFetchWrapped<
req.gen.GBVideoList & AuthedRequest,
BaseResponse<PagedData<GBVideoItem>>
>('App.Digital_VideoTask.GetList', {
token: loginState.token!,
user_id: loginState.user.id,
to_user_id: loginState.user.id,
page: page.value,
perpage: pageCount.value,
title: debounceSearch.value,
}), {
watch: [page, pageCount, debounceSearch],
},
)
const onCreateCourseGreenClick = () => {
slide.open(SlideCreateCourseGreen, {
onSuccess: () => {
refreshVideoList()
},
})
}
const onCourseGreenDelete = (task: GBVideoItem) => {
if (!task.task_id) return
useFetchWrapped<
req.gen.GBVideoDelete & AuthedRequest,
BaseResponse<resp.gen.GBVideoDelete>
>('App.Digital_VideoTask.Delete', {
token: loginState.token!,
user_id: loginState.user.id,
task_id: task.task_id,
}).then(res => {
if (res.data.code === 1) {
refreshVideoList()
toast.add({
title: '删除成功',
description: '已删除任务记录',
color: 'green',
icon: 'i-tabler-check',
})
} else {
toast.add({
title: '删除失败',
description: res.msg || '未知错误',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
}
})
}
const beforeLeave = (el: any) => {
el.style.width = `${ el.offsetWidth }px`
el.style.height = `${ el.offsetHeight }px`
}
const leave = (el: any, done: Function) => {
el.style.position = 'absolute'
el.style.transition = 'none' // 取消过渡动画
el.style.opacity = 0 // 立即隐藏元素
done()
}
onMounted(() => {
const i = setInterval(refreshVideoList, 1000 * 5)
onBeforeUnmount(() => clearInterval(i))
const driver = useDriver({
showProgress: true,
animate: true,
smoothScroll: true,
disableActiveInteraction: true,
popoverOffset: 12,
progressText: '{{current}} / {{total}}',
prevBtnText: '上一步',
nextBtnText: '下一步',
doneBtnText: '完成',
steps: [
{
element: '#button-create',
popover: {
title: '新建视频',
description: '点击这里开始新建绿幕视频',
},
},
{
element: '#input-search',
popover: {
title: '搜索生成记录',
description: '在这里输入视频标题,可以搜索符合条件的生成记录',
},
},
],
})
tourState.autoDriveTour(route.fullPath, driver)
})
</script> </script>
<template> <template>
<div> <div class="h-full">
绿幕视频生成 <div class="p-4 pb-0">
<BubbleTitle
:subtitle="!debounceSearch ? 'GB VIDEOS' : 'SEARCH...'"
:title="!debounceSearch ? '我的绿幕视频' : `标题搜索:${debounceSearch.toLocaleUpperCase()}`"
>
<template #action>
<UButtonGroup size="md">
<UInput
id="input-search"
v-model="searchInput"
:autofocus="false"
:ui="{ icon: { trailing: { pointer: '' } } }"
autocomplete="off"
placeholder="标题搜索"
variant="outline"
>
<template #trailing>
<UButton
v-show="searchInput !== ''"
:padded="false"
color="gray"
icon="i-tabler-x"
variant="link"
@click="searchInput = ''"
/>
</template>
</UInput>
</UButtonGroup>
<UButton
id="button-create"
:trailing="false"
color="primary"
icon="i-tabler-plus"
label="新建"
size="md"
variant="solid"
@click="onCreateCourseGreenClick"
/>
</template>
</BubbleTitle>
<GradientDivider/>
</div>
<Transition name="loading-screen">
<div
v-if="videoList?.data.items.length === 0"
class="w-full py-20 flex flex-col justify-center items-center gap-2"
>
<Icon class="text-7xl text-neutral-300 dark:text-neutral-700" name="i-tabler-photo-hexagon"/>
<p class="text-sm text-neutral-500 dark:text-neutral-400">
没有记录
</p>
</div>
<div v-else>
<div class="p-4">
<div class="relative grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3 fhd:grid-cols-5 gap-4">
<TransitionGroup
name="card"
@beforeLeave="beforeLeave"
@leave="leave"
>
<GBTaskCard
v-for="(v, i) in videoList?.data.items"
:key="v.task_id"
:video="v"
@delete="v => onCourseGreenDelete(v)"
/>
</TransitionGroup>
</div>
<div class="flex justify-end mt-4">
<UPagination v-model="page" :max="9" :page-count="pageCount" :total="videoList?.data.total || 0"/>
</div>
</div>
</div>
</Transition>
</div> </div>
</template> </template>

78
pnpm-lock.yaml generated
View File

@@ -53,6 +53,9 @@ importers:
nuxt: nuxt:
specifier: ^3.12.4 specifier: ^3.12.4
version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@20.14.12)(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)(sass@1.77.8)(terser@5.31.3)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)) version: 3.12.4(@parcel/watcher@2.4.1)(@types/node@20.14.12)(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)(sass@1.77.8)(terser@5.31.3)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))
nuxt-driver.js:
specifier: ^0.0.11
version: 0.0.11(magicast@0.3.4)(rollup@4.19.1)
radix-vue: radix-vue:
specifier: ^1.9.2 specifier: ^1.9.2
version: 1.9.2(vue@3.4.34) version: 1.9.2(vue@3.4.34)
@@ -90,6 +93,12 @@ importers:
'@vite-pwa/nuxt': '@vite-pwa/nuxt':
specifier: ^0.5.0 specifier: ^0.5.0
version: 0.5.0(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(workbox-build@7.1.1)(workbox-window@7.1.0) version: 0.5.0(magicast@0.3.4)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))(workbox-build@7.1.1)(workbox-window@7.1.0)
'@vueuse/core':
specifier: ^10.11.1
version: 10.11.1(vue@3.4.34)
'@vueuse/nuxt':
specifier: ^10.11.1
version: 10.11.1(magicast@0.3.4)(nuxt@3.12.4(@parcel/watcher@2.4.1)(@types/node@20.14.12)(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)(sass@1.77.8)(terser@5.31.3)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)))(rollup@4.19.1)(vue@3.4.34)
dayjs-nuxt: dayjs-nuxt:
specifier: ^2.1.9 specifier: ^2.1.9
version: 2.1.9(magicast@0.3.4)(rollup@4.19.1) version: 2.1.9(magicast@0.3.4)(rollup@4.19.1)
@@ -1835,6 +1844,9 @@ packages:
'@vueuse/core@10.11.0': '@vueuse/core@10.11.0':
resolution: {integrity: sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==} resolution: {integrity: sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==}
'@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/integrations@10.11.0': '@vueuse/integrations@10.11.0':
resolution: {integrity: sha512-Pp6MtWEIr+NDOccWd8j59Kpjy5YDXogXI61Kb1JxvSfVBO8NzFQkmrKmSZz47i+ZqHnIzxaT38L358yDHTncZg==} resolution: {integrity: sha512-Pp6MtWEIr+NDOccWd8j59Kpjy5YDXogXI61Kb1JxvSfVBO8NzFQkmrKmSZz47i+ZqHnIzxaT38L358yDHTncZg==}
peerDependencies: peerDependencies:
@@ -1882,9 +1894,20 @@ packages:
'@vueuse/metadata@10.11.0': '@vueuse/metadata@10.11.0':
resolution: {integrity: sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==} resolution: {integrity: sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==}
'@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/nuxt@10.11.1':
resolution: {integrity: sha512-UiaYSIwOkmUVn8Gl1AqtLWYR12flO+8sEu9X0Y1fNjSR7EWy9jMuiCvOGqwtoeTsqfHrivl0d5HfMzr11GFnMA==}
peerDependencies:
nuxt: ^3.0.0
'@vueuse/shared@10.11.0': '@vueuse/shared@10.11.0':
resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==} resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
abbrev@1.1.1: abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
@@ -2547,6 +2570,9 @@ packages:
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'} engines: {node: '>=12'}
driver.js@1.3.1:
resolution: {integrity: sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ==}
duplexer@0.1.2: duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
@@ -3602,6 +3628,9 @@ packages:
engines: {node: ^16.10.0 || >=18.0.0} engines: {node: ^16.10.0 || >=18.0.0}
hasBin: true hasBin: true
nuxt-driver.js@0.0.11:
resolution: {integrity: sha512-WzcNjEk7VGyMrt6vKAQGXiaf/sHp7cfGYtZ1O6fxow3DYWJ+a2NHsLE9ejumSSH753JKzmLNpJRrX0YYFCnl9g==}
nuxt@3.12.4: nuxt@3.12.4:
resolution: {integrity: sha512-/ddvyc2kgYYIN2UEjP8QIz48O/W3L0lZm7wChIDbOCj0vF/yLLeZHBaTb3aNvS9Hwp269nfjrm8j/mVxQK4RhA==} resolution: {integrity: sha512-/ddvyc2kgYYIN2UEjP8QIz48O/W3L0lZm7wChIDbOCj0vF/yLLeZHBaTb3aNvS9Hwp269nfjrm8j/mVxQK4RhA==}
engines: {node: ^14.18.0 || >=16.10.0} engines: {node: ^14.18.0 || >=16.10.0}
@@ -6493,7 +6522,7 @@ snapshots:
'@tailwindcss/container-queries': 0.1.1(tailwindcss@3.4.7) '@tailwindcss/container-queries': 0.1.1(tailwindcss@3.4.7)
'@tailwindcss/forms': 0.5.7(tailwindcss@3.4.7) '@tailwindcss/forms': 0.5.7(tailwindcss@3.4.7)
'@tailwindcss/typography': 0.5.13(tailwindcss@3.4.7) '@tailwindcss/typography': 0.5.13(tailwindcss@3.4.7)
'@vueuse/core': 10.11.0(vue@3.4.34) '@vueuse/core': 10.11.1(vue@3.4.34)
'@vueuse/integrations': 10.11.0(fuse.js@6.6.2)(idb-keyval@6.2.1)(vue@3.4.34) '@vueuse/integrations': 10.11.0(fuse.js@6.6.2)(idb-keyval@6.2.1)(vue@3.4.34)
'@vueuse/math': 10.11.0(vue@3.4.34) '@vueuse/math': 10.11.0(vue@3.4.34)
defu: 6.1.4 defu: 6.1.4
@@ -7165,6 +7194,16 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@vueuse/core@10.11.1(vue@3.4.34)':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 10.11.1
'@vueuse/shared': 10.11.1(vue@3.4.34)
vue-demi: 0.14.10(vue@3.4.34)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/integrations@10.11.0(fuse.js@6.6.2)(idb-keyval@6.2.1)(vue@3.4.34)': '@vueuse/integrations@10.11.0(fuse.js@6.6.2)(idb-keyval@6.2.1)(vue@3.4.34)':
dependencies: dependencies:
'@vueuse/core': 10.11.0(vue@3.4.34) '@vueuse/core': 10.11.0(vue@3.4.34)
@@ -7187,6 +7226,23 @@ snapshots:
'@vueuse/metadata@10.11.0': {} '@vueuse/metadata@10.11.0': {}
'@vueuse/metadata@10.11.1': {}
'@vueuse/nuxt@10.11.1(magicast@0.3.4)(nuxt@3.12.4(@parcel/watcher@2.4.1)(@types/node@20.14.12)(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)(sass@1.77.8)(terser@5.31.3)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)))(rollup@4.19.1)(vue@3.4.34)':
dependencies:
'@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.1)
'@vueuse/core': 10.11.1(vue@3.4.34)
'@vueuse/metadata': 10.11.1
local-pkg: 0.5.0
nuxt: 3.12.4(@parcel/watcher@2.4.1)(@types/node@20.14.12)(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)(sass@1.77.8)(terser@5.31.3)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3))
vue-demi: 0.14.10(vue@3.4.34)
transitivePeerDependencies:
- '@vue/composition-api'
- magicast
- rollup
- supports-color
- vue
'@vueuse/shared@10.11.0(vue@3.4.34)': '@vueuse/shared@10.11.0(vue@3.4.34)':
dependencies: dependencies:
vue-demi: 0.14.10(vue@3.4.34) vue-demi: 0.14.10(vue@3.4.34)
@@ -7194,6 +7250,13 @@ snapshots:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
'@vueuse/shared@10.11.1(vue@3.4.34)':
dependencies:
vue-demi: 0.14.10(vue@3.4.34)
transitivePeerDependencies:
- '@vue/composition-api'
- vue
abbrev@1.1.1: {} abbrev@1.1.1: {}
abort-controller@3.0.0: abort-controller@3.0.0:
@@ -7866,6 +7929,8 @@ snapshots:
dotenv@16.4.5: {} dotenv@16.4.5: {}
driver.js@1.3.1: {}
duplexer@0.1.2: {} duplexer@0.1.2: {}
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
@@ -9126,6 +9191,15 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
nuxt-driver.js@0.0.11(magicast@0.3.4)(rollup@4.19.1):
dependencies:
'@nuxt/kit': 3.12.4(magicast@0.3.4)(rollup@4.19.1)
driver.js: 1.3.1
transitivePeerDependencies:
- magicast
- rollup
- supports-color
nuxt@3.12.4(@parcel/watcher@2.4.1)(@types/node@20.14.12)(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)(sass@1.77.8)(terser@5.31.3)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)): nuxt@3.12.4(@parcel/watcher@2.4.1)(@types/node@20.14.12)(idb-keyval@6.2.1)(ioredis@5.4.1)(magicast@0.3.4)(rollup@4.19.1)(sass@1.77.8)(terser@5.31.3)(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)):
dependencies: dependencies:
'@nuxt/devalue': 2.0.2 '@nuxt/devalue': 2.0.2
@@ -9645,7 +9719,7 @@ snapshots:
'@internationalized/date': 3.5.5 '@internationalized/date': 3.5.5
'@internationalized/number': 3.5.3 '@internationalized/number': 3.5.3
'@tanstack/vue-virtual': 3.8.3(vue@3.4.34) '@tanstack/vue-virtual': 3.8.3(vue@3.4.34)
'@vueuse/core': 10.11.0(vue@3.4.34) '@vueuse/core': 10.11.1(vue@3.4.34)
'@vueuse/shared': 10.11.0(vue@3.4.34) '@vueuse/shared': 10.11.0(vue@3.4.34)
aria-hidden: 1.2.4 aria-hidden: 1.2.4
defu: 6.1.4 defu: 6.1.4

56
typings/types.d.ts vendored
View File

@@ -40,6 +40,24 @@ interface DigitalHumanItem {
digital_human_id: number digital_human_id: number
} }
interface GBVideoItem {
id: number
user_id: number
task_id: string
create_time: number
complete_time: number
progress: number
duration?: number
digital_human_id: number
title: string
content: string
bg_img: string
video_url?: string
video_cover?: string
subtitle?: string
speed: number
}
// Common request and response schemas // Common request and response schemas
namespace req { namespace req {
namespace user { namespace user {
@@ -115,6 +133,29 @@ namespace req {
page?: number page?: number
perpage?: number perpage?: number
} }
/**
* @param title 任务标题筛选
*/
interface GBVideoList {
to_user_id: number
page?: number
perpage?: number
title?: string
}
interface GBVideoCreate {
device_id: string
digital_human_id: number
title: string
content: string
bg_img?: string
speed?: number
}
interface GBVideoDelete {
task_id: string
}
} }
interface AssistantTemplateList { interface AssistantTemplateList {
@@ -191,6 +232,21 @@ namespace resp {
video_sub_id: number video_sub_id: number
url: string url: string
} }
/**
* @param request_id 任务 ID同 task_id
*/
interface GBVideoCreate {
data_id: string
task_id: string
}
/**
* @param code 1: 删除成功, 0: 删除失败
*/
interface GBVideoDelete {
code: 0 | 1
}
} }
} }