2
0
Просмотр исходного кода

✨ feat(mobile): 添加视频回放页面

- 创建VideoReplayPage组件实现视频列表展示功能
- 添加视频列表数据获取与处理逻辑
- 实现视频时长、日期和文件大小格式化工具函数
- 添加加载状态、错误状态和空数据状态处理
- 优化视频卡片UI设计,包含缩略图、播放按钮和视频信息
- 在路由配置中添加视频回放页面路由
yourname 6 месяцев назад
Родитель
Сommit
6307c29400
2 измененных файлов с 229 добавлено и 0 удалено
  1. 219 0
      src/client/mobile/pages/VideoReplayPage.tsx
  2. 10 0
      src/client/mobile/routes.tsx

+ 219 - 0
src/client/mobile/pages/VideoReplayPage.tsx

@@ -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;

+ 10 - 0
src/client/mobile/routes.tsx

@@ -18,6 +18,7 @@ import ExamIndex from './components/Exam/ExamIndex';
 import ExamAdmin from './components/Exam/ExamAdmin';
 import ExamCard from './components/Exam/ExamCard';
 import MemberPage from './pages/MemberPage';
+import VideoReplayPage from './pages/VideoReplayPage';
 import { MainLayout } from './layouts/MainLayout';
 import { getGlobalConfig } from '../utils/utils';
 import { UserType } from '@/server/modules/users/user.enum';
@@ -107,6 +108,15 @@ export const router = createBrowserRouter([
           </ProtectedRoute>
         ),
       },
+      {
+        path: 'video-replay',
+        element: (
+          <ProtectedRoute>
+            <VideoReplayPage />
+          </ProtectedRoute>
+        ),
+        errorElement: <ErrorPage />
+      },
       {
         path: '*',
         element: <NotFoundPage />,