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

532 lines
15 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.PowerPoint_Category.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.PowerPoint_SysPowerPoint.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.PowerPoint_SysPowerPoint.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: 'success',
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: 'error',
icon: 'i-tabler-alert-triangle',
})
})
}
const onFileSelect = async (files: FileList, type: 'preview' | 'ppt') => {
const url = await useFileGo(files[0]!, 'material')
if (type === 'preview') {
pptCreateState.preview_url = url
} else {
pptCreateState.file_url = url
}
toast.add({
title: '上传成功',
description: `已上传 ${type === 'preview' ? '预览图' : 'PPT 文件'}`,
color: 'success',
icon: 'i-tabler-check',
})
}
const onDeletePPT = (ppt: PPTTemplate) => {
useFetchWrapped<
{ powerpoint_id: number } & AuthedRequest,
BaseResponse<{ code: number }>
>('App.PowerPoint_SysPowerPoint.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: 'success',
icon: 'i-tabler-check',
})
} else {
toast.add({
title: '删除失败',
description: res.msg || '未知错误',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
}
})
.catch(() => {
toast.add({
title: '删除失败',
description: '未知错误',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
})
}
const createCatInput = ref('')
const onCreateCat = () => {
if (createCatInput.value) {
useFetchWrapped<
{ type: string } & AuthedRequest,
BaseResponse<{ ppt_cat_id: number }>
>('App.PowerPoint_SysPowerPoint.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: 'success',
icon: 'i-tabler-check',
})
createCatInput.value = ''
}
})
.catch(() => {
toast.add({
title: '创建失败',
description: '请检查输入是否正确',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
})
}
}
const onDeleteCat = (cat: PPTCategory) => {
useFetchWrapped<
{ ppt_cat_id: number } & AuthedRequest,
BaseResponse<{ code: number }>
>('App.PowerPoint_SysPowerPoint.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: 'success',
icon: 'i-tabler-check',
})
} else {
toast.add({
title: '删除失败',
description: res.msg || '未知错误',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
}
})
.catch(() => {
toast.add({
title: '删除失败',
description: '请检查输入是否正确',
color: 'error',
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="warning"
variant="soft"
icon="tabler:grid"
@click="isCatSlideOpen = true"
/>
<UButton
v-if="loginState.user.auth_code === 2"
label="创建模板"
color="warning"
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="cursor-pointer rounded-lg px-4 py-2 text-sm"
>
{{ cat.type }}
</div>
</div>
<div class="space-y-4">
<div
v-if="pptTemplates?.data.items.length === 0"
class="flex w-full flex-col items-center justify-center gap-2 py-20"
>
<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="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4"
>
<div
v-for="ppt in pptTemplates?.data.items"
:key="ppt.id"
class="relative overflow-hidden rounded-lg bg-white shadow-md"
>
<NuxtImg
:src="ppt.preview_url"
:alt="ppt.title"
class="aspect-video w-full object-cover"
/>
<div
class="bg-linear-to-t absolute inset-x-0 bottom-0 flex items-end justify-between from-black/50 to-transparent p-3 pt-6"
>
<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="error"
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="flex w-full justify-end">
<UPagination
v-if="(pptTemplates?.data.total || 0) > pagination.perpage"
:total="pptTemplates?.data.total"
:page-count="pagination.perpage"
:max="9"
v-model:page="pagination.page"
/>
</div>
</div>
</div>
<USlideover v-model:open="isCreateSlideOpen">
<template #content>
<UCard
:ui="{
body: 'flex-1',
}"
class="flex flex-1 flex-col"
>
<template #header>
<UButton
class="absolute end-5 top-5 z-10 flex"
color="neutral"
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"
>
<UFormField
label="模板标题"
name="title"
>
<UInput v-model="pptCreateState.title" />
</UFormField>
<UFormField
label="模板描述"
name="description"
>
<UTextarea v-model="pptCreateState.description" />
</UFormField>
<UFormField
label="模板分类"
name="type"
>
<USelectMenu
v-model="pptCreateState.type"
:items="selectMenuOptions"
value-key="value"
searchable
searchable-placeholder="搜索现有分类..."
/>
</UFormField>
<UFormField
label="预览图"
name="preview_url"
>
<UniFileDnD
@change="onFileSelect($event, 'preview')"
accept="image/png,image/jpeg"
/>
</UFormField>
<UFormField
label="PPT 文件"
name="file_url"
>
<UniFileDnD
@change="onFileSelect($event, 'ppt')"
accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
/>
</UFormField>
<div class="flex justify-end">
<UButton
label="创建"
color="primary"
type="submit"
/>
</div>
</UForm>
</UCard>
</template>
</USlideover>
<USlideover v-model:open="isCatSlideOpen">
<template #content>
<UCard
:ui="{
body: 'flex-1',
}"
class="flex flex-1 flex-col"
>
<template #header>
<UButton
class="absolute end-5 top-5 z-10 flex"
color="neutral"
icon="tabler:x"
padded
size="sm"
square
variant="ghost"
@click="isCatSlideOpen = false"
/>
PPT 模板分类管理
</template>
<div class="space-y-4">
<UFormField label="创建分类">
<UFieldGroup
orientation="horizontal"
class="w-full"
size="lg"
>
<UInput
class="flex-1"
placeholder="分类名称"
v-model="createCatInput"
/>
<UButton
icon="tabler:plus"
color="neutral"
label="创建"
:disabled="!createCatInput"
@click="onCreateCat"
/>
</UFieldGroup>
</UFormField>
<div class="rounded-md border dark:border-neutral-700">
<UTable
:columns="[
{ accessorKey: 'id', header: 'ID' },
{ accessorKey: 'type', header: '分类' },
{ accessorKey: 'create_time', header: '创建时间' },
{ accessorKey: 'actions' },
]"
:rows="pptCategories?.data.items"
>
<template #create_time-data="{ row }">
{{
dayjs(row.original.create_time * 1000).format('YYYY-MM-DD HH:mm:ss')
}}
</template>
<template #actions-data="{ row }">
<UButton
color="error"
icon="tabler:trash"
size="xs"
variant="soft"
@click="onDeleteCat(row.original)"
/>
</template>
</UTable>
</div>
</div>
</UCard>
</template>
</USlideover>
</div>
</template>
<style scoped></style>