feat(titles-template): 片头片尾页面

This commit is contained in:
2025-01-16 23:37:43 +08:00
parent 57ad96e87b
commit bb75f72759
4 changed files with 453 additions and 13 deletions

View File

@@ -0,0 +1,81 @@
<script lang="ts" setup>
defineProps({
type: {
type: String as PropType<'system' | 'user'>,
required: true,
},
data: {
type: Object as PropType<TitlesTemplate>,
required: true,
},
})
const emit = defineEmits({
'user-titles-request': (_titles: TitlesTemplate) => true,
'user-titles-delete': (_titles: TitlesTemplate) => true,
'system-titles-delete': (_titles: TitlesTemplate) => true,
})
const loginState = useLoginState()
</script>
<template>
<div
class="relative w-full flex flex-col rounded-lg border border-neutral-200 dark:border-neutral-700 overflow-hidden shadow-none hover:shadow transition-shadow"
>
<NuxtImg
placeholder
placeholder-class="w-full aspect-[16/9] object-cover bg-neutral-200 dark:bg-neutral-800"
class="w-full h-full object-cover relative"
:src="data.opening_url"
/>
<div class="relative p-2 flex justify-between items-center gap-2">
<div class="flex-1">
<h1
class="text-base font-medium line-clamp-1"
:title="data.title"
>
{{ data.title }}
</h1>
<p class="text-xs font-medium text-gray-400">
{{ data.description }}
</p>
</div>
<div>
<UButtonGroup
size="xs"
v-if="type === 'system'"
>
<UButton
label="使用模板"
color="white"
@click="emit('user-titles-request', data)"
/>
<UButton
icon="tabler:trash"
color="red"
@click="emit('system-titles-delete', data)"
v-if="
/** TODO: Temporarily disable this button */ false &&
loginState.user.auth_code === 2
"
/>
</UButtonGroup>
<UButtonGroup
size="xs"
v-if="type === 'user'"
>
<UButton
icon="tabler:trash"
label="删除素材"
variant="soft"
color="red"
@click="emit('user-titles-delete', data)"
/>
</UButtonGroup>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@@ -1,16 +1,18 @@
<script setup lang="tsx"> <script setup lang="ts">
import NavItem from '~/components/aigc/NavItem.vue' import NavItem from '~/components/aigc/NavItem.vue'
useSeoMeta({ useSeoMeta({
title: '智能生成', title: '智能生成',
}) })
const navList = ref<{ const navList = ref<
{
label: string label: string
icon: string icon: string
to: string to: string
admin?: boolean admin?: boolean
}[]>([ }[]
>([
{ {
label: '微课视频生成', label: '微课视频生成',
icon: 'tabler:presentation-analytics', icon: 'tabler:presentation-analytics',
@@ -26,6 +28,11 @@ const navList = ref<{
icon: 'tabler:user-screen', icon: 'tabler:user-screen',
to: '/generation/avatar-models', to: '/generation/avatar-models',
}, },
{
label: '片头片尾模板',
icon: 'tabler:keyframes',
to: '/generation/materials',
},
{ {
label: '用户管理', label: '用户管理',
icon: 'tabler:users', icon: 'tabler:users',
@@ -47,9 +54,9 @@ onMounted(() => {
<template> <template>
<div class="w-full flex relative"> <div class="w-full flex relative">
<div
<div 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] 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] border-r border-neutral-200 dark:border-neutral-700 transition-all duration-300 ease-out"
border-r border-neutral-200 dark:border-neutral-700 transition-all duration-300 ease-out"> >
<div class="flex flex-col flex-1 overflow-auto overflow-x-hidden"> <div class="flex flex-col flex-1 overflow-auto overflow-x-hidden">
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<ClientOnly> <ClientOnly>
@@ -78,7 +85,6 @@ onMounted(() => {
</div> </div>
</Transition> </Transition>
</LoginNeededContent> </LoginNeededContent>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,334 @@
<script lang="ts" setup>
import type { FormSubmitEvent } from '#ui/types'
import { number, object, string, type InferType } from 'yup'
const loginState = useLoginState()
const toast = useToast()
const isUserTitlesRequestModalActive = ref(false)
const systemPagination = reactive({
page: 1,
pageSize: 15,
})
const userPagination = reactive({
page: 1,
pageSize: 15,
})
const { data: systemTitlesTemplate, status: systemTitlesTemplateStatus } =
useAsyncData(
'systemTitlesTemplate',
() =>
useFetchWrapped<
PagedDataRequest & AuthedRequest,
BaseResponse<PagedData<TitlesTemplate>>
>('App.Digital_Titles.GetList', {
token: loginState.token!,
user_id: loginState.user.id,
page: systemPagination.page,
perpage: systemPagination.pageSize,
}),
{
watch: [systemPagination],
}
)
const {
data: userTitlesTemplate,
status: userTitlesTemplateStatus,
refresh: refreshUserTitlesTemplate,
} = useAsyncData(
'userTitlesTemplate',
() =>
useFetchWrapped<
PagedDataRequest & AuthedRequest & { process_status: 0 | 1 },
BaseResponse<PagedData<TitlesTemplate>>
>('App.User_UserTitles.GetList', {
token: loginState.token!,
user_id: loginState.user.id,
to_user_id: loginState.user.id,
page: userPagination.page,
perpage: userPagination.pageSize,
process_status: 1,
}),
{
watch: [userPagination],
}
)
const { data: userTitlesRequests, refresh: refreshUserTitlesRequests } =
useAsyncData('userTitlesTemplateRequests', () =>
useFetchWrapped<
PagedDataRequest & AuthedRequest & { process_status: 0 | 1 },
BaseResponse<PagedData<TitlesTemplate>>
>('App.User_UserTitles.GetList', {
token: loginState.token!,
user_id: loginState.user.id,
to_user_id: loginState.user.id,
process_status: 0,
})
)
const userTitlesSchema = object({
title_id: number().required().moreThan(0, '模板 ID 无效'),
title: string().required('请填写课程名称'),
description: string().required('请填写主讲人名字'),
})
type UserTitlesSchema = InferType<typeof userTitlesSchema>
const userTitlesState = reactive({
title_id: 0,
title: '',
description: '',
})
const onUserTitlesRequest = (titles: TitlesTemplate) => {
userTitlesState.title_id = titles.id
isUserTitlesRequestModalActive.value = true
}
const onUserTitlesDelete = (titles: TitlesTemplate) => {
useFetchWrapped<
Pick<req.gen.TitlesTemplateRequest, 'to_user_id'> & {
user_title_id: number
} & AuthedRequest,
BaseResponse<any>
>('App.User_UserTitles.DeleteConn', {
token: loginState.token!,
user_id: loginState.user.id,
to_user_id: loginState.user.id,
user_title_id: titles.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(() => {
userPagination.page = 1
refreshUserTitlesTemplate()
})
}
const onUserTitlesSubmit = (event: FormSubmitEvent<UserTitlesSchema>) => {
useFetchWrapped<
req.gen.TitlesTemplateRequest & AuthedRequest,
BaseResponse<any>
>('App.User_UserTitles.CreateConn', {
token: loginState.token!,
user_id: loginState.user.id,
to_user_id: loginState.user.id,
...event.data,
})
.then((res) => {
if (res.ret === 200) {
userTitlesState.title = ''
userTitlesState.description = ''
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(() => {
isUserTitlesRequestModalActive.value = false
userPagination.page = 1
refreshUserTitlesRequests()
})
}
</script>
<template>
<div class="h-full">
<div class="p-4 pb-0">
<BubbleTitle
title="片头片尾模版"
subtitle="Materials"
></BubbleTitle>
<GradientDivider />
</div>
<div class="p-4">
<div
class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-5 gap-4"
v-if="systemTitlesTemplateStatus === 'pending'"
>
<USkeleton
class="w-full aspect-video"
v-for="i in systemPagination.pageSize"
:key="i"
/>
</div>
<div
class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-5 gap-4"
v-else
>
<AigcGenerationTitlesTemplate
v-for="titles in systemTitlesTemplate?.data.items"
:data="titles"
type="system"
:key="titles.id"
@user-titles-request="onUserTitlesRequest"
/>
</div>
</div>
<div class="p-4 pb-0 pt-12">
<BubbleTitle
title="我的片头片尾"
:bubble="false"
></BubbleTitle>
<GradientDivider />
</div>
<div class="p-4 pt-2">
<UAlert
v-if="userTitlesRequests?.data.total"
color="primary"
icon="tabler:info-circle"
variant="subtle"
class="mb-4"
>
<template #title>
{{ userTitlesRequests?.data.total }} 个片头正在制作中
</template>
<template #description>
<div class="flex gap-2">
<div
v-for="titles in userTitlesRequests?.data.items"
:key="titles.id"
>
<p>
{{ titles.title }}
</p>
</div>
</div>
</template>
</UAlert>
<div
class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-5 gap-4"
v-if="userTitlesTemplateStatus === 'pending'"
>
<USkeleton
class="w-full aspect-video"
v-for="i in userPagination.pageSize"
:key="i"
/>
</div>
<div
class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-5 gap-4"
v-else
>
<AigcGenerationTitlesTemplate
v-for="titles in userTitlesTemplate?.data.items"
type="user"
:data="titles"
:key="titles.id"
@user-titles-delete="onUserTitlesDelete"
/>
</div>
</div>
<UModal v-model="isUserTitlesRequestModalActive">
<UCard
:ui="{
ring: '',
divide: 'divide-y divide-gray-100 dark:divide-gray-800',
}"
>
<template #header>
<div class="flex items-center justify-between">
<div
class="text-base font-semibold leading-6 text-gray-900 dark:text-white overflow-hidden"
>
<p>使用模板</p>
</div>
<UButton
class="-my-1"
color="gray"
icon="i-tabler-x"
variant="ghost"
@click="isUserTitlesRequestModalActive = false"
/>
</div>
</template>
<div>
<UForm
class="space-y-4"
:schema="userTitlesSchema"
:state="userTitlesState"
@submit="onUserTitlesSubmit"
>
<UFormGroup
label="课程名称"
name="title"
required
>
<UInput v-model="userTitlesState.title" />
</UFormGroup>
<UFormGroup
label="主讲人"
name="description"
required
>
<UInput v-model="userTitlesState.description" />
</UFormGroup>
<UFormGroup name="title_id">
<UInput
type="hidden"
v-model="userTitlesState.title_id"
/>
</UFormGroup>
<UAlert
icon="tabler:info-circle"
color="primary"
variant="subtle"
title="片头片尾模板"
description="提交模板相应字段后,待工作人员制作好后即可使用"
/>
<div class="flex justify-end gap-2">
<UButton
color="primary"
variant="soft"
label="取消"
@click="isUserTitlesRequestModalActive = false"
/>
<UButton
color="primary"
type="submit"
>
提交
</UButton>
</div>
</UForm>
</div>
</UCard>
</UModal>
</div>
</template>
<style scoped></style>

19
typings/types.d.ts vendored
View File

@@ -64,6 +64,18 @@ interface GBVideoItem {
speed: number speed: number
} }
interface TitlesTemplate {
id: number
create_time: number
opening_url: string
opening_file: string
ending_url: string
ending_file: string
type: number
title: string
description: string
}
// Common request and response schemas // Common request and response schemas
namespace req { namespace req {
namespace user { namespace user {
@@ -168,6 +180,13 @@ namespace req {
interface GBVideoDelete { interface GBVideoDelete {
task_id: string task_id: string
} }
interface TitlesTemplateRequest {
to_user_id: number
title_id: number
title: string
description: string
}
} }
interface AssistantTemplateList { interface AssistantTemplateList {