|
|
@@ -0,0 +1,219 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { Play, Clock, Eye, Calendar, Video } from 'lucide-react';
|
|
|
+import { vodVideoClient } from '@/client/api';
|
|
|
+import type { InferResponseType } from 'hono/client';
|
|
|
+
|
|
|
+// 定义视频响应类型
|
|
|
+type VodVideoResponse = InferResponseType<typeof vodVideoClient.$get, 200>['data'][0];
|
|
|
+
|
|
|
+const VideoReplayPage: React.FC = () => {
|
|
|
+ const [videos, setVideos] = useState<VodVideoResponse[]>([]);
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [error, setError] = useState<string>('');
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ fetchVideos();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const fetchVideos = async () => {
|
|
|
+ try {
|
|
|
+ setLoading(true);
|
|
|
+ const response = await vodVideoClient.$get({
|
|
|
+ query: {
|
|
|
+ page: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.status === 200) {
|
|
|
+ const data = await response.json();
|
|
|
+ setVideos(data.data);
|
|
|
+ } else {
|
|
|
+ setError('获取视频列表失败');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('获取视频列表错误:', err);
|
|
|
+ setError('网络请求失败');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatDuration = (seconds: number): string => {
|
|
|
+ const hours = Math.floor(seconds / 3600);
|
|
|
+ const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
+ const remainingSeconds = seconds % 60;
|
|
|
+
|
|
|
+ if (hours > 0) {
|
|
|
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
|
+ }
|
|
|
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatDate = (dateString: string): string => {
|
|
|
+ const date = new Date(dateString);
|
|
|
+ return date.toLocaleDateString('zh-CN', {
|
|
|
+ year: 'numeric',
|
|
|
+ month: '2-digit',
|
|
|
+ day: '2-digit'
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatFileSize = (bytes: number): string => {
|
|
|
+ if (bytes >= 1024 * 1024 * 1024) {
|
|
|
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
|
+ } else if (bytes >= 1024 * 1024) {
|
|
|
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
|
+ } else if (bytes >= 1024) {
|
|
|
+ return `${(bytes / 1024).toFixed(1)} KB`;
|
|
|
+ }
|
|
|
+ return `${bytes} B`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleVideoClick = (video: VodVideoResponse) => {
|
|
|
+ // 处理视频点击,这里可以跳转到视频播放页面
|
|
|
+ console.log('播放视频:', video.id, video.videoUrl);
|
|
|
+ // 在实际应用中,可以打开视频播放器或跳转到播放页面
|
|
|
+ if (video.videoUrl) {
|
|
|
+ window.open(video.videoUrl, '_blank');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if (loading) {
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-background p-4">
|
|
|
+ <div className="max-w-2xl mx-auto">
|
|
|
+ <h1 className="text-2xl font-bold mb-6">视频回放</h1>
|
|
|
+ <div className="space-y-4">
|
|
|
+ {[1, 2, 3, 4, 5].map((item) => (
|
|
|
+ <div key={item} className="bg-card rounded-lg p-4 animate-pulse">
|
|
|
+ <div className="flex space-x-4">
|
|
|
+ <div className="w-32 h-20 bg-muted rounded-md"></div>
|
|
|
+ <div className="flex-1 space-y-2">
|
|
|
+ <div className="h-4 bg-muted rounded"></div>
|
|
|
+ <div className="h-3 bg-muted rounded w-3/4"></div>
|
|
|
+ <div className="h-3 bg-muted rounded w-1/2"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error) {
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-background p-4">
|
|
|
+ <div className="max-w-2xl mx-auto">
|
|
|
+ <h1 className="text-2xl font-bold mb-6">视频回放</h1>
|
|
|
+ <div className="text-center py-12 text-destructive">
|
|
|
+ <p>{error}</p>
|
|
|
+ <button
|
|
|
+ onClick={fetchVideos}
|
|
|
+ className="mt-4 bg-primary text-primary-foreground px-4 py-2 rounded-md"
|
|
|
+ >
|
|
|
+ 重试
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-background p-4">
|
|
|
+ <div className="max-w-2xl mx-auto">
|
|
|
+ <h1 className="text-2xl font-bold mb-6 text-foreground">视频回放</h1>
|
|
|
+
|
|
|
+ <div className="space-y-4">
|
|
|
+ {videos.map((video) => (
|
|
|
+ <div
|
|
|
+ key={video.id}
|
|
|
+ className="bg-card rounded-lg p-4 border border-border hover:border-primary/50 transition-colors cursor-pointer"
|
|
|
+ onClick={() => handleVideoClick(video)}
|
|
|
+ >
|
|
|
+ <div className="flex space-x-4">
|
|
|
+ {/* 视频缩略图 */}
|
|
|
+ <div className="relative">
|
|
|
+ <div className="w-32 h-20 bg-muted rounded-md overflow-hidden">
|
|
|
+ {video.coverUrl ? (
|
|
|
+ <img
|
|
|
+ src={video.coverUrl}
|
|
|
+ alt={video.title}
|
|
|
+ className="w-full h-full object-cover"
|
|
|
+ onError={(e) => {
|
|
|
+ (e.target as HTMLImageElement).src = '/api/placeholder/300/200';
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <div className="w-full h-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
|
|
|
+ <Video className="w-8 h-8 text-white" />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className="absolute inset-0 bg-black/40 flex items-center justify-center">
|
|
|
+ <Play className="w-6 h-6 text-white" fill="white" />
|
|
|
+ </div>
|
|
|
+ {video.duration > 0 && (
|
|
|
+ <div className="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-1 py-0.5 rounded">
|
|
|
+ {formatDuration(video.duration)}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 视频信息 */}
|
|
|
+ <div className="flex-1 min-w-0">
|
|
|
+ <h3 className="font-medium text-foreground line-clamp-2 mb-1">
|
|
|
+ {video.title}
|
|
|
+ </h3>
|
|
|
+
|
|
|
+ {video.description && (
|
|
|
+ <p className="text-sm text-muted-foreground line-clamp-2 mb-2">
|
|
|
+ {video.description}
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className="flex items-center space-x-4 text-xs text-muted-foreground">
|
|
|
+ {video.duration > 0 && (
|
|
|
+ <div className="flex items-center">
|
|
|
+ <Clock className="w-3 h-3 mr-1" />
|
|
|
+ <span>{formatDuration(video.duration)}</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {video.size > 0 && (
|
|
|
+ <div className="flex items-center">
|
|
|
+ <span>{formatFileSize(video.size)}</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {video.createdAt && (
|
|
|
+ <div className="flex items-center">
|
|
|
+ <Calendar className="w-3 h-3 mr-1" />
|
|
|
+ <span>{formatDate(video.createdAt)}</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {videos.length === 0 && (
|
|
|
+ <div className="text-center py-12">
|
|
|
+ <div className="text-muted-foreground mb-4">
|
|
|
+ <Video className="w-12 h-12 mx-auto mb-2" />
|
|
|
+ <p>暂无视频内容</p>
|
|
|
+ <p className="text-sm mt-2">管理员上传视频后即可在此查看</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default VideoReplayPage;
|