Files
xsh-assistant-next/pages/generation/ppt-templates.vue

532 lines
14 KiB
Vue

<script lang="ts" setup>
import { number, object, string, type InferType } from 'yup'
import type { FormSubmitEvent } from '#ui/types'
import dayjs from 'dayjs'
const toast = useToast()
const loginState = useLoginState()
const isCreateSlideOpen = ref(false)
const isCatSlideOpen = ref(false)
const { data: pptCategories, refresh: refreshPPTCategories } = useAsyncData(
'pptCategories',
() =>
useFetchWrapped<
PagedDataRequest & AuthedRequest,
BaseResponse<PagedData<PPTCategory>>
>('App.Digital_PowerPointCat.GetList', {
token: loginState.token!,
user_id: loginState.user.id,
page: 1,
perpage: 20,
})
)
const selectedCat = ref<number>(0)
const pagination = reactive({
page: 1,
perpage: 18,
})
const { data: pptTemplates, refresh: refreshPptTemplates } = useAsyncData(
'pptTemplates',
() =>
useFetchWrapped<
PagedDataRequest & { type: string | number } & AuthedRequest,
BaseResponse<PagedData<PPTTemplate>>
>('App.Digital_PowerPoint.GetList', {
token: loginState.token!,
user_id: loginState.user.id,
page: pagination.page,
perpage: pagination.perpage,
type: selectedCat.value === 0 ? '' : selectedCat.value,
}),
{
watch: [selectedCat, pagination],
}
)
const onDownloadClick = (ppt: PPTTemplate) => {
const { download } = useDownload(ppt.file_url, `${ppt.title}.pptx`)
download()
}
const pptCreateState = reactive({
title: '',
description: '',
type: 0,
preview_url: '',
file_url: '',
})
const pptCreateSchema = object({
title: string().required('模板标题不能为空'),
description: string().required('模板描述不能为空'),
type: number().notOneOf([0], '无效分类').required('请选择模板分类'),
preview_url: string().required('请上传预览图'),
file_url: string().required('请上传 PPT 文件'),
})
type PPTCreateSchema = InferType<typeof pptCreateSchema>
const selectMenuOptions = computed(() => {
return pptCategories.value?.data.items.map((cat) => ({
label: cat.type,
value: cat.id,
}))
})
const onCreateSubmit = (event: FormSubmitEvent<PPTCreateSchema>) => {
useFetchWrapped<
{
title: string
description: string
type: number
preview_url: string
file_url: string
} & AuthedRequest,
BaseResponse<{ powerpoint_id: number }>
>('App.Digital_PowerPoint.Create', {
token: loginState.token!,
user_id: loginState.user.id,
title: event.data.title,
description: event.data.description,
type: event.data.type,
preview_url: event.data.preview_url,
file_url: event.data.file_url,
})
.then(async (res) => {
if (res.data.powerpoint_id) {
await refreshPPTCategories()
toast.add({
title: '创建成功',
description: '已加入模板库',
color: 'green',
icon: 'i-tabler-check',
})
isCreateSlideOpen.value = false
refreshPptTemplates()
Object.assign(pptCreateState, {
title: '',
description: '',
type: 0,
preview_url: '',
file_url: '',
})
}
})
.catch(() => {
toast.add({
title: '创建失败',
description: '请检查输入是否正确',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
})
}
const onFileSelect = async (files: FileList, type: 'preview' | 'ppt') => {
const url = await useFileGo(files[0])
if (type === 'preview') {
pptCreateState.preview_url = url
} else {
pptCreateState.file_url = url
}
toast.add({
title: '上传成功',
description: `已上传 ${type === 'preview' ? '预览图' : 'PPT 文件'}`,
color: 'green',
icon: 'i-tabler-check',
})
}
const onDeletePPT = (ppt: PPTTemplate) => {
useFetchWrapped<
{ powerpoint_id: number } & AuthedRequest,
BaseResponse<{ code: number }>
>('App.Digital_PowerPoint.Delete', {
token: loginState.token!,
user_id: loginState.user.id,
powerpoint_id: ppt.id,
})
.then(async (res) => {
if (res.ret === 200 && res.data.code === 1) {
await refreshPptTemplates()
toast.add({
title: '删除成功',
description: '已删除模板',
color: 'green',
icon: 'i-tabler-check',
})
} else {
toast.add({
title: '删除失败',
description: res.msg || '未知错误',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
}
})
.catch(() => {
toast.add({
title: '删除失败',
description: '未知错误',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
})
}
const createCatInput = ref('')
const onCreateCat = () => {
if (createCatInput.value) {
useFetchWrapped<
{ type: string } & AuthedRequest,
BaseResponse<{ ppt_cat_id: number }>
>('App.Digital_PowerPointCat.Create', {
token: loginState.token!,
user_id: loginState.user.id,
type: createCatInput.value,
})
.then(async (res) => {
if (res.data.ppt_cat_id) {
await refreshPPTCategories()
toast.add({
title: '创建成功',
description: '已加入分类列表',
color: 'green',
icon: 'i-tabler-check',
})
createCatInput.value = ''
}
})
.catch(() => {
toast.add({
title: '创建失败',
description: '请检查输入是否正确',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
})
}
}
const onDeleteCat = (cat: PPTCategory) => {
useFetchWrapped<
{ ppt_cat_id: number } & AuthedRequest,
BaseResponse<{ code: number }>
>('App.Digital_PowerPointCat.Delete', {
token: loginState.token!,
user_id: loginState.user.id,
ppt_cat_id: cat.id,
})
.then(async (res) => {
if (res.ret === 200 && res.data.code === 1) {
await refreshPPTCategories()
toast.add({
title: '删除成功',
description: '已删除分类',
color: 'green',
icon: 'i-tabler-check',
})
} else {
toast.add({
title: '删除失败',
description: res.msg || '未知错误',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
}
})
.catch(() => {
toast.add({
title: '删除失败',
description: '请检查输入是否正确',
color: 'red',
icon: 'i-tabler-alert-triangle',
})
})
}
</script>
<template>
<div>
<div class="p-4 pb-0">
<BubbleTitle
title="PPT 模板库"
subtitle="Slide Templates"
>
<template #action>
<UButton
v-if="loginState.user.auth_code === 2"
label="分类管理"
color="amber"
variant="soft"
icon="tabler:grid"
@click="isCatSlideOpen = true"
/>
<UButton
v-if="loginState.user.auth_code === 2"
label="创建模板"
color="amber"
variant="soft"
icon="tabler:plus"
@click="isCreateSlideOpen = true"
/>
</template>
</BubbleTitle>
<GradientDivider />
</div>
<div class="p-4 pt-0">
<!-- cat selector -->
<div class="flex flex-wrap gap-2">
<div
v-for="cat in [
{ id: 0, type: '全部' },
...(pptCategories?.data.items || []),
]"
@click="selectedCat = cat.id"
:key="cat.id"
:class="{
'bg-primary text-white': selectedCat === cat.id,
'bg-gray-100 text-gray-500': selectedCat !== cat.id,
}"
class="rounded-lg px-4 py-2 text-sm cursor-pointer"
>
{{ cat.type }}
</div>
</div>
<div class="space-y-4">
<div
v-if="pptTemplates?.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
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-4 mt-4"
>
<div
v-for="ppt in pptTemplates?.data.items"
:key="ppt.id"
class="relative bg-white rounded-lg shadow-md overflow-hidden"
>
<NuxtImg
:src="ppt.preview_url"
:alt="ppt.title"
class="w-full aspect-video object-cover"
/>
<div
class="absolute inset-x-0 bottom-0 p-3 pt-6 flex justify-between items-end bg-gradient-to-t from-black/50 to-transparent"
>
<div class="space-y-0.5">
<h3 class="text-base font-bold text-white">{{ ppt.title }}</h3>
<p class="text-xs font-medium text-neutral-200">
{{ ppt.description }}
</p>
</div>
<div class="flex items-center gap-2">
<!-- delete -->
<UButton
v-if="loginState.user.auth_code === 2"
size="sm"
color="red"
icon="tabler:trash"
variant="soft"
@click="onDeletePPT(ppt)"
/>
<UButton
label="下载"
size="sm"
color="primary"
icon="tabler:download"
@click="onDownloadClick(ppt)"
/>
</div>
</div>
</div>
</div>
<div class="w-full flex justify-end">
<UPagination
v-if="pptTemplates?.data.total > pagination.perpage"
:total="pptTemplates?.data.total"
:page-count="pagination.perpage"
:max="9"
v-model="pagination.page"
/>
</div>
</div>
</div>
<USlideover v-model="isCreateSlideOpen">
<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>
<UButton
class="flex absolute end-5 top-5 z-10"
color="gray"
icon="tabler:x"
padded
size="sm"
square
variant="ghost"
@click="isCreateSlideOpen = false"
/>
创建 PPT 模板
</template>
<UForm
class="space-y-4"
:schema="pptCreateSchema"
:state="pptCreateState"
@submit="onCreateSubmit"
>
<UFormGroup
label="模板标题"
name="title"
>
<UInput v-model="pptCreateState.title" />
</UFormGroup>
<UFormGroup
label="模板描述"
name="description"
>
<UTextarea v-model="pptCreateState.description" />
</UFormGroup>
<UFormGroup
label="模板分类"
name="type"
>
<USelectMenu
v-model="pptCreateState.type"
value-attribute="value"
option-attribute="label"
searchable
searchable-placeholder="搜索现有分类..."
:options="selectMenuOptions"
/>
</UFormGroup>
<UFormGroup
label="预览图"
name="preview_url"
>
<UniFileDnD
@change="onFileSelect($event, 'preview')"
accept="image/png,image/jpeg"
/>
</UFormGroup>
<UFormGroup
label="PPT 文件"
name="file_url"
>
<UniFileDnD
@change="onFileSelect($event, 'ppt')"
accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
/>
</UFormGroup>
<div class="flex justify-end">
<UButton
label="创建"
color="primary"
type="submit"
/>
</div>
</UForm>
</UCard>
</USlideover>
<USlideover v-model="isCatSlideOpen">
<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>
<UButton
class="flex absolute end-5 top-5 z-10"
color="gray"
icon="tabler:x"
padded
size="sm"
square
variant="ghost"
@click="isCatSlideOpen = false"
/>
PPT 模板分类管理
</template>
<div class="space-y-4">
<UFormGroup label="创建分类">
<UButtonGroup
orientation="horizontal"
class="w-full"
size="lg"
>
<UInput
class="flex-1"
placeholder="分类名称"
v-model="createCatInput"
/>
<UButton
icon="tabler:plus"
color="gray"
label="创建"
:disabled="!createCatInput"
@click="onCreateCat"
/>
</UButtonGroup>
</UFormGroup>
<div class="border dark:border-neutral-700 rounded-md">
<UTable
:columns="[
{ key: 'id', label: 'ID' },
{ key: 'type', label: '分类' },
{ key: 'create_time', label: '创建时间' },
{ key: 'actions' },
]"
:rows="pptCategories?.data.items"
>
<template #create_time-data="{ row }">
{{
dayjs(row.create_time * 1000).format('YYYY-MM-DD HH:mm:ss')
}}
</template>
<template #actions-data="{ row }">
<UButton
color="red"
icon="tabler:trash"
size="xs"
variant="soft"
@click="onDeleteCat(row)"
/>
</template>
</UTable>
</div>
</div>
</UCard>
</USlideover>
</div>
</template>
<style scoped></style>