1680 lines
50 KiB
Vue
1680 lines
50 KiB
Vue
<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>
|
||
<UFieldGroup>
|
||
<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
|
||
}
|
||
"
|
||
/>
|
||
</UFieldGroup>
|
||
<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>
|