Merge remote-tracking branch 'refs/remotes/origin/main'
This commit is contained in:
58
components/aigc/NavItem.vue
Normal file
58
components/aigc/NavItem.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'i-tabler-photo-filled',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
admin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hide: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const active = computed(() => {
|
||||
return route.path === props.to
|
||||
})
|
||||
|
||||
const activeClass = computed(() => {
|
||||
return props.admin ? 'bg-amber-500 text-white' : 'bg-primary text-white'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
v-if="!hide"
|
||||
:class="{
|
||||
[activeClass]: active,
|
||||
'hover:bg-neutral-200 dark:hover:bg-neutral-800': !active,
|
||||
}"
|
||||
:to="to"
|
||||
class="px-4 py-3 flex justify-between items-center rounded-lg transition cursor-pointer"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon :name="icon" class="text-xl inline"/>
|
||||
<h1 class="flex-1 text-[14px] font-medium line-clamp-1">
|
||||
{{ label }}
|
||||
</h1>
|
||||
</div>
|
||||
<UBadge v-if="admin" color="amber" label="OP" size="xs" variant="subtle"/>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type {PropType} from 'vue'
|
||||
import type {ChatSession} from '~/typings/llm'
|
||||
import type { PropType } from 'vue'
|
||||
import type { ChatSession } from '~/typings/llm'
|
||||
|
||||
const props = defineProps({
|
||||
active: {
|
||||
@@ -29,10 +29,10 @@ const dayjs = useDayjs()
|
||||
<Icon
|
||||
v-if="!!chatSession.assistant"
|
||||
name="i-tabler-masks-theater"
|
||||
class="text-lg -mt-1 mr-1"
|
||||
class="text-lg mr-1 "
|
||||
/>
|
||||
<span>
|
||||
{{ !!chatSession.assistant ? chatSession.assistant.tpl_name : chatSession.subject }}
|
||||
<span class="flex-1 text-ellipsis overflow-x-hidden">
|
||||
{{ !!chatSession.assistant ? chatSession.assistant.tpl_name : chatSession.subject }}啊塞啊塞啊塞啊塞啊塞啊塞
|
||||
</span>
|
||||
</div>
|
||||
<div class="chat-card-meta">
|
||||
@@ -43,7 +43,7 @@ const dayjs = useDayjs()
|
||||
@click.stop="emit('remove', chatSession)"
|
||||
class="chat-card-remove-btn text-neutral-400 group-hover:opacity-100 md:group-hover:-translate-x-0.5"
|
||||
>
|
||||
<UIcon name="i-tabler-trash"/>
|
||||
<Icon name="i-tabler-trash"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,7 +59,7 @@ const dayjs = useDayjs()
|
||||
}
|
||||
|
||||
&-title {
|
||||
@apply w-[calc(100%-16px)] text-sm font-medium text-ellipsis text-nowrap overflow-x-hidden;
|
||||
@apply w-[calc(100%-16px)] inline-flex items-center text-sm font-medium text-ellipsis text-nowrap overflow-x-hidden;
|
||||
}
|
||||
|
||||
&-meta {
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import CGTaskCard from '~/components/aigc/course-generate/CGTaskCard.vue'
|
||||
import FileDnD from '~/components/uni/FileDnD/index.vue'
|
||||
import { useFetchWrapped } from '~/composables/useFetchWrapped'
|
||||
import { type InferType, number, object, string } from 'yup'
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
import ModalAuthentication from '~/components/ModalAuthentication.vue'
|
||||
|
||||
const toast = useToast()
|
||||
const modal = useModal()
|
||||
const loginState = useLoginState()
|
||||
|
||||
const isCreateCourseModalOpen = ref(false)
|
||||
const creationPending = ref(false)
|
||||
const deletePending = ref(false)
|
||||
|
||||
const {
|
||||
data: courseList,
|
||||
pending: courseListPending,
|
||||
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: 1,
|
||||
perpage: 10,
|
||||
}), {
|
||||
transform: res => res?.data.items || [],
|
||||
},
|
||||
)
|
||||
|
||||
const onCreateCourseClick = () => {
|
||||
isCreateCourseModalOpen.value = true
|
||||
}
|
||||
|
||||
const createCourseSchema = object({
|
||||
task_title: string().trim().min(4, '标题必须大于4个字符').max(20, '标题不能超过20个字符').required('请输入微课标题'),
|
||||
gen_server: string().required(),
|
||||
speed: number().default(1.0).min(0.5).max(1.5).required(),
|
||||
})
|
||||
|
||||
type CreateCourseSchema = InferType<typeof createCourseSchema>
|
||||
|
||||
const createCourseState = reactive({
|
||||
task_title: undefined,
|
||||
gen_server: 'main',
|
||||
speed: 1.0,
|
||||
})
|
||||
|
||||
const selected_file = ref<File[] | null>(null)
|
||||
|
||||
const onCreateCourseSubmit = async (event: FormSubmitEvent<CreateCourseSchema>) => {
|
||||
console.log(event.data)
|
||||
if (!selected_file.value) {
|
||||
toast.add({
|
||||
title: '未选择文件',
|
||||
description: '请先选择 PPTX 文件',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-alert-triangle',
|
||||
})
|
||||
return
|
||||
}
|
||||
creationPending.value = true
|
||||
// upload PPTX file
|
||||
useFileGo(selected_file.value[0]).then(url => {
|
||||
useFetchWrapped<req.gen.CourseGenCreate & AuthedRequest, BaseResponse<resp.gen.CourseGenCreate>>('App.Digital_Convert.Create', {
|
||||
token: loginState.token!,
|
||||
user_id: loginState.user.id,
|
||||
task_title: event.data.task_title,
|
||||
gen_server: event.data.gen_server as 'main' | 'standby1',
|
||||
speed: 2 - event.data.speed,
|
||||
ppt_url: url,
|
||||
digital_human_id: 40696,
|
||||
custom_video: '[]',
|
||||
opening_url: '',
|
||||
ending_url: '',
|
||||
}).then(res => {
|
||||
if (res.data.record_status === 1) {
|
||||
toast.add({
|
||||
title: '创建成功',
|
||||
description: '微课视频已开始生成',
|
||||
color: 'green',
|
||||
icon: 'i-tabler-check',
|
||||
})
|
||||
refreshCourseList()
|
||||
isCreateCourseModalOpen.value = false
|
||||
} else {
|
||||
toast.add({
|
||||
title: '创建失败',
|
||||
description: '未知错误',
|
||||
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',
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const i = setInterval(refreshCourseList, 1000 * 5)
|
||||
onBeforeUnmount(() => clearInterval(i))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="font-sans h-full">
|
||||
<div class="p-4 border-b dark:border-neutral-700">
|
||||
<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()
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<Transition name="loading-screen">
|
||||
<div v-if="!loginState.is_logged_in" class="w-full h-full">
|
||||
<div class="w-full h-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
||||
<Icon name="i-tabler-user-circle" class="text-7xl text-neutral-300 dark:text-neutral-700"/>
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400">请登录后使用</p>
|
||||
<UButton class="mt-2 font-bold" color="black" variant="solid" size="xs"
|
||||
@click="modal.open(ModalAuthentication)">
|
||||
登录
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="courseList?.length === 0"
|
||||
class="w-full h-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
||||
<Icon name="i-tabler-photo-hexagon" class="text-7xl text-neutral-300 dark:text-neutral-700"/>
|
||||
<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 gap-4">
|
||||
<TransitionGroup name="card">
|
||||
<CGTaskCard
|
||||
v-for="(course, index) in courseList"
|
||||
:key="course.task_id || 'unknown' + index"
|
||||
:course="course"
|
||||
@delete="task_id => onCourseDelete(task_id)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<UModal
|
||||
v-model="isCreateCourseModalOpen"
|
||||
prevent-close
|
||||
>
|
||||
<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">
|
||||
新建微课视频
|
||||
</h3>
|
||||
<UButton
|
||||
class="-my-1"
|
||||
color="gray"
|
||||
icon="i-tabler-x"
|
||||
variant="ghost"
|
||||
@click="isCreateCourseModalOpen = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UForm
|
||||
:schema="createCourseSchema"
|
||||
:state="createCourseState"
|
||||
class="space-y-4"
|
||||
@submit="onCreateCourseSubmit"
|
||||
>
|
||||
<div class="flex justify-between gap-2 *:flex-1">
|
||||
<UFormGroup label="微课标题" name="task_title">
|
||||
<UInput v-model="createCourseState.task_title" placeholder="请输入微课标题"/>
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="生成线路" name="gen_server">
|
||||
<USelectMenu
|
||||
v-model="createCourseState.gen_server"
|
||||
:options="[{
|
||||
label: '主线路',
|
||||
value: 'main',
|
||||
}, {
|
||||
label: '备用线路',
|
||||
value: 'standby1',
|
||||
}]"
|
||||
option-attribute="label"
|
||||
value-attribute="value"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<UFormGroup label="PPT 文件">
|
||||
<FileDnD
|
||||
accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||
@change="file => selected_file = file"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<div class="flex justify-end space-x-4 pt-4">
|
||||
<UButton
|
||||
color="gray"
|
||||
label="取消"
|
||||
variant="ghost"
|
||||
@click="isCreateCourseModalOpen = false"
|
||||
/>
|
||||
<UButton
|
||||
color="primary"
|
||||
label="提交"
|
||||
type="submit"
|
||||
variant="solid"
|
||||
:loading="creationPending"
|
||||
/>
|
||||
</div>
|
||||
</UForm>
|
||||
</UCard>
|
||||
</UModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.loading-screen-leave-active {
|
||||
@apply transition duration-300;
|
||||
}
|
||||
|
||||
.loading-screen-leave-to {
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
.card-enter-active, .card-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.card-enter, .card-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.card-enter-to, .card-leave {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
绿幕视频生成
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -3,7 +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/course-generate/SRTEditor.vue'
|
||||
import SRTEditor from '~/components/aigc/generation/SRTEditor.vue'
|
||||
|
||||
const toast = useToast()
|
||||
const { metaSymbol } = useShortcuts()
|
||||
@@ -37,9 +37,9 @@ defineShortcuts({
|
||||
},
|
||||
},
|
||||
'meta_s': {
|
||||
handler: () => {
|
||||
handler: async () => {
|
||||
if (isDropdownOpen.value && isDownloadable.value) {
|
||||
startDownload(props.course.subtitle_url, `眩生花微课_${ props.course.title }_${ props.course.task_id }.srt`)
|
||||
await startDownload(await fetchCourseSubtitleUrl(props.course), `眩生花微课_${ props.course.title }_${ props.course.task_id }.srt`)
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -181,9 +181,9 @@ const copyTaskId = (extraMessage?: string) => {
|
||||
<div class="flex-1 overflow-hidden pt-1">
|
||||
<h1
|
||||
:title="course.title"
|
||||
class="text-sm font-medium overflow-hidden text-ellipsis text-nowrap"
|
||||
class="inline-flex items-center text-sm font-medium overflow-hidden text-ellipsis text-nowrap"
|
||||
>
|
||||
<Icon class="-mt-0.5 -ml-0.5 text-base" name="i-tabler-book-2"/>
|
||||
<Icon class="text-base" name="i-tabler-book-2"/>
|
||||
<span class="pl-0.5">{{ course.title }}</span>
|
||||
</h1>
|
||||
<p class="text-xs pt-0.5 text-neutral-400 space-x-2">
|
||||
@@ -220,7 +220,7 @@ const copyTaskId = (extraMessage?: string) => {
|
||||
disabled: !isDownloadable,
|
||||
click: () => isPreviewModalOpen = true,
|
||||
}, {
|
||||
label: '查看字幕',
|
||||
label: '编辑字幕',
|
||||
icon: 'i-solar-subtitles-linear',
|
||||
shortcuts: [metaSymbol, 'D'],
|
||||
disabled: !isDownloadable,
|
||||
@@ -233,8 +233,8 @@ const copyTaskId = (extraMessage?: string) => {
|
||||
icon: 'i-tabler-file-download',
|
||||
shortcuts: [metaSymbol, 'S'],
|
||||
disabled: !isDownloadable,
|
||||
click: () => {
|
||||
startDownload(course.subtitle_url, `眩生花微课_${ props.course.title }_${ props.course.task_id }.srt`)
|
||||
click: async () => {
|
||||
await startDownload(await fetchCourseSubtitleUrl(course), `眩生花微课_${ props.course.title }_${ props.course.task_id }.srt`)
|
||||
}
|
||||
}], [{
|
||||
label: '删除记录',
|
||||
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>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
import { encode } from '@monosky/base64'
|
||||
|
||||
interface Subtitle {
|
||||
start: string;
|
||||
@@ -17,25 +18,29 @@ const props = defineProps({
|
||||
|
||||
const dayjs = useDayjs()
|
||||
const toast = useToast()
|
||||
const loginState = useLoginState()
|
||||
|
||||
const isDrawerActive = ref(false)
|
||||
const isLoading = ref(true)
|
||||
const isSaving = ref(false)
|
||||
const rawSrt = ref<string | null>(null)
|
||||
const subtitles = ref<Subtitle[]>([])
|
||||
const modified = ref(false)
|
||||
|
||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||
|
||||
const loadSrt = async () => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await fetch(props.course.subtitle_url)
|
||||
// const response = await fetch(props.course.subtitle_url)
|
||||
const response = await fetch(await fetchCourseSubtitleUrl(props.course))
|
||||
const text = await response.text()
|
||||
rawSrt.value = text
|
||||
parseSrt(text)
|
||||
} catch (err) {
|
||||
toast.add({
|
||||
title: '加载字幕失败',
|
||||
description: err as string || '未知错误',
|
||||
description: `${ err }` || '未知错误',
|
||||
color: 'red',
|
||||
})
|
||||
} finally {
|
||||
@@ -129,6 +134,30 @@ const onSubtitleInputClick = (subtitle: Subtitle) => {
|
||||
videoElement.value.pause()
|
||||
}
|
||||
|
||||
const saveNewSubtitle = () => {
|
||||
isSaving.value = true
|
||||
const encodedSubtitle = encode(generateSrt())
|
||||
useFetchWrapped<
|
||||
req.gen.CourseSubtitleCreate & AuthedRequest,
|
||||
BaseResponse<resp.gen.CourseSubtitleCreate>
|
||||
>('App.Digital_VideoSubtitle.CreateFile', {
|
||||
token: loginState.token!,
|
||||
user_id: loginState.user.id,
|
||||
sub_type: 1,
|
||||
sub_content: encodedSubtitle,
|
||||
task_id: props.course?.task_id,
|
||||
}).then(_ => {
|
||||
modified.value = false
|
||||
toast.add({
|
||||
color: 'green',
|
||||
title: '字幕已保存',
|
||||
description: '修改后的字幕文件已保存',
|
||||
})
|
||||
}).finally(() => {
|
||||
isSaving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (rawSrt.value) {
|
||||
parseSrt(rawSrt.value)
|
||||
@@ -240,6 +269,7 @@ defineExpose({
|
||||
:autofocus="false"
|
||||
:color="subtitle.active ? 'primary' : undefined"
|
||||
@click="onSubtitleInputClick(subtitle)"
|
||||
@input="() => { if(!modified) modified = true }"
|
||||
>
|
||||
<template #trailing>
|
||||
<Icon v-if="subtitle.active" name="tabler:keyframe-align-vertical-filled"/>
|
||||
@@ -251,12 +281,12 @@ defineExpose({
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<!-- TODO: 24/07/02 Modified subtitles upload -->
|
||||
<UButton @click="() => {
|
||||
console.log(generateSrt())
|
||||
}">
|
||||
Generate
|
||||
</UButton>
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<span v-if="modified" class="text-sm text-yellow-500 font-medium">已更改但未保存</span>
|
||||
<UButton :disabled="!modified" :loading="isSaving" icon="i-tabler-device-floppy" @click="saveNewSubtitle">
|
||||
保存{{ isSaving ? '中' : '' }}
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</USlideover>
|
||||
Reference in New Issue
Block a user