feat: course generation
This commit is contained in:
@@ -1,75 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import CourseGenerate from '~/components/aigc/course-generate/CourseGenerate.vue'
|
||||
import CourseGenerateGreenScreen from '~/components/aigc/course-generate/CourseGenerateGreenScreen.vue'
|
||||
|
||||
useHead({
|
||||
title: 'PPT 生成视频 | XSH AI',
|
||||
})
|
||||
|
||||
const showSidebar = ref(false)
|
||||
const toggleSidebar = () => {
|
||||
showSidebar.value = !showSidebar.value
|
||||
}
|
||||
|
||||
const routes = [
|
||||
{
|
||||
name: '微课视频生成',
|
||||
component: CourseGenerate,
|
||||
},
|
||||
{
|
||||
name: '绿幕视频生成',
|
||||
component: CourseGenerateGreenScreen,
|
||||
},
|
||||
]
|
||||
|
||||
const currentComponentIndex = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full flex relative">
|
||||
<div
|
||||
:class="{'translate-x-0': showSidebar}"
|
||||
class="absolute -translate-x-full md:sticky md:translate-x-0 z-10 flex flex-col h-[calc(100vh-4rem)] bg-neutral-100 dark:bg-neutral-900 p-4 w-full md:w-[300px]
|
||||
shadow-sidebar border-r border-transparent dark:border-neutral-700 transition-all duration-300 ease-out"
|
||||
>
|
||||
<div class="flex-1 flex flex-col overflow-auto overflow-x-hidden">
|
||||
<div class="flex flex-col gap-3 relative">
|
||||
<div class="space-y-2 nav">
|
||||
|
||||
<div
|
||||
v-for="(route, index) in routes"
|
||||
:key="index"
|
||||
:class="['nav-item', { active: currentComponentIndex === index }]"
|
||||
@click="currentComponentIndex = index"
|
||||
>
|
||||
{{ route.name }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-[calc(100vh-4rem)] flex-1 overflow-y-auto bg-white dark:bg-neutral-900">
|
||||
<ClientOnly>
|
||||
<Component :is="routes[currentComponentIndex].component"/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nav {
|
||||
&-item {
|
||||
@apply w-full px-4 py-3 bg-transparent rounded-md cursor-pointer select-none;
|
||||
@apply transition duration-150 ease-out;
|
||||
|
||||
&:hover {
|
||||
@apply bg-black/10;
|
||||
}
|
||||
|
||||
&.active {
|
||||
@apply bg-primary/20 text-primary shadow shadow-primary/20 font-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,31 @@
|
||||
<script setup lang="tsx">
|
||||
import NavItem from '~/components/ppt/NavItem.vue'
|
||||
import NavItem from '~/components/aigc/NavItem.vue'
|
||||
|
||||
useSeoMeta({
|
||||
title: '智能生成',
|
||||
})
|
||||
|
||||
const navList = [
|
||||
{
|
||||
label: '微课视频生成',
|
||||
icon: 'tabler:presentation-analytics',
|
||||
to: '/aigc/generation/course',
|
||||
},
|
||||
{
|
||||
label: '绿幕视频生成',
|
||||
icon: 'i-tabler-video',
|
||||
to: '/aigc/generation/green-screen',
|
||||
},
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
if (route.fullPath === '/aigc/generation') {
|
||||
router.push('/aigc/generation/course')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -15,14 +37,11 @@ useSeoMeta({
|
||||
<div class="flex flex-col gap-1">
|
||||
<ClientOnly>
|
||||
<NavItem
|
||||
icon="tabler:presentation-analytics"
|
||||
label="微课视频生成"
|
||||
to="/aigc/generation/video-generate"
|
||||
/>
|
||||
<NavItem
|
||||
icon="tabler:user-screen"
|
||||
label="数字讲师"
|
||||
to="/aigc/generation/digital-teachers"
|
||||
v-for="(item, i) in navList"
|
||||
:key="i"
|
||||
:icon="item.icon"
|
||||
:label="item.label"
|
||||
:to="item.to"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
@@ -30,10 +49,14 @@ useSeoMeta({
|
||||
</div>
|
||||
|
||||
<LoginNeededContent
|
||||
content-class="h-[calc(100vh-4rem)] flex-1 bg-white dark:bg-neutral-900"
|
||||
content-class="h-[calc(100vh-4rem)] flex-1 overflow-y-auto bg-white dark:bg-neutral-900"
|
||||
>
|
||||
<Transition name="subpage" mode="out-in">
|
||||
<NuxtPage :page-key="route => route.fullPath"/>
|
||||
<div>
|
||||
<Suspense>
|
||||
<NuxtPage :page-key="route.fullPath" keepalive/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</Transition>
|
||||
</LoginNeededContent>
|
||||
|
||||
@@ -43,11 +66,11 @@ useSeoMeta({
|
||||
<style>
|
||||
.subpage-enter-active,
|
||||
.subpage-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
@apply transition-all duration-300;
|
||||
}
|
||||
|
||||
.subpage-enter-from,
|
||||
.subpage-leave-to {
|
||||
opacity: 0;
|
||||
@apply opacity-0 translate-x-4;
|
||||
}
|
||||
</style>
|
||||
170
pages/aigc/generation/course.vue
Normal file
170
pages/aigc/generation/course.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<script lang="ts" setup>
|
||||
import CGTaskCard from '~/components/aigc/course-generate/CGTaskCard.vue'
|
||||
import { useFetchWrapped } from '~/composables/useFetchWrapped'
|
||||
import ModalAuthentication from '~/components/ModalAuthentication.vue'
|
||||
import SlideCreateCourse from '~/components/SlideCreateCourse.vue'
|
||||
|
||||
const toast = useToast()
|
||||
const modal = useModal()
|
||||
const slide = useSlideover()
|
||||
const loginState = useLoginState()
|
||||
|
||||
const deletePending = ref(false)
|
||||
const page = ref(1)
|
||||
|
||||
const {
|
||||
data: courseList,
|
||||
refresh: refreshCourseList,
|
||||
} = useAsyncData(
|
||||
() => useFetchWrapped<
|
||||
req.gen.CourseGenList & AuthedRequest,
|
||||
BaseResponse<PagedData<resp.gen.CourseGenItem>>
|
||||
>('App.Digital_Convert.GetList', {
|
||||
token: loginState.token!,
|
||||
user_id: loginState.user.id,
|
||||
to_user_id: loginState.user.id,
|
||||
page: page.value,
|
||||
perpage: 16,
|
||||
}), {
|
||||
watch: [page],
|
||||
},
|
||||
)
|
||||
|
||||
const onCreateCourseClick = () => {
|
||||
slide.open(SlideCreateCourse, {
|
||||
onSuccess: () => {
|
||||
refreshCourseList()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onCourseDelete = (task_id: string) => {
|
||||
if (!task_id) return
|
||||
deletePending.value = true
|
||||
useFetchWrapped<
|
||||
req.gen.CourseGenDelete & AuthedRequest,
|
||||
BaseResponse<resp.gen.CourseGenDelete>
|
||||
>('App.Digital_Convert.Delete', {
|
||||
token: loginState.token!,
|
||||
user_id: loginState.user.id,
|
||||
to_user_id: loginState.user.id,
|
||||
task_id,
|
||||
}).then(res => {
|
||||
if (res.ret === 200) {
|
||||
toast.add({
|
||||
title: '删除成功',
|
||||
description: '已删除任务记录',
|
||||
color: 'green',
|
||||
icon: 'i-tabler-check',
|
||||
})
|
||||
} else {
|
||||
toast.add({
|
||||
title: '删除失败',
|
||||
description: res.msg || '未知错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-alert-triangle',
|
||||
})
|
||||
}
|
||||
}).finally(() => {
|
||||
deletePending.value = false
|
||||
refreshCourseList()
|
||||
})
|
||||
}
|
||||
|
||||
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(refreshCourseList, 1000 * 5)
|
||||
onBeforeUnmount(() => clearInterval(i))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="font-sans h-full">
|
||||
<div class="p-4 pb-0">
|
||||
<BubbleTitle subtitle="VIDEOS" title="我的微课视频">
|
||||
<template #action>
|
||||
<UButton
|
||||
:trailing="false"
|
||||
color="primary"
|
||||
icon="i-tabler-plus"
|
||||
label="新建微课"
|
||||
size="md"
|
||||
variant="solid"
|
||||
@click="() => {
|
||||
if (!loginState.is_logged_in) {
|
||||
modal.open(ModalAuthentication)
|
||||
return
|
||||
}
|
||||
onCreateCourseClick()
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</BubbleTitle>
|
||||
<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">
|
||||
<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 class="p-4">
|
||||
<div class="relative grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 fhd:grid-cols-5 gap-4">
|
||||
<TransitionGroup
|
||||
name="card"
|
||||
@beforeLeave="beforeLeave"
|
||||
@leave="leave"
|
||||
>
|
||||
<CGTaskCard
|
||||
v-for="(course, index) in courseList?.data.items"
|
||||
:key="course.task_id || 'unknown' + index"
|
||||
:course="course"
|
||||
@delete="task_id => onCourseDelete(task_id)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<UPagination v-model="page" :page-count="16" :total="courseList?.data.total || 0"/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</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,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
useSeoMeta({
|
||||
title: '数字化教师',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<BubbleTitle title="数字化讲师" subtitle="DIGITAL" />
|
||||
<GradientDivider />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
13
pages/aigc/generation/green-screen.vue
Normal file
13
pages/aigc/generation/green-screen.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
绿幕视频生成
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,69 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
useSeoMeta({
|
||||
title: '课程视频生成',
|
||||
})
|
||||
|
||||
const testItem = {
|
||||
'id': 1599,
|
||||
'device_id': 'Test_Device_V3',
|
||||
'user_id': 1,
|
||||
'task_id': 'SQOeN1j2heRoQeGGTFh3Tu2WP9kUcz4L',
|
||||
'create_time': 1713408239,
|
||||
'token': 'not use',
|
||||
'progress': 100,
|
||||
'digital_human_id': 40696,
|
||||
'complete_time': 1713409821,
|
||||
'duration': 1578819,
|
||||
'video_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/75d1e1cee595a7f5758c59289d1a74b9.mp4',
|
||||
'subtitle_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/a95cfef4524e90f5509a5c248e5c2061.srt',
|
||||
'video_cover': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/b4161a85573fc09be82fa7cf7dd9abfa.png',
|
||||
'custom_video': '[]',
|
||||
'title': '1-2 一键启动:零基础快速搭建Keil开发环境实操教程',
|
||||
'ppt_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/material/2024-04-18/0a8827a1ae32ece196536a19bab1dff5.pptx',
|
||||
'opening_url': '',
|
||||
'ending_url': '',
|
||||
'video_duration': 507,
|
||||
'message': 'ok',
|
||||
'speed': 1,
|
||||
}
|
||||
const testItem2 = {
|
||||
'id': 1599,
|
||||
'device_id': 'Test_Device_V3',
|
||||
'user_id': 1,
|
||||
'task_id': 'SQOeN1j2heRoQeGGTFh3Tu2WP9kUcz4L',
|
||||
'create_time': 1713408239,
|
||||
'token': 'not use',
|
||||
'progress': null,
|
||||
'digital_human_id': 40696,
|
||||
'complete_time': 1713409821,
|
||||
'duration': 1578819,
|
||||
'video_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/75d1e1cee595a7f5758c59289d1a74b9.mp4',
|
||||
'subtitle_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/a95cfef4524e90f5509a5c248e5c2061.srt',
|
||||
'video_cover': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/file/2024-04-18/b4161a85573fc09be82fa7cf7dd9abfa.png',
|
||||
'custom_video': '[]',
|
||||
'title': '1-2 一键启动:零基础快速搭建Keil开发环境实操教程',
|
||||
'ppt_url': 'https://static-xsh.oss-cn-chengdu.aliyuncs.com/material/2024-04-18/0a8827a1ae32ece196536a19bab1dff5.pptx',
|
||||
'opening_url': '',
|
||||
'ending_url': '',
|
||||
'video_duration': 507,
|
||||
'message': 'ok',
|
||||
'speed': 1,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<BubbleTitle title="我的微课" subtitle="VIDEOS"/>
|
||||
<GradientDivider/>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<PPTGenerationRecord :record="testItem"/>
|
||||
<PPTGenerationRecord :record="testItem2"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user