feat: 绿幕视频创建和微课视频创建
This commit is contained in:
@@ -28,7 +28,7 @@ const props = defineProps({
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex gap-2.5">
|
||||
<slot name="action"/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
const onCreateCourseSubmit = async (event: FormSubmitEvent<CreateCourseSchema>) => {
|
||||
console.log(event.data)
|
||||
if (!selected_file.value) {
|
||||
toast.add({
|
||||
title: '未选择文件',
|
||||
@@ -167,6 +166,11 @@ const onCreateCourseSubmit = async (event: FormSubmitEvent<CreateCourseSchema>)
|
||||
</div>
|
||||
|
||||
<UFormGroup label="PPT 文件" required>
|
||||
<template #help>
|
||||
<p class="text-xs text-neutral-400">
|
||||
仅支持 .pptx 格式
|
||||
</p>
|
||||
</template>
|
||||
<FileDnD
|
||||
accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
@change="file => selected_file = file"
|
||||
|
||||
198
components/SlideCreateCourseGreen.vue
Normal file
198
components/SlideCreateCourseGreen.vue
Normal 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>
|
||||
@@ -3,6 +3,7 @@ import type { PropType } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { useDownload } from '~/composables/useDownload'
|
||||
import gsap from 'gsap'
|
||||
import SRTEditor from '~/components/aigc/generation/SRTEditor.vue'
|
||||
|
||||
const toast = useToast()
|
||||
const { metaSymbol } = useShortcuts()
|
||||
@@ -284,7 +285,7 @@ const copyTaskId = (extraMessage?: string) => {
|
||||
/>
|
||||
</UCard>
|
||||
</UModal>
|
||||
<LazyAigcCourseGenerateSRTEditor
|
||||
<SRTEditor
|
||||
ref="srtEditor"
|
||||
:course="course"
|
||||
/>
|
||||
188
components/aigc/generation/GBTaskCard.vue
Normal file
188
components/aigc/generation/GBTaskCard.vue
Normal 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>
|
||||
32
composables/useTourState.ts
Normal file
32
composables/useTourState.ts
Normal 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'],
|
||||
},
|
||||
})
|
||||
@@ -17,6 +17,8 @@ export default defineNuxtConfig({
|
||||
'@vite-pwa/nuxt',
|
||||
'@nuxtjs/google-fonts',
|
||||
'@nuxt/image',
|
||||
'@vueuse/nuxt',
|
||||
'nuxt-driver.js',
|
||||
],
|
||||
|
||||
icon: {
|
||||
@@ -30,7 +32,7 @@ export default defineNuxtConfig({
|
||||
|
||||
dayjs: {
|
||||
locales: ['zh', 'en'],
|
||||
plugins: ['relativeTime', 'utc', 'timezone'],
|
||||
plugins: ['relativeTime', 'utc', 'timezone', 'duration'],
|
||||
defaultLocale: 'zh',
|
||||
defaultTimezone: 'Asia/Shanghai',
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"idb-keyval": "^6.2.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"nuxt": "^3.12.4",
|
||||
"nuxt-driver.js": "^0.0.11",
|
||||
"radix-vue": "^1.9.2",
|
||||
"vue": "^3.4.34",
|
||||
"vue-router": "^4.4.0",
|
||||
@@ -38,6 +39,8 @@
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/markdown-it": "^13.0.9",
|
||||
"@vite-pwa/nuxt": "^0.5.0",
|
||||
"@vueuse/core": "^10.11.1",
|
||||
"@vueuse/nuxt": "^10.11.1",
|
||||
"dayjs-nuxt": "^2.1.9",
|
||||
"sass": "^1.77.8"
|
||||
},
|
||||
|
||||
@@ -73,4 +73,27 @@ onMounted(() => {
|
||||
.subpage-leave-to {
|
||||
@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>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import CGTaskCard from '~/components/aigc/course-generate/CGTaskCard.vue'
|
||||
import { useFetchWrapped } from '~/composables/useFetchWrapped'
|
||||
import CGTaskCard from '~/components/aigc/generation/CGTaskCard.vue'
|
||||
import ModalAuthentication from '~/components/ModalAuthentication.vue'
|
||||
import SlideCreateCourse from '~/components/SlideCreateCourse.vue'
|
||||
import { useFetchWrapped } from '~/composables/useFetchWrapped'
|
||||
|
||||
const toast = useToast()
|
||||
const modal = useModal()
|
||||
@@ -90,7 +90,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="font-sans h-full">
|
||||
<div>
|
||||
<div class="p-4 pb-0">
|
||||
<BubbleTitle subtitle="VIDEOS" title="我的微课视频">
|
||||
<template #action>
|
||||
@@ -114,8 +114,10 @@ onMounted(() => {
|
||||
<GradientDivider/>
|
||||
</div>
|
||||
<Transition name="loading-screen">
|
||||
<div v-if="courseList?.data.items.length === 0"
|
||||
class="w-full h-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
||||
<div
|
||||
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"/>
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400">
|
||||
没有记录
|
||||
@@ -137,7 +139,7 @@ onMounted(() => {
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<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>
|
||||
</Transition>
|
||||
@@ -145,26 +147,5 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<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>
|
||||
@@ -1,10 +1,204 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
||||
78
pnpm-lock.yaml
generated
78
pnpm-lock.yaml
generated
@@ -53,6 +53,9 @@ importers:
|
||||
nuxt:
|
||||
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))
|
||||
nuxt-driver.js:
|
||||
specifier: ^0.0.11
|
||||
version: 0.0.11(magicast@0.3.4)(rollup@4.19.1)
|
||||
radix-vue:
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.2(vue@3.4.34)
|
||||
@@ -90,6 +93,12 @@ importers:
|
||||
'@vite-pwa/nuxt':
|
||||
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)
|
||||
'@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:
|
||||
specifier: ^2.1.9
|
||||
version: 2.1.9(magicast@0.3.4)(rollup@4.19.1)
|
||||
@@ -1835,6 +1844,9 @@ packages:
|
||||
'@vueuse/core@10.11.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-Pp6MtWEIr+NDOccWd8j59Kpjy5YDXogXI61Kb1JxvSfVBO8NzFQkmrKmSZz47i+ZqHnIzxaT38L358yDHTncZg==}
|
||||
peerDependencies:
|
||||
@@ -1882,9 +1894,20 @@ packages:
|
||||
'@vueuse/metadata@10.11.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==}
|
||||
|
||||
'@vueuse/shared@10.11.1':
|
||||
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
|
||||
|
||||
abbrev@1.1.1:
|
||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||
|
||||
@@ -2547,6 +2570,9 @@ packages:
|
||||
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
driver.js@1.3.1:
|
||||
resolution: {integrity: sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ==}
|
||||
|
||||
duplexer@0.1.2:
|
||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||
|
||||
@@ -3602,6 +3628,9 @@ packages:
|
||||
engines: {node: ^16.10.0 || >=18.0.0}
|
||||
hasBin: true
|
||||
|
||||
nuxt-driver.js@0.0.11:
|
||||
resolution: {integrity: sha512-WzcNjEk7VGyMrt6vKAQGXiaf/sHp7cfGYtZ1O6fxow3DYWJ+a2NHsLE9ejumSSH753JKzmLNpJRrX0YYFCnl9g==}
|
||||
|
||||
nuxt@3.12.4:
|
||||
resolution: {integrity: sha512-/ddvyc2kgYYIN2UEjP8QIz48O/W3L0lZm7wChIDbOCj0vF/yLLeZHBaTb3aNvS9Hwp269nfjrm8j/mVxQK4RhA==}
|
||||
engines: {node: ^14.18.0 || >=16.10.0}
|
||||
@@ -6493,7 +6522,7 @@ snapshots:
|
||||
'@tailwindcss/container-queries': 0.1.1(tailwindcss@3.4.7)
|
||||
'@tailwindcss/forms': 0.5.7(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/math': 10.11.0(vue@3.4.34)
|
||||
defu: 6.1.4
|
||||
@@ -7165,6 +7194,16 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- 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)':
|
||||
dependencies:
|
||||
'@vueuse/core': 10.11.0(vue@3.4.34)
|
||||
@@ -7187,6 +7226,23 @@ snapshots:
|
||||
|
||||
'@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)':
|
||||
dependencies:
|
||||
vue-demi: 0.14.10(vue@3.4.34)
|
||||
@@ -7194,6 +7250,13 @@ snapshots:
|
||||
- '@vue/composition-api'
|
||||
- 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: {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
@@ -7866,6 +7929,8 @@ snapshots:
|
||||
|
||||
dotenv@16.4.5: {}
|
||||
|
||||
driver.js@1.3.1: {}
|
||||
|
||||
duplexer@0.1.2: {}
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
@@ -9126,6 +9191,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
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)):
|
||||
dependencies:
|
||||
'@nuxt/devalue': 2.0.2
|
||||
@@ -9645,7 +9719,7 @@ snapshots:
|
||||
'@internationalized/date': 3.5.5
|
||||
'@internationalized/number': 3.5.3
|
||||
'@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)
|
||||
aria-hidden: 1.2.4
|
||||
defu: 6.1.4
|
||||
|
||||
56
typings/types.d.ts
vendored
56
typings/types.d.ts
vendored
@@ -40,6 +40,24 @@ interface DigitalHumanItem {
|
||||
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
|
||||
namespace req {
|
||||
namespace user {
|
||||
@@ -115,6 +133,29 @@ namespace req {
|
||||
page?: 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 {
|
||||
@@ -191,6 +232,21 @@ namespace resp {
|
||||
video_sub_id: number
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user