Files
xsh-assistant-next/app/pages/generation/admin/materials.vue

1680 lines
50 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import { object, string, number } from 'yup'
import type { FormSubmitEvent, TableColumn } from '#ui/types'
useHead({
title: '片头片尾管理 | 管理员',
})
const toast = useToast()
const loginState = useLoginState()
// 检查用户权限
if (loginState.user.auth_code !== 2) {
throw createError({
statusCode: 403,
statusMessage: '无权访问管理页面',
})
}
// 状态筛选
const statusFilter = ref<0 | 1>(0) // 0: 待处理, 1: 已完成
// 分页
const pagination = reactive({
page: 1,
pageSize: 20,
})
// 获取用户片头片尾请求列表
// 用户片头请求的原始模板信息类型
interface TitlesTemplateInfo {
create_time: number
opening_url: string
opening_file: string
ending_url: string
ending_file: string
type: number
title: string
description: string
}
// 用户片头请求类型(包含原始模板信息)
type UserTitlesRequest = TitlesTemplate & {
user_id?: number
to_user_id?: number
remark?: string
info?: TitlesTemplateInfo
}
const {
data: titlesListResp,
status: titlesListStatus,
refresh: refreshTitlesList,
} = useAsyncData(
'admin-titles-list',
() =>
useFetchWrapped<
PagedDataRequest & AuthedRequest & { process_status: 0 | 1 },
BaseResponse<PagedData<UserTitlesRequest>>
>('App.User_UserTitles.GetStatusList', {
token: loginState.token!,
user_id: loginState.user.id,
page: pagination.page,
perpage: pagination.pageSize,
process_status: statusFilter.value,
}),
{
watch: [pagination, statusFilter],
}
)
const titlesList = computed(() => titlesListResp.value?.data.items || [])
// 表格列定义
const columns: TableColumn<UserTitlesRequest>[] = [
{
accessorKey: 'id',
header: 'ID',
},
{
accessorKey: 'user_id',
header: '用户ID',
},
{
accessorKey: 'title',
header: '课程名称',
},
{
accessorKey: 'description',
header: '主讲人',
},
{
accessorKey: 'remark',
header: '备注',
},
{
accessorKey: 'info',
header: '原始模板',
},
{
accessorKey: 'create_time',
header: '创建时间',
},
{
accessorKey: 'preview',
header: '制作结果',
},
{
accessorKey: 'actions',
header: '操作',
},
]
// 处理弹窗相关状态
const isProcessModalOpen = ref(false)
const currentTitlesItem = ref<UserTitlesRequest | null>(null)
const processFormState = reactive({
title: '',
description: '',
opening_url: '',
opening_file: '',
ending_url: '',
ending_file: '',
})
const processFormSchema = object({
title: string().required('请输入课程名称'),
description: string().required('请输入主讲人'),
opening_file: string().required('请上传片头视频'),
opening_url: string().required('请上传片头封面'),
ending_file: string().required('请上传片尾视频'),
ending_url: string().required('请上传片尾封面'),
})
const isProcessing = ref(false)
// 片头片尾文件上传相关
const openingVideoFile = ref<File | null>(null)
const openingCoverFile = ref<File | null>(null)
const endingVideoFile = ref<File | null>(null)
const endingCoverFile = ref<File | null>(null)
const isUploadingOpeningVideo = ref(false)
const isUploadingOpeningCover = ref(false)
const isUploadingEndingVideo = ref(false)
const isUploadingEndingCover = ref(false)
// 处理片头片尾请求
const handleProcessTitles = (
item: TitlesTemplate & {
user_id?: number
to_user_id?: number
remark?: string
}
) => {
currentTitlesItem.value = item
// 预填充表单数据
processFormState.title = item.title || ''
processFormState.description = item.description || ''
processFormState.opening_url = item.opening_url || ''
processFormState.opening_file = item.opening_file || ''
processFormState.ending_url = item.ending_url || ''
processFormState.ending_file = item.ending_file || ''
openingVideoFile.value = null
openingCoverFile.value = null
endingVideoFile.value = null
endingCoverFile.value = null
isProcessModalOpen.value = true
}
// 处理文件上传 - 片头视频
const handleOpeningVideoUpload = async (files: FileList) => {
const file = files[0]
if (!file) return
// 验证文件类型
if (!file.type.startsWith('video/')) {
toast.add({
title: '文件格式错误',
description: '请上传视频文件',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
// 验证文件大小 (100MB)
if (file.size > 100 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '视频文件大小不能超过100MB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
try {
isUploadingOpeningVideo.value = true
openingVideoFile.value = file
const uploadUrl = await useFileGo(file, 'material')
processFormState.opening_file = uploadUrl
toast.add({
title: '片头视频上传成功',
color: 'success',
icon: 'i-tabler-check',
})
} catch (error) {
console.error('片头视频上传失败:', error)
toast.add({
title: '上传失败',
description: error instanceof Error ? error.message : '请重试',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isUploadingOpeningVideo.value = false
}
}
// 处理文件上传 - 片头封面
const handleOpeningCoverUpload = async (files: FileList) => {
const file = files[0]
if (!file) return
// 验证文件类型
if (!file.type.startsWith('image/')) {
toast.add({
title: '文件格式错误',
description: '请上传图片文件',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
// 验证文件大小 (10MB)
if (file.size > 10 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '图片文件大小不能超过10MB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
try {
isUploadingOpeningCover.value = true
openingCoverFile.value = file
const uploadUrl = await useFileGo(file, 'material')
processFormState.opening_url = uploadUrl
toast.add({
title: '片头封面上传成功',
color: 'success',
icon: 'i-tabler-check',
})
} catch (error) {
console.error('片头封面上传失败:', error)
toast.add({
title: '上传失败',
description: error instanceof Error ? error.message : '请重试',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isUploadingOpeningCover.value = false
}
}
// 处理文件上传 - 片尾视频
const handleEndingVideoUpload = async (files: FileList) => {
const file = files[0]
if (!file) return
// 验证文件类型
if (!file.type.startsWith('video/')) {
toast.add({
title: '文件格式错误',
description: '请上传视频文件',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
// 验证文件大小 (100MB)
if (file.size > 100 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '视频文件大小不能超过100MB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
try {
isUploadingEndingVideo.value = true
endingVideoFile.value = file
const uploadUrl = await useFileGo(file, 'material')
processFormState.ending_file = uploadUrl
toast.add({
title: '片尾视频上传成功',
color: 'success',
icon: 'i-tabler-check',
})
} catch (error) {
console.error('片尾视频上传失败:', error)
toast.add({
title: '上传失败',
description: error instanceof Error ? error.message : '请重试',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isUploadingEndingVideo.value = false
}
}
// 处理文件上传 - 片尾封面
const handleEndingCoverUpload = async (files: FileList) => {
const file = files[0]
if (!file) return
// 验证文件类型
if (!file.type.startsWith('image/')) {
toast.add({
title: '文件格式错误',
description: '请上传图片文件',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
// 验证文件大小 (10MB)
if (file.size > 10 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '图片文件大小不能超过10MB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
try {
isUploadingEndingCover.value = true
endingCoverFile.value = file
const uploadUrl = await useFileGo(file, 'material')
processFormState.ending_url = uploadUrl
toast.add({
title: '片尾封面上传成功',
color: 'success',
icon: 'i-tabler-check',
})
} catch (error) {
console.error('片尾封面上传失败:', error)
toast.add({
title: '上传失败',
description: error instanceof Error ? error.message : '请重试',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isUploadingEndingCover.value = false
}
}
// 提交处理表单
const onProcessSubmit = async (
event: FormSubmitEvent<typeof processFormState>
) => {
if (!currentTitlesItem.value) return
if (isProcessing.value) return
try {
isProcessing.value = true
const result = await useFetchWrapped<
{
to_user_id: number
user_title_id: number
process_status: 0 | 1
opening_url: string
opening_file: string
ending_url: string
ending_file: string
title: string
description: string
} & AuthedRequest,
BaseResponse<0 | 1>
>('App.User_UserTitles.updateStatus', {
token: loginState.token!,
user_id: loginState.user.id,
to_user_id:
currentTitlesItem.value.to_user_id ||
currentTitlesItem.value.user_id ||
0,
user_title_id: currentTitlesItem.value.id,
process_status: 1, // 标记为已完成
opening_url: event.data.opening_url,
opening_file: event.data.opening_file,
ending_url: event.data.ending_url,
ending_file: event.data.ending_file,
title: event.data.title,
description: event.data.description,
})
if (result.ret === 200 && result.data === 1) {
toast.add({
title: '处理成功',
description: `片头片尾已成功分配给用户 ${currentTitlesItem.value.to_user_id || currentTitlesItem.value.user_id}`,
color: 'success',
icon: 'i-tabler-check',
})
// 重置表单和状态
processFormState.title = ''
processFormState.description = ''
processFormState.opening_url = ''
processFormState.opening_file = ''
processFormState.ending_url = ''
processFormState.ending_file = ''
openingVideoFile.value = null
openingCoverFile.value = null
endingVideoFile.value = null
endingCoverFile.value = null
currentTitlesItem.value = null
isProcessModalOpen.value = false
// 刷新列表
await refreshTitlesList()
} else {
throw new Error(result.msg || '处理失败')
}
} catch (error) {
console.error('处理片头片尾失败:', error)
const errorMessage =
error instanceof Error ? error.message : '处理失败,请重试'
toast.add({
title: '处理失败',
description: errorMessage,
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isProcessing.value = false
}
}
// 删除请求
const handleDeleteTitles = async (
item: TitlesTemplate & { user_id?: number; to_user_id?: number }
) => {
try {
const result = await useFetchWrapped<
{
to_user_id: number
user_title_id: number
} & AuthedRequest,
BaseResponse<{ code: 0 | 1 }>
>('App.User_UserTitles.DeleteConn', {
token: loginState.token!,
user_id: loginState.user.id,
to_user_id: item.to_user_id || item.user_id || 0,
user_title_id: item.id,
})
if (result.ret === 200 && result.data.code === 1) {
toast.add({
title: '删除成功',
description: '片头片尾请求已删除',
color: 'success',
icon: 'i-tabler-check',
})
await refreshTitlesList()
} else {
throw new Error(result.msg || '删除失败')
}
} catch (error) {
console.error('删除片头片尾请求失败:', error)
const errorMessage =
error instanceof Error ? error.message : '删除失败,请重试'
toast.add({
title: '删除失败',
description: errorMessage,
color: 'error',
icon: 'i-tabler-alert-triangle',
})
}
}
// 格式化时间
const formatTime = (timestamp: number) => {
if (!timestamp) return '-'
return new Date(timestamp * 1000).toLocaleString('zh-CN')
}
// 预览视频
const previewVideoUrl = ref('')
const previewVideoTitle = ref('')
const isPreviewModalOpen = ref(false)
const previewVideo = (videoUrl: string, title: string) => {
previewVideoUrl.value = videoUrl
previewVideoTitle.value = title
isPreviewModalOpen.value = true
}
// 预览图片
const previewImageUrl = ref('')
const previewImageTitle = ref('')
const isPreviewImageModalOpen = ref(false)
const previewImage = (imageUrl: string, title: string) => {
previewImageUrl.value = imageUrl
previewImageTitle.value = title
isPreviewImageModalOpen.value = true
}
// ========== 创建系统片头片尾模板 ==========
const isCreateModalOpen = ref(false)
const isCreating = ref(false)
const createFormState = reactive({
title: '',
description: '',
opening_url: '',
opening_file: '',
ending_url: '',
ending_file: '',
type: 0, // 0: 系统自带, 1: 用户自定义
})
const createFormSchema = object({
title: string().required('请输入标题'),
description: string().required('请输入描述'),
opening_file: string().required('请上传片头视频'),
opening_url: string().required('请上传片头封面'),
ending_file: string().required('请上传片尾视频'),
ending_url: string().required('请上传片尾封面'),
type: number().required(),
})
// 创建表单的文件上传状态
const createOpeningVideoFile = ref<File | null>(null)
const createOpeningCoverFile = ref<File | null>(null)
const createEndingVideoFile = ref<File | null>(null)
const createEndingCoverFile = ref<File | null>(null)
const isUploadingCreateOpeningVideo = ref(false)
const isUploadingCreateOpeningCover = ref(false)
const isUploadingCreateEndingVideo = ref(false)
const isUploadingCreateEndingCover = ref(false)
// 打开创建弹窗
const openCreateModal = () => {
createFormState.title = ''
createFormState.description = ''
createFormState.opening_url = ''
createFormState.opening_file = ''
createFormState.ending_url = ''
createFormState.ending_file = ''
createFormState.type = 0
createOpeningVideoFile.value = null
createOpeningCoverFile.value = null
createEndingVideoFile.value = null
createEndingCoverFile.value = null
isCreateModalOpen.value = true
}
// 创建表单的文件上传处理
const handleCreateOpeningVideoUpload = async (files: FileList) => {
const file = files[0]
if (!file) return
if (!file.type.startsWith('video/')) {
toast.add({
title: '文件格式错误',
description: '请上传视频文件',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
if (file.size > 100 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '视频文件大小不能超过100MB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
try {
isUploadingCreateOpeningVideo.value = true
createOpeningVideoFile.value = file
const uploadUrl = await useFileGo(file, 'material')
createFormState.opening_file = uploadUrl
toast.add({
title: '片头视频上传成功',
color: 'success',
icon: 'i-tabler-check',
})
} catch (error) {
console.error('片头视频上传失败:', error)
toast.add({
title: '上传失败',
description: error instanceof Error ? error.message : '请重试',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isUploadingCreateOpeningVideo.value = false
}
}
const handleCreateOpeningCoverUpload = async (files: FileList) => {
const file = files[0]
if (!file) return
if (!file.type.startsWith('image/')) {
toast.add({
title: '文件格式错误',
description: '请上传图片文件',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
if (file.size > 10 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '图片文件大小不能超过10MB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
try {
isUploadingCreateOpeningCover.value = true
createOpeningCoverFile.value = file
const uploadUrl = await useFileGo(file, 'material')
createFormState.opening_url = uploadUrl
toast.add({
title: '片头封面上传成功',
color: 'success',
icon: 'i-tabler-check',
})
} catch (error) {
console.error('片头封面上传失败:', error)
toast.add({
title: '上传失败',
description: error instanceof Error ? error.message : '请重试',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isUploadingCreateOpeningCover.value = false
}
}
const handleCreateEndingVideoUpload = async (files: FileList) => {
const file = files[0]
if (!file) return
if (!file.type.startsWith('video/')) {
toast.add({
title: '文件格式错误',
description: '请上传视频文件',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
if (file.size > 100 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '视频文件大小不能超过100MB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
try {
isUploadingCreateEndingVideo.value = true
createEndingVideoFile.value = file
const uploadUrl = await useFileGo(file, 'material')
createFormState.ending_file = uploadUrl
toast.add({
title: '片尾视频上传成功',
color: 'success',
icon: 'i-tabler-check',
})
} catch (error) {
console.error('片尾视频上传失败:', error)
toast.add({
title: '上传失败',
description: error instanceof Error ? error.message : '请重试',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isUploadingCreateEndingVideo.value = false
}
}
const handleCreateEndingCoverUpload = async (files: FileList) => {
const file = files[0]
if (!file) return
if (!file.type.startsWith('image/')) {
toast.add({
title: '文件格式错误',
description: '请上传图片文件',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
if (file.size > 10 * 1024 * 1024) {
toast.add({
title: '文件过大',
description: '图片文件大小不能超过10MB',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
return
}
try {
isUploadingCreateEndingCover.value = true
createEndingCoverFile.value = file
const uploadUrl = await useFileGo(file, 'material')
createFormState.ending_url = uploadUrl
toast.add({
title: '片尾封面上传成功',
color: 'success',
icon: 'i-tabler-check',
})
} catch (error) {
console.error('片尾封面上传失败:', error)
toast.add({
title: '上传失败',
description: error instanceof Error ? error.message : '请重试',
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isUploadingCreateEndingCover.value = false
}
}
// 提交创建表单
const onCreateSubmit = async (
event: FormSubmitEvent<typeof createFormState>
) => {
if (isCreating.value) return
try {
isCreating.value = true
const result = await useFetchWrapped<
{
opening_url: string
opening_file: string
ending_url: string
ending_file: string
type: number
title: string
description: string
} & AuthedRequest,
BaseResponse<{ title_id: number }>
>('App.Digital_Titles.Create', {
token: loginState.token!,
user_id: loginState.user.id,
opening_url: event.data.opening_url,
opening_file: event.data.opening_file,
ending_url: event.data.ending_url,
ending_file: event.data.ending_file,
type: event.data.type,
title: event.data.title,
description: event.data.description,
})
if (result.ret === 200 && result.data.title_id) {
toast.add({
title: '创建成功',
description: `系统片头片尾模板已创建ID: ${result.data.title_id}`,
color: 'success',
icon: 'i-tabler-check',
})
// 重置表单
createFormState.title = ''
createFormState.description = ''
createFormState.opening_url = ''
createFormState.opening_file = ''
createFormState.ending_url = ''
createFormState.ending_file = ''
createFormState.type = 0
createOpeningVideoFile.value = null
createOpeningCoverFile.value = null
createEndingVideoFile.value = null
createEndingCoverFile.value = null
isCreateModalOpen.value = false
} else {
throw new Error(result.msg || '创建失败')
}
} catch (error) {
console.error('创建片头片尾模板失败:', error)
const errorMessage =
error instanceof Error ? error.message : '创建失败,请重试'
toast.add({
title: '创建失败',
description: errorMessage,
color: 'error',
icon: 'i-tabler-alert-triangle',
})
} finally {
isCreating.value = false
}
}
</script>
<template>
<div>
<div class="p-4 pb-0">
<BubbleTitle
title="片头片尾管理"
subtitle="Materials Management"
>
<template #action>
<div class="flex gap-2">
<UButton
color="primary"
variant="soft"
icon="i-tabler-plus"
label="创建系统模板"
@click="openCreateModal"
/>
<UButton
color="neutral"
variant="soft"
icon="i-tabler-refresh"
label="刷新"
@click="() => refreshTitlesList()"
/>
</div>
</template>
</BubbleTitle>
<GradientDivider />
</div>
<div class="p-4">
<UAlert
icon="i-tabler-user-shield"
title="管理员功能"
description="当前正在管理用户提交的片头片尾制作请求,仅管理员可见"
class="mb-4"
/>
<!-- 状态筛选 -->
<div class="mb-4 flex items-center gap-4">
<span class="text-sm text-gray-600 dark:text-gray-400">状态筛选</span>
<UButtonGroup>
<UButton
:color="statusFilter === 0 ? 'primary' : 'neutral'"
:variant="statusFilter === 0 ? 'solid' : 'ghost'"
label="待处理"
icon="i-tabler-clock"
@click="
() => {
statusFilter = 0
pagination.page = 1
}
"
/>
<UButton
:color="statusFilter === 1 ? 'primary' : 'neutral'"
:variant="statusFilter === 1 ? 'solid' : 'ghost'"
label="已完成"
icon="i-tabler-check"
@click="
() => {
statusFilter = 1
pagination.page = 1
}
"
/>
</UButtonGroup>
<UBadge
v-if="titlesListResp?.data.total"
color="primary"
variant="subtle"
>
{{ titlesListResp.data.total }}
</UBadge>
</div>
<div class="flex flex-col gap-4">
<UTable
:data="titlesList"
:columns="columns"
:loading="titlesListStatus === 'pending'"
loading-color="warning"
loading-animation="carousel"
class="rounded-md border dark:border-neutral-800"
>
<template #create_time-cell="{ row }">
<span class="text-sm">
{{ formatTime(row.original.create_time) }}
</span>
</template>
<template #remark-cell="{ row }">
<span class="block max-w-32 truncate text-sm text-gray-500">
{{ row.original.remark || '-' }}
</span>
</template>
<template #info-cell="{ row }">
<div
v-if="row.original.info"
class="flex items-center gap-2"
>
<img
v-if="row.original.info.opening_url"
:src="row.original.info.opening_url"
:alt="row.original.info.title"
class="rounded-xs h-9 w-16 cursor-pointer object-cover transition-opacity hover:opacity-80"
@click="
previewVideo(
row.original.info.opening_file,
`原始模板: ${row.original.info.title}`
)
"
/>
<div class="flex min-w-0 flex-col">
<span
class="truncate text-xs font-medium text-gray-700 dark:text-gray-300"
:title="row.original.info.title"
>
{{ row.original.info.title }}
</span>
<span class="text-2xs text-gray-500 dark:text-gray-400">
{{ row.original.info.description }}
</span>
</div>
</div>
<span
v-else
class="text-sm text-gray-400"
>
-
</span>
</template>
<template #preview-cell="{ row }">
<div
class="flex items-center gap-3"
v-if="row.original.opening_file || row.original.ending_file"
>
<!-- 片头 -->
<div
v-if="row.original.opening_file"
class="flex flex-col items-center gap-0.5"
>
<img
v-if="row.original.opening_url"
:src="row.original.opening_url"
alt="片头"
class="rounded-xs h-8 w-14 cursor-pointer object-cover ring-1 ring-blue-200 transition-opacity hover:opacity-80 dark:ring-blue-800"
@click="
previewVideo(row.original.opening_file, '制作片头预览')
"
/>
<div
v-else
class="rounded-xs flex h-8 w-14 cursor-pointer items-center justify-center bg-blue-100 transition-opacity hover:opacity-80 dark:bg-blue-900/30"
@click="
previewVideo(row.original.opening_file, '制作片头预览')
"
>
<UIcon
name="i-tabler-player-play"
class="text-blue-500"
/>
</div>
<span class="text-2xs text-blue-600 dark:text-blue-400">
片头
</span>
</div>
<!-- 片尾 -->
<div
v-if="row.original.ending_file"
class="flex flex-col items-center gap-0.5"
>
<img
v-if="row.original.ending_url"
:src="row.original.ending_url"
alt="片尾"
class="rounded-xs h-8 w-14 cursor-pointer object-cover ring-1 ring-green-200 transition-opacity hover:opacity-80 dark:ring-green-800"
@click="
previewVideo(row.original.ending_file, '制作片尾预览')
"
/>
<div
v-else
class="rounded-xs flex h-8 w-14 cursor-pointer items-center justify-center bg-green-100 transition-opacity hover:opacity-80 dark:bg-green-900/30"
@click="
previewVideo(row.original.ending_file, '制作片尾预览')
"
>
<UIcon
name="i-tabler-player-play"
class="text-green-500"
/>
</div>
<span class="text-2xs text-green-600 dark:text-green-400">
片尾
</span>
</div>
</div>
<span
v-else
class="text-sm text-gray-400"
>
未上传
</span>
</template>
<template #actions-cell="{ row }">
<div class="flex gap-2">
<UButton
v-if="statusFilter === 0"
color="warning"
variant="soft"
size="xs"
icon="i-tabler-upload"
label="处理"
@click="handleProcessTitles(row.original)"
/>
<UButton
v-else
color="info"
variant="soft"
size="xs"
icon="i-tabler-edit"
label="编辑"
@click="handleProcessTitles(row.original)"
/>
<UButton
color="error"
variant="soft"
size="xs"
icon="i-tabler-trash"
label="删除"
@click="handleDeleteTitles(row.original)"
/>
</div>
</template>
</UTable>
<div class="flex justify-end">
<UPagination
v-model:page="pagination.page"
:max="9"
:page-count="pagination.pageSize"
:total="titlesListResp?.data.total || 0"
/>
</div>
</div>
</div>
<!-- 处理片头片尾弹窗 -->
<USlideover v-model:open="isProcessModalOpen">
<template #content>
<UCard
:ui="{
body: 'flex-1 overflow-y-auto',
}"
class="flex flex-1 flex-col"
>
<template #header>
<UButton
class="absolute end-5 top-5 z-10 flex"
color="neutral"
icon="i-tabler-x"
padded
size="sm"
square
variant="ghost"
@click="isProcessModalOpen = false"
/>
<div>
<h3 class="text-lg font-semibold">处理片头片尾请求</h3>
<p class="mt-1 text-sm text-gray-500">
为用户
{{
currentTitlesItem?.to_user_id || currentTitlesItem?.user_id
}}
上传制作好的片头片尾视频
</p>
</div>
</template>
<UForm
class="space-y-4"
:schema="processFormSchema"
:state="processFormState"
@submit="onProcessSubmit"
>
<UFormField
label="课程名称"
name="title"
>
<UInput v-model="processFormState.title" />
</UFormField>
<UFormField
label="主讲人"
name="description"
>
<UInput v-model="processFormState.description" />
</UFormField>
<USeparator label="片头" />
<UFormField
label="上传片头视频"
name="opening_file"
required
>
<UniFileDnD
accept="video/mp4,video/webm,video/quicktime,video/x-msvideo,video/x-ms-wmv"
@change="handleOpeningVideoUpload"
>
<template #default>
<div class="py-4 text-center">
<UIcon
v-if="!isUploadingOpeningVideo"
name="i-tabler-video-plus"
class="mx-auto h-12 w-12 text-gray-400"
/>
<UIcon
v-else
name="i-tabler-loader-2"
class="mx-auto h-12 w-12 animate-spin text-gray-400"
/>
<div class="mt-2">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{
isUploadingOpeningVideo
? '上传中...'
: openingVideoFile
? openingVideoFile.name
: '点击或拖拽上传片头视频'
}}
</span>
</div>
<p
v-if="processFormState.opening_file"
class="mx-auto mt-1 max-w-xs truncate text-xs text-green-600"
>
{{ processFormState.opening_file }}
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
<UFormField
label="上传片头封面"
name="opening_url"
required
>
<UniFileDnD
accept="image/png,image/jpeg,image/jpg,image/webp"
@change="handleOpeningCoverUpload"
>
<template #default>
<div class="py-4 text-center">
<UIcon
v-if="!isUploadingOpeningCover"
name="i-tabler-photo-plus"
class="mx-auto h-12 w-12 text-gray-400"
/>
<UIcon
v-else
name="i-tabler-loader-2"
class="mx-auto h-12 w-12 animate-spin text-gray-400"
/>
<div class="mt-2">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{
isUploadingOpeningCover
? '上传中...'
: openingCoverFile
? openingCoverFile.name
: '点击或拖拽上传片头封面'
}}
</span>
</div>
<p
v-if="processFormState.opening_url"
class="mx-auto mt-1 max-w-xs truncate text-xs text-green-600"
>
{{ processFormState.opening_url }}
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
<USeparator label="片尾" />
<UFormField
label="上传片尾视频"
name="ending_file"
required
>
<UniFileDnD
accept="video/mp4,video/webm,video/quicktime,video/x-msvideo,video/x-ms-wmv"
@change="handleEndingVideoUpload"
>
<template #default>
<div class="py-4 text-center">
<UIcon
v-if="!isUploadingEndingVideo"
name="i-tabler-video-plus"
class="mx-auto h-12 w-12 text-gray-400"
/>
<UIcon
v-else
name="i-tabler-loader-2"
class="mx-auto h-12 w-12 animate-spin text-gray-400"
/>
<div class="mt-2">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{
isUploadingEndingVideo
? '上传中...'
: endingVideoFile
? endingVideoFile.name
: '点击或拖拽上传片尾视频'
}}
</span>
</div>
<p
v-if="processFormState.ending_file"
class="mx-auto mt-1 max-w-xs truncate text-xs text-green-600"
>
{{ processFormState.ending_file }}
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
<UFormField
label="上传片尾封面"
name="ending_url"
required
>
<UniFileDnD
accept="image/png,image/jpeg,image/jpg,image/webp"
@change="handleEndingCoverUpload"
>
<template #default>
<div class="py-4 text-center">
<UIcon
v-if="!isUploadingEndingCover"
name="i-tabler-photo-plus"
class="mx-auto h-12 w-12 text-gray-400"
/>
<UIcon
v-else
name="i-tabler-loader-2"
class="mx-auto h-12 w-12 animate-spin text-gray-400"
/>
<div class="mt-2">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{
isUploadingEndingCover
? '上传中...'
: endingCoverFile
? endingCoverFile.name
: '点击或拖拽上传片尾封面'
}}
</span>
</div>
<p
v-if="processFormState.ending_url"
class="mx-auto mt-1 max-w-xs truncate text-xs text-green-600"
>
{{ processFormState.ending_url }}
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
<div class="flex justify-end gap-2 pt-4">
<UButton
type="button"
color="neutral"
variant="soft"
@click="isProcessModalOpen = false"
>
取消
</UButton>
<UButton
type="submit"
color="primary"
:loading="isProcessing"
:disabled="
isProcessing ||
isUploadingOpeningVideo ||
isUploadingOpeningCover ||
isUploadingEndingVideo ||
isUploadingEndingCover
"
>
{{ isProcessing ? '提交中...' : '提交并分配' }}
</UButton>
</div>
</UForm>
</UCard>
</template>
</USlideover>
<!-- 视频预览弹窗 -->
<UModal v-model:open="isPreviewModalOpen">
<template #content>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">{{ previewVideoTitle }}</h3>
<UButton
color="neutral"
icon="i-tabler-x"
variant="ghost"
@click="isPreviewModalOpen = false"
/>
</div>
</template>
<div class="aspect-video">
<video
v-if="previewVideoUrl"
:src="previewVideoUrl"
controls
class="rounded-xs h-full w-full"
/>
</div>
</UCard>
</template>
</UModal>
<!-- 图片预览弹窗 -->
<UModal v-model:open="isPreviewImageModalOpen">
<template #content>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">{{ previewImageTitle }}</h3>
<UButton
color="neutral"
icon="i-tabler-x"
variant="ghost"
@click="isPreviewImageModalOpen = false"
/>
</div>
</template>
<div class="flex justify-center">
<img
v-if="previewImageUrl"
:src="previewImageUrl"
:alt="previewImageTitle"
class="rounded-xs max-h-[70vh] max-w-full object-contain"
/>
</div>
</UCard>
</template>
</UModal>
<!-- 创建系统片头片尾模板弹窗 -->
<USlideover v-model:open="isCreateModalOpen">
<template #content>
<UCard
:ui="{
body: 'flex-1 overflow-y-auto',
footer: 'sticky bottom-0 bg-white dark:bg-gray-900',
}"
class="flex h-full flex-1 flex-col"
>
<template #header>
<UButton
class="absolute end-5 top-5 z-10 flex"
color="neutral"
icon="i-tabler-x"
padded
size="sm"
square
variant="ghost"
@click="isCreateModalOpen = false"
/>
<div>
<h3 class="text-lg font-semibold">创建系统片头片尾模板</h3>
<p class="mt-1 text-sm text-gray-500">
创建新的系统片头片尾模板供所有用户使用
</p>
</div>
</template>
<UForm
id="createForm"
class="space-y-4"
:schema="createFormSchema"
:state="createFormState"
@submit="onCreateSubmit"
>
<UFormField
label="标题"
name="title"
required
>
<UInput
v-model="createFormState.title"
placeholder="请输入模板标题"
/>
</UFormField>
<UFormField
label="描述"
name="description"
required
>
<UTextarea
v-model="createFormState.description"
placeholder="请输入模板描述"
/>
</UFormField>
<USeparator label="片头" />
<UFormField
label="上传片头视频"
name="opening_file"
required
>
<UniFileDnD
accept="video/mp4,video/webm,video/quicktime,video/x-msvideo,video/x-ms-wmv"
@change="handleCreateOpeningVideoUpload"
>
<template #default>
<div class="py-4 text-center">
<UIcon
v-if="!isUploadingCreateOpeningVideo"
name="i-tabler-video-plus"
class="mx-auto h-12 w-12 text-gray-400"
/>
<UIcon
v-else
name="i-tabler-loader-2"
class="mx-auto h-12 w-12 animate-spin text-gray-400"
/>
<div class="mt-2">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{
isUploadingCreateOpeningVideo
? '上传中...'
: createOpeningVideoFile
? createOpeningVideoFile.name
: '点击或拖拽上传片头视频'
}}
</span>
</div>
<p
v-if="createFormState.opening_file"
class="mx-auto mt-1 max-w-xs truncate text-xs text-green-600"
>
{{ createFormState.opening_file }}
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
<UFormField
label="上传片头封面"
name="opening_url"
required
>
<UniFileDnD
accept="image/png,image/jpeg,image/jpg,image/webp"
@change="handleCreateOpeningCoverUpload"
>
<template #default>
<div class="py-4 text-center">
<UIcon
v-if="!isUploadingCreateOpeningCover"
name="i-tabler-photo-plus"
class="mx-auto h-12 w-12 text-gray-400"
/>
<UIcon
v-else
name="i-tabler-loader-2"
class="mx-auto h-12 w-12 animate-spin text-gray-400"
/>
<div class="mt-2">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{
isUploadingCreateOpeningCover
? '上传中...'
: createOpeningCoverFile
? createOpeningCoverFile.name
: '点击或拖拽上传片头封面'
}}
</span>
</div>
<p
v-if="createFormState.opening_url"
class="mx-auto mt-1 max-w-xs truncate text-xs text-green-600"
>
{{ createFormState.opening_url }}
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
<USeparator label="片尾" />
<UFormField
label="上传片尾视频"
name="ending_file"
required
>
<UniFileDnD
accept="video/mp4,video/webm,video/quicktime,video/x-msvideo,video/x-ms-wmv"
@change="handleCreateEndingVideoUpload"
>
<template #default>
<div class="py-4 text-center">
<UIcon
v-if="!isUploadingCreateEndingVideo"
name="i-tabler-video-plus"
class="mx-auto h-12 w-12 text-gray-400"
/>
<UIcon
v-else
name="i-tabler-loader-2"
class="mx-auto h-12 w-12 animate-spin text-gray-400"
/>
<div class="mt-2">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{
isUploadingCreateEndingVideo
? '上传中...'
: createEndingVideoFile
? createEndingVideoFile.name
: '点击或拖拽上传片尾视频'
}}
</span>
</div>
<p
v-if="createFormState.ending_file"
class="mx-auto mt-1 max-w-xs truncate text-xs text-green-600"
>
{{ createFormState.ending_file }}
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
<UFormField
label="上传片尾封面"
name="ending_url"
required
>
<UniFileDnD
accept="image/png,image/jpeg,image/jpg,image/webp"
@change="handleCreateEndingCoverUpload"
>
<template #default>
<div class="py-4 text-center">
<UIcon
v-if="!isUploadingCreateEndingCover"
name="i-tabler-photo-plus"
class="mx-auto h-12 w-12 text-gray-400"
/>
<UIcon
v-else
name="i-tabler-loader-2"
class="mx-auto h-12 w-12 animate-spin text-gray-400"
/>
<div class="mt-2">
<span class="text-sm text-gray-600 dark:text-gray-400">
{{
isUploadingCreateEndingCover
? '上传中...'
: createEndingCoverFile
? createEndingCoverFile.name
: '点击或拖拽上传片尾封面'
}}
</span>
</div>
<p
v-if="createFormState.ending_url"
class="mx-auto mt-1 max-w-xs truncate text-xs text-green-600"
>
{{ createFormState.ending_url }}
</p>
</div>
</template>
</UniFileDnD>
</UFormField>
</UForm>
<template #footer>
<div class="flex justify-end gap-2">
<UButton
type="button"
color="neutral"
variant="soft"
@click="isCreateModalOpen = false"
>
取消
</UButton>
<UButton
type="submit"
form="createForm"
color="primary"
:loading="isCreating"
:disabled="
isCreating ||
isUploadingCreateOpeningVideo ||
isUploadingCreateOpeningCover ||
isUploadingCreateEndingVideo ||
isUploadingCreateEndingCover
"
>
{{ isCreating ? '创建中...' : '创建模板' }}
</UButton>
</div>
</template>
</UCard>
</template>
</USlideover>
</div>
</template>
<style scoped></style>