11
Change-Id: Id6671597b5f501cc2c20a5c996c52c389d46938a
diff --git a/Merge/front/src/components/VideoPreview.jsx b/Merge/front/src/components/VideoPreview.jsx
new file mode 100644
index 0000000..9183c11
--- /dev/null
+++ b/Merge/front/src/components/VideoPreview.jsx
@@ -0,0 +1,199 @@
+import React, { useState, useRef } from 'react'
+import { Play, Pause, Volume2, VolumeX, Maximize2, X } from 'lucide-react'
+
+const VideoPreview = ({ src, poster, onClose, className = '', style = {} }) => {
+ const videoRef = useRef(null)
+ const [isPlaying, setIsPlaying] = useState(false)
+ const [isMuted, setIsMuted] = useState(false)
+ const [isFullscreen, setIsFullscreen] = useState(false)
+ const [duration, setDuration] = useState(0)
+ const [currentTime, setCurrentTime] = useState(0)
+
+ 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 toggleFullscreen = () => {
+ if (videoRef.current) {
+ if (!isFullscreen) {
+ if (videoRef.current.requestFullscreen) {
+ videoRef.current.requestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ }
+ }
+ setIsFullscreen(!isFullscreen)
+ }
+ }
+
+ const handleTimeUpdate = () => {
+ if (videoRef.current) {
+ setCurrentTime(videoRef.current.currentTime)
+ }
+ }
+
+ const handleLoadedMetadata = () => {
+ if (videoRef.current) {
+ setDuration(videoRef.current.duration)
+ }
+ }
+
+ const handleSeek = (e) => {
+ if (videoRef.current) {
+ const rect = e.currentTarget.getBoundingClientRect()
+ const clickX = e.clientX - rect.left
+ const newTime = (clickX / rect.width) * duration
+ videoRef.current.currentTime = newTime
+ setCurrentTime(newTime)
+ }
+ }
+
+ const formatTime = (time) => {
+ const minutes = Math.floor(time / 60)
+ const seconds = Math.floor(time % 60)
+ return `${minutes}:${seconds.toString().padStart(2, '0')}`
+ }
+
+ return (
+ <div className={`video-preview ${className}`} style={style}>
+ <div className="video-container" style={{ position: 'relative', borderRadius: 8, overflow: 'hidden' }}>
+ <video
+ ref={videoRef}
+ src={src}
+ poster={poster}
+ onTimeUpdate={handleTimeUpdate}
+ onLoadedMetadata={handleLoadedMetadata}
+ onPlay={() => setIsPlaying(true)}
+ onPause={() => setIsPlaying(false)}
+ style={{ width: '100%', height: '100%', objectFit: 'cover' }}
+ preload="metadata"
+ />
+
+ {/* 视频控制层 */}
+ <div
+ className="video-controls"
+ style={{
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ background: 'linear-gradient(transparent, rgba(0,0,0,0.7))',
+ padding: '20px 12px 12px',
+ opacity: 1,
+ transition: 'opacity 0.3s'
+ }}
+ >
+ {/* 进度条 */}
+ <div
+ className="progress-bar"
+ style={{
+ height: 4,
+ background: 'rgba(255,255,255,0.3)',
+ borderRadius: 2,
+ marginBottom: 8,
+ cursor: 'pointer'
+ }}
+ onClick={handleSeek}
+ >
+ <div
+ style={{
+ height: '100%',
+ background: '#fff',
+ borderRadius: 2,
+ width: `${duration ? (currentTime / duration) * 100 : 0}%`,
+ transition: 'width 0.1s'
+ }}
+ />
+ </div>
+
+ {/* 控制按钮 */}
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+ <button
+ onClick={togglePlay}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'white',
+ cursor: 'pointer',
+ padding: 4,
+ borderRadius: 4
+ }}
+ >
+ {isPlaying ? <Pause size={20} /> : <Play size={20} />}
+ </button>
+
+ <button
+ onClick={toggleMute}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'white',
+ cursor: 'pointer',
+ padding: 4,
+ borderRadius: 4
+ }}
+ >
+ {isMuted ? <VolumeX size={18} /> : <Volume2 size={18} />}
+ </button>
+
+ <span style={{ color: 'white', fontSize: 12 }}>
+ {formatTime(currentTime)} / {formatTime(duration)}
+ </span>
+ </div>
+
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+ <button
+ onClick={toggleFullscreen}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'white',
+ cursor: 'pointer',
+ padding: 4,
+ borderRadius: 4
+ }}
+ >
+ <Maximize2 size={18} />
+ </button>
+
+ {onClose && (
+ <button
+ onClick={onClose}
+ style={{
+ background: 'none',
+ border: 'none',
+ color: 'white',
+ cursor: 'pointer',
+ padding: 4,
+ borderRadius: 4
+ }}
+ >
+ <X size={18} />
+ </button>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+}
+
+export default VideoPreview