|
|
@@ -0,0 +1,599 @@
|
|
|
+import React, { useState } from 'react';
|
|
|
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
+import { format } from 'date-fns';
|
|
|
+import { zhCN } from 'date-fns/locale';
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
+import { useForm } from 'react-hook-form';
|
|
|
+import { toast } from 'sonner';
|
|
|
+import { Plus, Search, Edit, Trash2, Eye, Video, Play, Clock, File } from 'lucide-react';
|
|
|
+
|
|
|
+import { vodVideoClient } from '@/client/api';
|
|
|
+import type { InferResponseType, InferRequestType } from 'hono/client';
|
|
|
+import { VodVideoSchema, CreateVodVideoDto, UpdateVodVideoDto, VodVideoStatus } from '@/server/modules/vod/vod-video.schema';
|
|
|
+import { VodUpload } from '@/client/admin/components/vod/VodUpload';
|
|
|
+
|
|
|
+import { Button } from '@/client/components/ui/button';
|
|
|
+import { Input } from '@/client/components/ui/input';
|
|
|
+import { Badge } from '@/client/components/ui/badge';
|
|
|
+import { Card, CardContent, CardHeader, CardTitle } from '@/client/components/ui/card';
|
|
|
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
|
|
|
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
|
|
|
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
|
|
|
+import { Textarea } from '@/client/components/ui/textarea';
|
|
|
+import { Skeleton } from '@/client/components/ui/skeleton';
|
|
|
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/client/components/ui/alert-dialog';
|
|
|
+
|
|
|
+import { DataTablePagination } from '@/client/admin/components/DataTablePagination';
|
|
|
+
|
|
|
+type VodVideoListResponse = InferResponseType<typeof vodVideoClient.$get, 200>;
|
|
|
+type VodVideoDetailResponse = InferResponseType<typeof vodVideoClient[':id']['$get'], 200>;
|
|
|
+type CreateVodVideoRequest = InferRequestType<typeof vodVideoClient.$post>['json'];
|
|
|
+type UpdateVodVideoRequest = InferRequestType<typeof vodVideoClient[':id']['$put']>['json'];
|
|
|
+
|
|
|
+// 创建表单schema
|
|
|
+const createFormSchema = CreateVodVideoDto;
|
|
|
+const updateFormSchema = UpdateVodVideoDto;
|
|
|
+
|
|
|
+// 视频状态映射
|
|
|
+const videoStatusMap = {
|
|
|
+ [VodVideoStatus.PENDING]: { label: '待处理', color: 'default' },
|
|
|
+ [VodVideoStatus.PROCESSING]: { label: '处理中', color: 'processing' },
|
|
|
+ [VodVideoStatus.COMPLETED]: { label: '已完成', color: 'success' },
|
|
|
+ [VodVideoStatus.FAILED]: { label: '失败', color: 'destructive' }
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化文件大小
|
|
|
+const formatFileSize = (bytes: number): string => {
|
|
|
+ if (bytes === 0) return '0 B';
|
|
|
+ const k = 1024;
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化视频时长
|
|
|
+const formatDuration = (seconds: number): string => {
|
|
|
+ const hours = Math.floor(seconds / 3600);
|
|
|
+ const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
+ const secs = seconds % 60;
|
|
|
+
|
|
|
+ if (hours > 0) {
|
|
|
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
|
+ }
|
|
|
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
|
+};
|
|
|
+
|
|
|
+// VOD视频管理页面
|
|
|
+export const VodVideosPage = () => {
|
|
|
+ const [searchParams, setSearchParams] = useState({
|
|
|
+ page: 1,
|
|
|
+ limit: 10,
|
|
|
+ search: ''
|
|
|
+ });
|
|
|
+ const [isModalOpen, setIsModalOpen] = useState(false);
|
|
|
+ const [isCreateForm, setIsCreateForm] = useState(true);
|
|
|
+ const [editingVideo, setEditingVideo] = useState<any>(null);
|
|
|
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
|
+ const [videoToDelete, setVideoToDelete] = useState<number | null>(null);
|
|
|
+ const [uploadedFileInfo, setUploadedFileInfo] = useState<{
|
|
|
+ fileId: string;
|
|
|
+ videoUrl: string;
|
|
|
+ coverUrl?: string;
|
|
|
+ } | null>(null);
|
|
|
+
|
|
|
+ const queryClient = useQueryClient();
|
|
|
+
|
|
|
+ // 数据查询
|
|
|
+ const { data, isLoading, refetch } = useQuery({
|
|
|
+ queryKey: ['vod-videos', searchParams],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await vodVideoClient.$get({
|
|
|
+ query: {
|
|
|
+ page: searchParams.page,
|
|
|
+ pageSize: searchParams.limit,
|
|
|
+ keyword: searchParams.search
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (res.status !== 200) throw new Error('获取视频列表失败');
|
|
|
+ return await res.json();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建视频
|
|
|
+ const createMutation = useMutation({
|
|
|
+ mutationFn: async (data: CreateVodVideoRequest) => {
|
|
|
+ const res = await vodVideoClient.$post({ json: data });
|
|
|
+ if (res.status !== 201) throw new Error('创建视频失败');
|
|
|
+ return await res.json();
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ toast.success('视频创建成功');
|
|
|
+ setIsModalOpen(false);
|
|
|
+ createForm.reset();
|
|
|
+ setUploadedFileInfo(null);
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['vod-videos'] });
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ toast.error(`创建视频失败: ${error.message}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新视频
|
|
|
+ const updateMutation = useMutation({
|
|
|
+ mutationFn: async ({ id, data }: { id: number; data: UpdateVodVideoRequest }) => {
|
|
|
+ const res = await vodVideoClient[':id']['$put']({
|
|
|
+ param: { id: id.toString() },
|
|
|
+ json: data
|
|
|
+ });
|
|
|
+ if (res.status !== 200) throw new Error('更新视频失败');
|
|
|
+ return await res.json();
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ toast.success('视频更新成功');
|
|
|
+ setIsModalOpen(false);
|
|
|
+ updateForm.reset();
|
|
|
+ setEditingVideo(null);
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['vod-videos'] });
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ toast.error(`更新视频失败: ${error.message}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 删除视频
|
|
|
+ const deleteMutation = useMutation({
|
|
|
+ mutationFn: async (id: number) => {
|
|
|
+ const res = await vodVideoClient[':id']['$delete']({
|
|
|
+ param: { id: id.toString() }
|
|
|
+ });
|
|
|
+ if (res.status !== 204) throw new Error('删除视频失败');
|
|
|
+ return { success: true };
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ toast.success('视频删除成功');
|
|
|
+ setDeleteDialogOpen(false);
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['vod-videos'] });
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ toast.error(`删除视频失败: ${error.message}`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 表单实例
|
|
|
+ const createForm = useForm<CreateVodVideoRequest>({
|
|
|
+ resolver: zodResolver(createFormSchema),
|
|
|
+ defaultValues: {
|
|
|
+ title: '',
|
|
|
+ description: '',
|
|
|
+ fileId: '',
|
|
|
+ videoUrl: '',
|
|
|
+ coverUrl: '',
|
|
|
+ duration: 0,
|
|
|
+ size: 0,
|
|
|
+ status: VodVideoStatus.PENDING
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const updateForm = useForm<UpdateVodVideoRequest>({
|
|
|
+ resolver: zodResolver(updateFormSchema),
|
|
|
+ defaultValues: {
|
|
|
+ title: '',
|
|
|
+ description: '',
|
|
|
+ fileId: '',
|
|
|
+ videoUrl: '',
|
|
|
+ coverUrl: '',
|
|
|
+ duration: 0,
|
|
|
+ size: 0,
|
|
|
+ status: VodVideoStatus.PENDING
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理搜索
|
|
|
+ const handleSearch = (e: React.FormEvent) => {
|
|
|
+ e.preventDefault();
|
|
|
+ refetch();
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理创建视频
|
|
|
+ const handleCreateVideo = () => {
|
|
|
+ setIsCreateForm(true);
|
|
|
+ setEditingVideo(null);
|
|
|
+ createForm.reset();
|
|
|
+ setUploadedFileInfo(null);
|
|
|
+ setIsModalOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理编辑视频
|
|
|
+ const handleEditVideo = (video: any) => {
|
|
|
+ setIsCreateForm(false);
|
|
|
+ setEditingVideo(video);
|
|
|
+ updateForm.reset({
|
|
|
+ title: video.title,
|
|
|
+ description: video.description || '',
|
|
|
+ fileId: video.fileId,
|
|
|
+ videoUrl: video.videoUrl,
|
|
|
+ coverUrl: video.coverUrl || '',
|
|
|
+ duration: video.duration,
|
|
|
+ size: video.size,
|
|
|
+ status: video.status
|
|
|
+ });
|
|
|
+ setIsModalOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理删除视频
|
|
|
+ const handleDeleteVideo = (id: number) => {
|
|
|
+ setVideoToDelete(id);
|
|
|
+ setDeleteDialogOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 确认删除
|
|
|
+ const confirmDelete = () => {
|
|
|
+ if (videoToDelete) {
|
|
|
+ deleteMutation.mutate(videoToDelete);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理表单提交
|
|
|
+ const handleCreateSubmit = (data: CreateVodVideoRequest) => {
|
|
|
+ createMutation.mutate(data);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleUpdateSubmit = (data: UpdateVodVideoRequest) => {
|
|
|
+ if (editingVideo) {
|
|
|
+ updateMutation.mutate({ id: editingVideo.id, data });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 处理上传成功
|
|
|
+ const handleUploadSuccess = (result: { fileId: string; videoUrl: string; coverUrl?: string }) => {
|
|
|
+ setUploadedFileInfo(result);
|
|
|
+ createForm.setValue('fileId', result.fileId);
|
|
|
+ createForm.setValue('videoUrl', result.videoUrl);
|
|
|
+ if (result.coverUrl) {
|
|
|
+ createForm.setValue('coverUrl', result.coverUrl);
|
|
|
+ }
|
|
|
+ toast.success('视频上传成功,请填写其他信息');
|
|
|
+ };
|
|
|
+
|
|
|
+ // 渲染视频状态标签
|
|
|
+ const renderStatusBadge = (status: number) => {
|
|
|
+ const statusInfo = videoStatusMap[status as keyof typeof videoStatusMap] || { label: '未知', color: 'default' };
|
|
|
+ return <Badge variant={statusInfo.color as any}>{statusInfo.label}</Badge>;
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-6">
|
|
|
+ {/* 页面标题和操作按钮 */}
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-bold">视频管理</h1>
|
|
|
+ <p className="text-sm text-muted-foreground">管理VOD视频资源</p>
|
|
|
+ </div>
|
|
|
+ <Button onClick={handleCreateVideo}>
|
|
|
+ <Plus className="h-4 w-4 mr-2" />
|
|
|
+ 新增视频
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 搜索区域 */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="text-lg">搜索</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <form onSubmit={handleSearch} className="flex gap-4">
|
|
|
+ <Input
|
|
|
+ placeholder="搜索标题、描述或文件ID..."
|
|
|
+ value={searchParams.search}
|
|
|
+ onChange={(e) => setSearchParams({ ...searchParams, search: e.target.value })}
|
|
|
+ className="max-w-sm"
|
|
|
+ />
|
|
|
+ <Button type="submit" variant="outline">
|
|
|
+ <Search className="h-4 w-4 mr-2" />
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+ </form>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* 数据表格 */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle className="text-lg">视频列表</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ {isLoading ? (
|
|
|
+ <div className="space-y-4">
|
|
|
+ {Array.from({ length: 5 }).map((_, index) => (
|
|
|
+ <div key={index} className="flex items-center space-x-4">
|
|
|
+ <Skeleton className="h-12 w-12 rounded-full" />
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Skeleton className="h-4 w-[250px]" />
|
|
|
+ <Skeleton className="h-4 w-[200px]" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <Table>
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead>标题</TableHead>
|
|
|
+ <TableHead>描述</TableHead>
|
|
|
+ <TableHead>文件ID</TableHead>
|
|
|
+ <TableHead>状态</TableHead>
|
|
|
+ <TableHead>时长</TableHead>
|
|
|
+ <TableHead>大小</TableHead>
|
|
|
+ <TableHead>创建时间</TableHead>
|
|
|
+ <TableHead>操作</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {data?.data?.length ? (
|
|
|
+ data.data.map((video: any) => (
|
|
|
+ <TableRow key={video.id}>
|
|
|
+ <TableCell className="font-medium">{video.title}</TableCell>
|
|
|
+ <TableCell className="max-w-xs truncate">{video.description || '-'}</TableCell>
|
|
|
+ <TableCell className="font-mono text-sm">{video.fileId}</TableCell>
|
|
|
+ <TableCell>{renderStatusBadge(video.status)}</TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <div className="flex items-center">
|
|
|
+ <Clock className="h-4 w-4 mr-1" />
|
|
|
+ {formatDuration(video.duration)}
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <div className="flex items-center">
|
|
|
+ <File className="h-4 w-4 mr-1" />
|
|
|
+ {formatFileSize(video.size)}
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ {format(new Date(video.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell>
|
|
|
+ <div className="flex space-x-2">
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => handleEditVideo(video)}
|
|
|
+ >
|
|
|
+ <Edit className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => window.open(video.videoUrl, '_blank')}
|
|
|
+ >
|
|
|
+ <Play className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ variant="destructive"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => handleDeleteVideo(video.id)}
|
|
|
+ >
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ))
|
|
|
+ ) : (
|
|
|
+ <TableRow>
|
|
|
+ <TableCell colSpan={8} className="text-center text-muted-foreground">
|
|
|
+ 暂无数据
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ )}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+
|
|
|
+ {/* 分页控件 */}
|
|
|
+ {data?.pagination && (
|
|
|
+ <DataTablePagination
|
|
|
+ currentPage={data.pagination.current}
|
|
|
+ pageSize={data.pagination.pageSize}
|
|
|
+ totalCount={data.pagination.total}
|
|
|
+ onPageChange={(page, limit) => setSearchParams({ ...searchParams, page, limit })}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* 创建/编辑模态框 */}
|
|
|
+ <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
|
+ <DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
|
|
+ <DialogHeader>
|
|
|
+ <DialogTitle>
|
|
|
+ {isCreateForm ? '创建视频' : '编辑视频'}
|
|
|
+ </DialogTitle>
|
|
|
+ <DialogDescription>
|
|
|
+ {isCreateForm ? '创建一个新的视频记录' : '编辑现有视频信息'}
|
|
|
+ </DialogDescription>
|
|
|
+ </DialogHeader>
|
|
|
+
|
|
|
+ {isCreateForm ? (
|
|
|
+ // 创建表单
|
|
|
+ <Form {...createForm}>
|
|
|
+ <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
|
|
|
+ {/* 视频上传组件 */}
|
|
|
+ <VodUpload
|
|
|
+ onUploadSuccess={handleUploadSuccess}
|
|
|
+ onUploadError={(error) => toast.error(`上传失败: ${error.message}`)}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* 标题字段 */}
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="title"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 视频标题
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入视频标题..." {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormDescription>视频的标题信息</FormDescription>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* 描述字段 */}
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="description"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>视频描述</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Textarea
|
|
|
+ placeholder="请输入视频描述..."
|
|
|
+ className="resize-none"
|
|
|
+ {...field}
|
|
|
+ value={field.value || ''}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormDescription>视频的详细描述信息(可选)</FormDescription>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* 隐藏字段 */}
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="fileId"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem className="hidden">
|
|
|
+ <FormControl>
|
|
|
+ <Input {...field} />
|
|
|
+ </FormControl>
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="videoUrl"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem className="hidden">
|
|
|
+ <FormControl>
|
|
|
+ <Input {...field} />
|
|
|
+ </FormControl>
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <FormField
|
|
|
+ control={createForm.control}
|
|
|
+ name="coverUrl"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem className="hidden">
|
|
|
+ <FormControl>
|
|
|
+ <Input {...field} value={field.value || ''} />
|
|
|
+ </FormControl>
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <DialogFooter>
|
|
|
+ <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="submit"
|
|
|
+ disabled={!uploadedFileInfo || createMutation.isPending}
|
|
|
+ >
|
|
|
+ {createMutation.isPending ? '创建中...' : '创建'}
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </form>
|
|
|
+ </Form>
|
|
|
+ ) : (
|
|
|
+ // 编辑表单
|
|
|
+ <Form {...updateForm}>
|
|
|
+ <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
|
|
|
+ {/* 标题字段 */}
|
|
|
+ <FormField
|
|
|
+ control={updateForm.control}
|
|
|
+ name="title"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel className="flex items-center">
|
|
|
+ 视频标题
|
|
|
+ <span className="text-red-500 ml-1">*</span>
|
|
|
+ </FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Input placeholder="请输入视频标题..." {...field} />
|
|
|
+ </FormControl>
|
|
|
+ <FormDescription>视频的标题信息</FormDescription>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* 描述字段 */}
|
|
|
+ <FormField
|
|
|
+ control={updateForm.control}
|
|
|
+ name="description"
|
|
|
+ render={({ field }) => (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>视频描述</FormLabel>
|
|
|
+ <FormControl>
|
|
|
+ <Textarea
|
|
|
+ placeholder="请输入视频描述..."
|
|
|
+ className="resize-none"
|
|
|
+ {...field}
|
|
|
+ value={field.value || ''}
|
|
|
+ />
|
|
|
+ </FormControl>
|
|
|
+ <FormDescription>视频的详细描述信息(可选)</FormDescription>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ )}
|
|
|
+ />
|
|
|
+
|
|
|
+ <DialogFooter>
|
|
|
+ <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
|
|
|
+ 取消
|
|
|
+ </Button>
|
|
|
+ <Button type="submit" disabled={updateMutation.isPending}>
|
|
|
+ {updateMutation.isPending ? '更新中...' : '更新'}
|
|
|
+ </Button>
|
|
|
+ </DialogFooter>
|
|
|
+ </form>
|
|
|
+ </Form>
|
|
|
+ )}
|
|
|
+ </DialogContent>
|
|
|
+ </Dialog>
|
|
|
+
|
|
|
+ {/* 删除确认对话框 */}
|
|
|
+ <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
|
+ <AlertDialogContent>
|
|
|
+ <AlertDialogHeader>
|
|
|
+ <AlertDialogTitle>确认删除</AlertDialogTitle>
|
|
|
+ <AlertDialogDescription>
|
|
|
+ 确定要删除这个视频吗?此操作无法撤销,视频文件将无法恢复。
|
|
|
+ </AlertDialogDescription>
|
|
|
+ </AlertDialogHeader>
|
|
|
+ <AlertDialogFooter>
|
|
|
+ <AlertDialogCancel>取消</AlertDialogCancel>
|
|
|
+ <AlertDialogAction
|
|
|
+ onClick={confirmDelete}
|
|
|
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
|
+ disabled={deleteMutation.isPending}
|
|
|
+ >
|
|
|
+ {deleteMutation.isPending ? '删除中...' : '删除'}
|
|
|
+ </AlertDialogAction>
|
|
|
+ </AlertDialogFooter>
|
|
|
+ </AlertDialogContent>
|
|
|
+ </AlertDialog>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|