Commit 4b5110b9471dd9a824c99053e2371871cc4c3b14

Authored by 史婷婷
1 parent f5fc8d3d

feat: 徽智制联-deepSeek-初版

  1 +.videoPlayer {
  2 + position: relative;
  3 + width: 100%;
  4 + max-width: 800px;
  5 + margin: 0 auto;
  6 + border-radius: 8px;
  7 + overflow: hidden;
  8 +}
  9 +
  10 +.video {
  11 + width: 100%;
  12 + display: block;
  13 +}
  14 +
  15 +.controls {
  16 + position: absolute;
  17 + bottom: 0;
  18 + left: 0;
  19 + right: 0;
  20 + width: 100%;
  21 + height: 100%;
  22 + display: flex;
  23 + flex-direction: column;
  24 + justify-content: flex-end;
  25 + transition: opacity 0.3s ease;
  26 +}
  27 +
  28 +.controls.hidden {
  29 + opacity: 0;
  30 +}
  31 +
  32 +.controls.visible {
  33 + opacity: 1;
  34 +}
  35 +
  36 +.overlay {
  37 + background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, transparent 100%);
  38 + padding: 20px;
  39 + display: flex;
  40 + flex-direction: column;
  41 +}
  42 +
  43 +.progressBarContainer {
  44 + width: 100%;
  45 + margin-bottom: 10px;
  46 +}
  47 +
  48 +.controlsContent {
  49 + display: flex;
  50 + justify-content: space-between;
  51 + align-items: center;
  52 +}
  53 +
  54 +.leftControls, .rightControls {
  55 + display: flex;
  56 + align-items: center;
  57 +}
  58 +
  59 +.playPauseButton, .muteButton, .fullscreenButton {
  60 + background: none;
  61 + border: none;
  62 + color: white;
  63 + cursor: pointer;
  64 + padding: 4px;
  65 + margin-right: 10px;
  66 + display: flex;
  67 + align-items: center;
  68 + justify-content: center;
  69 +}
  70 +
  71 +.playPauseButton:hover, .muteButton:hover, .fullscreenButton:hover {
  72 + background-color: rgba(255, 255, 255, 0.1);
  73 + border-radius: 50%;
  74 +}
  75 +
  76 +.time {
  77 + color: white;
  78 + font-size: 14px;
  79 + margin-left: 8px;
  80 +}
  81 +
  82 +.volumeControl {
  83 + display: flex;
  84 + align-items: center;
  85 + margin-right: 16px;
  86 +}
  87 +
  88 +.volumeSlider {
  89 + width: 60px;
  90 + height: 4px;
  91 + background: rgba(255, 255, 255, 0.3);
  92 + border-radius: 2px;
  93 + cursor: pointer;
  94 + margin-left: 12px;
  95 + position: relative;
  96 +}
  97 +
  98 +.volumeLevel {
  99 + position: absolute;
  100 + top: 0;
  101 + left: 0;
  102 + height: 100%;
  103 + background: #ffffff;
  104 + border-radius: 2px;
  105 +}
  106 +
  107 +.progressBar {
  108 + position: relative;
  109 + width: 100%;
  110 + height: 4px;
  111 + background: rgba(255, 255, 255, 0.3);
  112 + cursor: pointer;
  113 + border-radius: 2px;
  114 + overflow: visible;
  115 + transition: height 0.2s ease;
  116 +}
  117 +
  118 +.progressBar:hover {
  119 + height: 6px;
  120 +}
  121 +
  122 +.progress {
  123 + height: 100%;
  124 + background: #ffffff;
  125 + transition: width 0.1s ease-in-out;
  126 +}
  127 +
  128 +.hoverTimeIndicator {
  129 + position: absolute;
  130 + bottom: 100%;
  131 + transform: translateX(-50%);
  132 + background-color: rgba(0, 0, 0, 0.7);
  133 + color: white;
  134 + padding: 4px 8px;
  135 + border-radius: 4px;
  136 + font-size: 12px;
  137 + pointer-events: none;
  138 + white-space: nowrap;
  139 + margin-bottom: 8px;
  140 +}
  141 +
  142 +.hoverTimeIndicator::after {
  143 + content: '';
  144 + position: absolute;
  145 + top: 100%;
  146 + left: 50%;
  147 + margin-left: -4px;
  148 + border-width: 4px;
  149 + border-style: solid;
  150 + border-color: rgba(0, 0, 0, 0.7) transparent transparent transparent;
  151 +}
  152 +
  153 +.controls.smallSize .controlsContent {
  154 + justify-content: space-between;
  155 +}
  156 +
  157 +.controls.smallSize .leftControls,
  158 +.controls.smallSize .rightControls {
  159 + flex: 0 0 auto;
  160 + display: flex;
  161 + align-items: center;
  162 +}
  163 +
  164 +.controls.smallSize .rightControls {
  165 + justify-content: flex-end;
  166 +}
  167 +
  168 +.controls.smallSize .progressBarContainer {
  169 + margin-bottom: 4px;
  170 +}
  171 +
  172 +.controls.smallSize .playPauseButton,
  173 +.controls.smallSize .muteButton,
  174 +.controls.smallSize .fullscreenButton {
  175 + padding: 2px;
  176 + margin-right: 4px;
  177 +}
  178 +
  179 +.controls.smallSize .playPauseButton svg,
  180 +.controls.smallSize .muteButton svg,
  181 +.controls.smallSize .fullscreenButton svg {
  182 + width: 16px;
  183 + height: 16px;
  184 +}
  185 +
  186 +.controls.smallSize .muteButton {
  187 + order: -1;
  188 +}
  1 +import React, { useCallback, useEffect, useRef, useState } from 'react'
  2 +import styles from './VideoPlayer.module.css'
  3 +
  4 +type VideoPlayerProps = {
  5 + src: string
  6 +}
  7 +
  8 +const PlayIcon = () => (
  9 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  10 + <path d="M8 5V19L19 12L8 5Z" fill="currentColor"/>
  11 + </svg>
  12 +)
  13 +
  14 +const PauseIcon = () => (
  15 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  16 + <path d="M6 19H10V5H6V19ZM14 5V19H18V5H14Z" fill="currentColor"/>
  17 + </svg>
  18 +)
  19 +
  20 +const MuteIcon = () => (
  21 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  22 + <path d="M3 9V15H7L12 20V4L7 9H3ZM16.5 12C16.5 10.23 15.48 8.71 14 7.97V16.02C15.48 15.29 16.5 13.77 16.5 12ZM14 3.23V5.29C16.89 6.15 19 8.83 19 12C19 15.17 16.89 17.85 14 18.71V20.77C18.01 19.86 21 16.28 21 12C21 7.72 18.01 4.14 14 3.23Z" fill="currentColor"/>
  23 + </svg>
  24 +)
  25 +
  26 +const UnmuteIcon = () => (
  27 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  28 + <path d="M4.34 2.93L2.93 4.34L7.29 8.7L7 9H3V15H7L12 20V13.41L16.18 17.59C15.69 17.96 15.16 18.27 14.58 18.5V20.58C15.94 20.22 17.15 19.56 18.13 18.67L19.66 20.2L21.07 18.79L4.34 2.93ZM10 15.17L7.83 13H5V11H7.83L10 8.83V15.17ZM19 12C19 12.82 18.85 13.61 18.59 14.34L20.12 15.87C20.68 14.7 21 13.39 21 12C21 7.72 18.01 4.14 14 3.23V5.29C16.89 6.15 19 8.83 19 12ZM12 4L10.12 5.88L12 7.76V4ZM16.5 12C16.5 10.23 15.48 8.71 14 7.97V10.18L16.45 12.63C16.48 12.43 16.5 12.22 16.5 12Z" fill="currentColor"/>
  29 + </svg>
  30 +)
  31 +
  32 +const FullscreenIcon = () => (
  33 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  34 + <path d="M7 14H5V19H10V17H7V14ZM5 10H7V7H10V5H5V10ZM17 17H14V19H19V14H17V17ZM14 5V7H17V10H19V5H14Z" fill="currentColor"/>
  35 + </svg>
  36 +)
  37 +
  38 +const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
  39 + const [isPlaying, setIsPlaying] = useState(false)
  40 + const [currentTime, setCurrentTime] = useState(0)
  41 + const [duration, setDuration] = useState(0)
  42 + const [isMuted, setIsMuted] = useState(false)
  43 + const [volume, setVolume] = useState(1)
  44 + const [isDragging, setIsDragging] = useState(false)
  45 + const [isControlsVisible, setIsControlsVisible] = useState(true)
  46 + const [hoverTime, setHoverTime] = useState<number | null>(null)
  47 + const videoRef = useRef<HTMLVideoElement>(null)
  48 + const progressRef = useRef<HTMLDivElement>(null)
  49 + const volumeRef = useRef<HTMLDivElement>(null)
  50 + const controlsTimeoutRef = useRef<NodeJS.Timeout | null>(null)
  51 + const [isSmallSize, setIsSmallSize] = useState(false)
  52 + const containerRef = useRef<HTMLDivElement>(null)
  53 +
  54 + useEffect(() => {
  55 + const video = videoRef.current
  56 + if (!video)
  57 + return
  58 +
  59 + const setVideoData = () => {
  60 + setDuration(video.duration)
  61 + setVolume(video.volume)
  62 + }
  63 +
  64 + const setVideoTime = () => {
  65 + setCurrentTime(video.currentTime)
  66 + }
  67 +
  68 + const handleEnded = () => {
  69 + setIsPlaying(false)
  70 + }
  71 +
  72 + video.addEventListener('loadedmetadata', setVideoData)
  73 + video.addEventListener('timeupdate', setVideoTime)
  74 + video.addEventListener('ended', handleEnded)
  75 +
  76 + return () => {
  77 + video.removeEventListener('loadedmetadata', setVideoData)
  78 + video.removeEventListener('timeupdate', setVideoTime)
  79 + video.removeEventListener('ended', handleEnded)
  80 + }
  81 + }, [src])
  82 +
  83 + useEffect(() => {
  84 + return () => {
  85 + if (controlsTimeoutRef.current)
  86 + clearTimeout(controlsTimeoutRef.current)
  87 + }
  88 + }, [])
  89 +
  90 + const showControls = useCallback(() => {
  91 + setIsControlsVisible(true)
  92 + if (controlsTimeoutRef.current)
  93 + clearTimeout(controlsTimeoutRef.current)
  94 +
  95 + controlsTimeoutRef.current = setTimeout(() => setIsControlsVisible(false), 3000)
  96 + }, [])
  97 +
  98 + const togglePlayPause = useCallback(() => {
  99 + const video = videoRef.current
  100 + if (video) {
  101 + if (isPlaying)
  102 + video.pause()
  103 + else video.play().catch(error => console.error('Error playing video:', error))
  104 + setIsPlaying(!isPlaying)
  105 + }
  106 + }, [isPlaying])
  107 +
  108 + const toggleMute = useCallback(() => {
  109 + const video = videoRef.current
  110 + if (video) {
  111 + const newMutedState = !video.muted
  112 + video.muted = newMutedState
  113 + setIsMuted(newMutedState)
  114 + setVolume(newMutedState ? 0 : (video.volume > 0 ? video.volume : 1))
  115 + video.volume = newMutedState ? 0 : (video.volume > 0 ? video.volume : 1)
  116 + }
  117 + }, [])
  118 +
  119 + const toggleFullscreen = useCallback(() => {
  120 + const video = videoRef.current
  121 + if (video) {
  122 + if (document.fullscreenElement)
  123 + document.exitFullscreen()
  124 + else video.requestFullscreen()
  125 + }
  126 + }, [])
  127 +
  128 + const formatTime = (time: number) => {
  129 + const minutes = Math.floor(time / 60)
  130 + const seconds = Math.floor(time % 60)
  131 + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  132 + }
  133 +
  134 + const updateVideoProgress = useCallback((clientX: number) => {
  135 + const progressBar = progressRef.current
  136 + const video = videoRef.current
  137 + if (progressBar && video) {
  138 + const rect = progressBar.getBoundingClientRect()
  139 + const pos = (clientX - rect.left) / rect.width
  140 + const newTime = pos * video.duration
  141 + if (newTime >= 0 && newTime <= video.duration) {
  142 + setHoverTime(newTime)
  143 + if (isDragging)
  144 + video.currentTime = newTime
  145 + }
  146 + }
  147 + }, [isDragging])
  148 +
  149 + const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
  150 + updateVideoProgress(e.clientX)
  151 + }, [updateVideoProgress])
  152 +
  153 + const handleMouseLeave = useCallback(() => {
  154 + if (!isDragging)
  155 + setHoverTime(null)
  156 + }, [isDragging])
  157 +
  158 + const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
  159 + e.preventDefault()
  160 + setIsDragging(true)
  161 + updateVideoProgress(e.clientX)
  162 + }, [updateVideoProgress])
  163 +
  164 + useEffect(() => {
  165 + const handleGlobalMouseMove = (e: MouseEvent) => {
  166 + if (isDragging)
  167 + updateVideoProgress(e.clientX)
  168 + }
  169 +
  170 + const handleGlobalMouseUp = () => {
  171 + setIsDragging(false)
  172 + setHoverTime(null)
  173 + }
  174 +
  175 + if (isDragging) {
  176 + document.addEventListener('mousemove', handleGlobalMouseMove)
  177 + document.addEventListener('mouseup', handleGlobalMouseUp)
  178 + }
  179 +
  180 + return () => {
  181 + document.removeEventListener('mousemove', handleGlobalMouseMove)
  182 + document.removeEventListener('mouseup', handleGlobalMouseUp)
  183 + }
  184 + }, [isDragging, updateVideoProgress])
  185 +
  186 + const checkSize = useCallback(() => {
  187 + if (containerRef.current)
  188 + setIsSmallSize(containerRef.current.offsetWidth < 400)
  189 + }, [])
  190 +
  191 + useEffect(() => {
  192 + checkSize()
  193 + window.addEventListener('resize', checkSize)
  194 + return () => window.removeEventListener('resize', checkSize)
  195 + }, [checkSize])
  196 +
  197 + const handleVolumeChange = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
  198 + const volumeBar = volumeRef.current
  199 + const video = videoRef.current
  200 + if (volumeBar && video) {
  201 + const rect = volumeBar.getBoundingClientRect()
  202 + const newVolume = (e.clientX - rect.left) / rect.width
  203 + const clampedVolume = Math.max(0, Math.min(1, newVolume))
  204 + video.volume = clampedVolume
  205 + setVolume(clampedVolume)
  206 + setIsMuted(clampedVolume === 0)
  207 + }
  208 + }, [])
  209 +
  210 + return (
  211 + <div ref={containerRef} className={styles.videoPlayer} onMouseMove={showControls} onMouseEnter={showControls}>
  212 + <video ref={videoRef} src={src} className={styles.video} />
  213 + <div className={`${styles.controls} ${isControlsVisible ? styles.visible : styles.hidden} ${isSmallSize ? styles.smallSize : ''}`}>
  214 + <div className={styles.overlay}>
  215 + <div className={styles.progressBarContainer}>
  216 + <div
  217 + ref={progressRef}
  218 + className={styles.progressBar}
  219 + onClick={handleMouseDown}
  220 + onMouseMove={handleMouseMove}
  221 + onMouseLeave={handleMouseLeave}
  222 + onMouseDown={handleMouseDown}
  223 + >
  224 + <div className={styles.progress} style={{ width: `${(currentTime / duration) * 100}%` }} />
  225 + {hoverTime !== null && (
  226 + <div
  227 + className={styles.hoverTimeIndicator}
  228 + style={{ left: `${(hoverTime / duration) * 100}%` }}
  229 + >
  230 + {formatTime(hoverTime)}
  231 + </div>
  232 + )}
  233 + </div>
  234 + </div>
  235 + <div className={styles.controlsContent}>
  236 + <div className={styles.leftControls}>
  237 + <button className={styles.playPauseButton} onClick={togglePlayPause}>
  238 + {isPlaying ? <PauseIcon /> : <PlayIcon />}
  239 + </button>
  240 + {!isSmallSize && (<span className={styles.time}>{formatTime(currentTime)} / {formatTime(duration)}</span>)}
  241 + </div>
  242 + <div className={styles.rightControls}>
  243 + <button className={styles.muteButton} onClick={toggleMute}>
  244 + {isMuted ? <UnmuteIcon /> : <MuteIcon />}
  245 + </button>
  246 + {!isSmallSize && (
  247 + <div className={styles.volumeControl}>
  248 + <div
  249 + ref={volumeRef}
  250 + className={styles.volumeSlider}
  251 + onClick={handleVolumeChange}
  252 + onMouseDown={(e) => {
  253 + handleVolumeChange(e)
  254 + const handleMouseMove = (e: MouseEvent) => handleVolumeChange(e as unknown as React.MouseEvent<HTMLDivElement>)
  255 + const handleMouseUp = () => {
  256 + document.removeEventListener('mousemove', handleMouseMove)
  257 + document.removeEventListener('mouseup', handleMouseUp)
  258 + }
  259 + document.addEventListener('mousemove', handleMouseMove)
  260 + document.addEventListener('mouseup', handleMouseUp)
  261 + }}
  262 + >
  263 + <div className={styles.volumeLevel} style={{ width: `${volume * 100}%` }} />
  264 + </div>
  265 + </div>
  266 + )}
  267 + <button className={styles.fullscreenButton} onClick={toggleFullscreen}>
  268 + <FullscreenIcon />
  269 + </button>
  270 + </div>
  271 + </div>
  272 + </div>
  273 + </div>
  274 + </div>
  275 + )
  276 +}
  277 +
  278 +export default VideoPlayer
  1 +import React from 'react'
  2 +import VideoPlayer from './VideoPlayer'
  3 +
  4 +type Props = {
  5 + srcs: string[]
  6 +}
  7 +
  8 +const VideoGallery: React.FC<Props> = ({ srcs }) => {
  9 + return (<><br/>{srcs.map((src, index) => (<React.Fragment key={`video_${index}`}><br/><VideoPlayer src={src}/></React.Fragment>))}</>)
  10 +}
  11 +
  12 +export default React.memo(VideoGallery)