|
|
@@ -0,0 +1,257 @@
|
|
|
+import React, { useState, useEffect, useRef } from 'react';
|
|
|
+import { useParams, useNavigate } from 'react-router';
|
|
|
+import { ArrowLeft, Play, Pause, Volume2, VolumeX, Maximize, Settings } from 'lucide-react';
|
|
|
+import { vodVideoClient } from '@/client/api';
|
|
|
+import type { InferResponseType } from 'hono/client';
|
|
|
+
|
|
|
+// 定义视频响应类型
|
|
|
+type VodVideoResponse = InferResponseType<typeof vodVideoClient.$get, 200>['data'][0];
|
|
|
+
|
|
|
+const VideoPlayerPage: React.FC = () => {
|
|
|
+ const { id } = useParams<{ id: string }>();
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
+
|
|
|
+ const [video, setVideo] = useState<VodVideoResponse | null>(null);
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [error, setError] = useState<string>('');
|
|
|
+ const [isPlaying, setIsPlaying] = useState(false);
|
|
|
+ const [isMuted, setIsMuted] = useState(false);
|
|
|
+ const [currentTime, setCurrentTime] = useState(0);
|
|
|
+ const [duration, setDuration] = useState(0);
|
|
|
+ const [showControls, setShowControls] = useState(true);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (id) {
|
|
|
+ fetchVideo();
|
|
|
+ }
|
|
|
+ }, [id]);
|
|
|
+
|
|
|
+ const fetchVideo = async () => {
|
|
|
+ try {
|
|
|
+ setLoading(true);
|
|
|
+ const response = await vodVideoClient[':id'].$get({
|
|
|
+ param: { id }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (response.status === 200) {
|
|
|
+ const data = await response.json();
|
|
|
+ setVideo(data);
|
|
|
+ } else {
|
|
|
+ setError('获取视频信息失败');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('获取视频信息错误:', err);
|
|
|
+ setError('网络请求失败');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const togglePlay = () => {
|
|
|
+ if (videoRef.current) {
|
|
|
+ if (isPlaying) {
|
|
|
+ videoRef.current.pause();
|
|
|
+ } else {
|
|
|
+ videoRef.current.play();
|
|
|
+ }
|
|
|
+ setIsPlaying(!isPlaying);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const toggleMute = () => {
|
|
|
+ if (videoRef.current) {
|
|
|
+ videoRef.current.muted = !isMuted;
|
|
|
+ setIsMuted(!isMuted);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleTimeUpdate = () => {
|
|
|
+ if (videoRef.current) {
|
|
|
+ setCurrentTime(videoRef.current.currentTime);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleLoadedMetadata = () => {
|
|
|
+ if (videoRef.current) {
|
|
|
+ setDuration(videoRef.current.duration);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSeek = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
|
+ if (videoRef.current && duration > 0) {
|
|
|
+ const progressBar = e.currentTarget;
|
|
|
+ const rect = progressBar.getBoundingClientRect();
|
|
|
+ const percent = (e.clientX - rect.left) / rect.width;
|
|
|
+ const newTime = percent * duration;
|
|
|
+
|
|
|
+ videoRef.current.currentTime = newTime;
|
|
|
+ setCurrentTime(newTime);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatTime = (time: number): string => {
|
|
|
+ const minutes = Math.floor(time / 60);
|
|
|
+ const seconds = Math.floor(time % 60);
|
|
|
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleFullscreen = () => {
|
|
|
+ if (videoRef.current) {
|
|
|
+ if (videoRef.current.requestFullscreen) {
|
|
|
+ videoRef.current.requestFullscreen();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if (loading) {
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-black flex items-center justify-center">
|
|
|
+ <div className="text-white">加载中...</div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error || !video) {
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-black flex items-center justify-center">
|
|
|
+ <div className="text-white text-center">
|
|
|
+ <p>{error || '视频不存在'}</p>
|
|
|
+ <button
|
|
|
+ onClick={() => navigate('/mobile/video-replay')}
|
|
|
+ className="mt-4 bg-primary text-primary-foreground px-4 py-2 rounded-md"
|
|
|
+ >
|
|
|
+ 返回视频列表
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-black">
|
|
|
+ {/* 顶部导航栏 */}
|
|
|
+ <div className="fixed top-0 left-0 right-0 z-50 bg-black/50 backdrop-blur-sm p-4">
|
|
|
+ <div className="flex items-center">
|
|
|
+ <button
|
|
|
+ onClick={() => navigate('/mobile/video-replay')}
|
|
|
+ className="text-white p-2 rounded-full hover:bg-white/10"
|
|
|
+ >
|
|
|
+ <ArrowLeft className="w-6 h-6" />
|
|
|
+ </button>
|
|
|
+ <h1 className="text-white text-sm font-medium ml-4 line-clamp-1 flex-1">
|
|
|
+ {video.title}
|
|
|
+ </h1>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 视频播放器 */}
|
|
|
+ <div
|
|
|
+ className="w-full h-screen flex items-center justify-center bg-black"
|
|
|
+ onClick={() => setShowControls(!showControls)}
|
|
|
+ >
|
|
|
+ <video
|
|
|
+ ref={videoRef}
|
|
|
+ className="w-full h-full object-contain"
|
|
|
+ src={video.videoUrl}
|
|
|
+ onTimeUpdate={handleTimeUpdate}
|
|
|
+ onLoadedMetadata={handleLoadedMetadata}
|
|
|
+ onPlay={() => setIsPlaying(true)}
|
|
|
+ onPause={() => setIsPlaying(false)}
|
|
|
+ onEnded={() => setIsPlaying(false)}
|
|
|
+ autoPlay
|
|
|
+ playsInline
|
|
|
+ />
|
|
|
+
|
|
|
+ {/* 视频控制栏 */}
|
|
|
+ {showControls && (
|
|
|
+ <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4">
|
|
|
+ {/* 进度条 */}
|
|
|
+ <div
|
|
|
+ className="w-full h-1 bg-white/30 rounded-full mb-4 cursor-pointer"
|
|
|
+ onClick={handleSeek}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className="h-full bg-primary rounded-full"
|
|
|
+ style={{ width: `${(currentTime / duration) * 100}%` }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 控制按钮 */}
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div className="flex items-center space-x-4">
|
|
|
+ <button
|
|
|
+ onClick={togglePlay}
|
|
|
+ className="text-white p-2 rounded-full hover:bg-white/10"
|
|
|
+ >
|
|
|
+ {isPlaying ? (
|
|
|
+ <Pause className="w-6 h-6" />
|
|
|
+ ) : (
|
|
|
+ <Play className="w-6 h-6" />
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button
|
|
|
+ onClick={toggleMute}
|
|
|
+ className="text-white p-2 rounded-full hover:bg-white/10"
|
|
|
+ >
|
|
|
+ {isMuted ? (
|
|
|
+ <VolumeX className="w-5 h-5" />
|
|
|
+ ) : (
|
|
|
+ <Volume2 className="w-5 h-5" />
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <span className="text-white text-sm">
|
|
|
+ {formatTime(currentTime)} / {formatTime(duration)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-center space-x-2">
|
|
|
+ <button
|
|
|
+ onClick={handleFullscreen}
|
|
|
+ className="text-white p-2 rounded-full hover:bg-white/10"
|
|
|
+ >
|
|
|
+ <Maximize className="w-5 h-5" />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 播放/暂停按钮 */}
|
|
|
+ {!showControls && (
|
|
|
+ <button
|
|
|
+ onClick={togglePlay}
|
|
|
+ className="absolute inset-0 flex items-center justify-center"
|
|
|
+ >
|
|
|
+ {!isPlaying && (
|
|
|
+ <div className="bg-black/50 rounded-full p-4">
|
|
|
+ <Play className="w-12 h-12 text-white" fill="white" />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 视频信息 */}
|
|
|
+ <div className="bg-background p-4">
|
|
|
+ <h2 className="text-xl font-bold text-foreground mb-2">
|
|
|
+ {video.title}
|
|
|
+ </h2>
|
|
|
+
|
|
|
+ {video.description && (
|
|
|
+ <p className="text-muted-foreground mb-4">
|
|
|
+ {video.description}
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+
|
|
|
+ <div className="text-sm text-muted-foreground">
|
|
|
+ <p>上传时间: {new Date(video.createdAt).toLocaleDateString('zh-CN')}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default VideoPlayerPage;
|