|
@@ -25,23 +25,27 @@
|
|
|
<div class="custom-controls" :class="{ 'controls-hidden': !showControls }">
|
|
<div class="custom-controls" :class="{ 'controls-hidden': !showControls }">
|
|
|
<div class="mini-top-bar">
|
|
<div class="mini-top-bar">
|
|
|
<div class="mini-top-left">
|
|
<div class="mini-top-left">
|
|
|
- <div class="mini-top-icon" title="返回" @click="handleBack">
|
|
|
|
|
|
|
+ <div class="mini-top-icon" data-title="返回" @click="handleBack">
|
|
|
<img src="./svg/fanhui.svg" alt="返回"/>
|
|
<img src="./svg/fanhui.svg" alt="返回"/>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="mini-top-right">
|
|
<div class="mini-top-right">
|
|
|
- <div class="mini-top-icon" title="分享" @click="handleShare">
|
|
|
|
|
|
|
+ <div class="mini-top-icon" data-title="分享" @click="handleShare">
|
|
|
<img src="./svg/fenxiang.svg" alt="分享"/>
|
|
<img src="./svg/fenxiang.svg" alt="分享"/>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="speed-toast" v-if="showSpeedToast">
|
|
|
|
|
+ <span class="toast-icon" v-if="toastType === 'speed'">⚡</span>
|
|
|
|
|
+ <span class="toast-icon" v-if="toastType === 'volume'">🔊</span>
|
|
|
|
|
+ <span class="toast-text">{{ speedToastText }}</span>
|
|
|
|
|
+ </div>
|
|
|
<div class="center-play-btn" v-if="!isPlaying" @click="togglePlay">
|
|
<div class="center-play-btn" v-if="!isPlaying" @click="togglePlay">
|
|
|
<img src="./svg/play.svg" alt="播放"/>
|
|
<img src="./svg/play.svg" alt="播放"/>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="video-click-area" @click="handleVideoClick"></div>
|
|
|
|
|
<div class="mini-controls">
|
|
<div class="mini-controls">
|
|
|
<div class="mini-control-bar">
|
|
<div class="mini-control-bar">
|
|
|
- <div class="play-btn" @click="togglePlay">
|
|
|
|
|
|
|
+ <div class="play-btn" @click="togglePlay" :data-title="!isPlaying ? '播放':'暂停'">
|
|
|
<img v-if="!isPlaying" src="./svg/play.svg" alt="播放"/>
|
|
<img v-if="!isPlaying" src="./svg/play.svg" alt="播放"/>
|
|
|
<img v-else src="./svg/pause.svg" alt="暂停"/>
|
|
<img v-else src="./svg/pause.svg" alt="暂停"/>
|
|
|
</div>
|
|
</div>
|
|
@@ -58,22 +62,59 @@
|
|
|
<div class="mini-progress-dot" :style="{ left: progressPercent + '%' }"></div>
|
|
<div class="mini-progress-dot" :style="{ left: progressPercent + '%' }"></div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="time-duration">{{ formatTime(duration) }}</div>
|
|
<div class="time-duration">{{ formatTime(duration) }}</div>
|
|
|
- <div class="pip-btn" @click="togglePiP">
|
|
|
|
|
|
|
+ <div class="pip-btn" @click="togglePiP" data-title="小窗口播放">
|
|
|
<img src="./svg/xiaochuang.svg" alt="小窗播放"/>
|
|
<img src="./svg/xiaochuang.svg" alt="小窗播放"/>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="pip-btn" @click="togglePiP">
|
|
|
|
|
- <img src="./svg/beisu1.svg" alt="小窗播放"/>
|
|
|
|
|
|
|
+ <div class="pip-btn" @click.stop ref="speedBtn" @mouseenter="handleSpeedMenuEnter"
|
|
|
|
|
+ @mouseleave="handleSpeedMenuLeave" data-title="倍速"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img src="./svg/beisu.svg" alt="倍速播放"/>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="speed-menu-wrapper" @mouseenter="handleSpeedMenuEnter" @mouseleave="handleSpeedMenuLeave">
|
|
|
|
|
+ <div class="speed-menu" v-if="showSpeedMenu" @click.stop>
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="rate in speedOptions"
|
|
|
|
|
+ :key="rate"
|
|
|
|
|
+ class="speed-option"
|
|
|
|
|
+ :class="{ active: playbackRate === rate }"
|
|
|
|
|
+ @click="changePlaybackRate(rate)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ rate }}x
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="pip-btn" @click="toggleMute">
|
|
|
|
|
- <img v-if="!isMuted" src="./svg/yinliang.svg" alt="音量"/>
|
|
|
|
|
|
|
+ <div class="pip-btn" @click.stop ref="volumeBtn" @mouseenter="handleVolumeMenuEnter"
|
|
|
|
|
+ @mouseleave="handleVolumeMenuLeave" data-title="音量"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img v-if="!isMuted && volume > 0.5" src="./svg/yinliang.svg" alt="音量"/>
|
|
|
|
|
+ <img v-else-if="!isMuted && volume > 0" src="./svg/yinliang.svg" alt="低音量"/>
|
|
|
<img v-else src="./svg/jingyin.svg" alt="静音"/>
|
|
<img v-else src="./svg/jingyin.svg" alt="静音"/>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="fullscreen-btn" @click="toggleFullscreen">
|
|
|
|
|
- <img src="./svg/quanping.svg" alt="全屏"/>
|
|
|
|
|
|
|
+ <div class="volume-menu-wrapper" @mouseenter="handleVolumeMenuEnter" @mouseleave="handleVolumeMenuLeave">
|
|
|
|
|
+ <div class="volume-menu" v-if="showVolumeMenu" @click.stop>
|
|
|
|
|
+ <div class="volume-slider-container">
|
|
|
|
|
+ <div class="volume-slider-vertical"
|
|
|
|
|
+ ref="volumeSlider"
|
|
|
|
|
+ @click="onVolumeSliderClick"
|
|
|
|
|
+ @mousedown="onVolumeSliderMouseDown"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="volume-slider-track">
|
|
|
|
|
+ <div class="volume-slider-fill" :style="{ height: (volume * 100) + '%' }"></div>
|
|
|
|
|
+ <div class="volume-slider-thumb" :style="{ bottom: (volume * 100) + '%' }"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="volume-percent">{{ Math.round(volume * 100) }}%</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="fullscreen-btn" @click="toggleFullscreen" :data-title="isFullscreen ? '退出全屏':'全屏'">
|
|
|
|
|
+ <img v-if="!isFullscreen" src="./svg/quanping.svg" alt="全屏"/>
|
|
|
|
|
+ <img v-else src="./svg/qxquanping.svg" alt="退出全屏"/>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <div class="video-click-area" @click="handleVideoClick"></div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -85,10 +126,6 @@ export default {
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
default: false
|
|
default: false
|
|
|
},
|
|
},
|
|
|
- controls: {
|
|
|
|
|
- type: Boolean,
|
|
|
|
|
- default: true
|
|
|
|
|
- },
|
|
|
|
|
autoplay: {
|
|
autoplay: {
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
default: false
|
|
default: false
|
|
@@ -112,7 +149,19 @@ export default {
|
|
|
showControls: true,
|
|
showControls: true,
|
|
|
controlsTimer: null,
|
|
controlsTimer: null,
|
|
|
isDragging: false,
|
|
isDragging: false,
|
|
|
- isPiPActive: false
|
|
|
|
|
|
|
+ isPiPActive: false,
|
|
|
|
|
+ isFullscreen: false,
|
|
|
|
|
+ playbackRate: 1.0,
|
|
|
|
|
+ showSpeedMenu: false,
|
|
|
|
|
+ showSpeedToast: false,
|
|
|
|
|
+ speedToastText: '',
|
|
|
|
|
+ toastType: null,
|
|
|
|
|
+ speedToastTimer: null,
|
|
|
|
|
+ speedOptions: [0.5, 0.75, 1.0, 1.25, 1.5, 2.0],
|
|
|
|
|
+ volume: 1.0,
|
|
|
|
|
+ previousVolume: 1.0,
|
|
|
|
|
+ showVolumeMenu: false,
|
|
|
|
|
+ menuHideTimer: null
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
computed: {
|
|
computed: {
|
|
@@ -123,18 +172,40 @@ export default {
|
|
|
bufferedPercent() {
|
|
bufferedPercent() {
|
|
|
if (this.duration === 0) return 0
|
|
if (this.duration === 0) return 0
|
|
|
return (this.buffered / this.duration) * 100
|
|
return (this.buffered / this.duration) * 100
|
|
|
- },
|
|
|
|
|
- isPiPSupported() {
|
|
|
|
|
- const video = this.$refs.videoPlayer
|
|
|
|
|
- return video && (video.requestPictureInPicture || document.pictureInPictureEnabled)
|
|
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
mounted() {
|
|
mounted() {
|
|
|
|
|
+ document.addEventListener('fullscreenchange', this.handleFullscreenChange)
|
|
|
|
|
+ document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange)
|
|
|
|
|
+ document.addEventListener('mozfullscreenchange', this.handleFullscreenChange)
|
|
|
|
|
+ document.addEventListener('MSFullscreenChange', this.handleFullscreenChange)
|
|
|
|
|
+ document.addEventListener('keydown', this.handleKeydown)
|
|
|
|
|
+
|
|
|
|
|
+ const video = this.$refs.videoPlayer
|
|
|
|
|
+ if (video) {
|
|
|
|
|
+ this.volume = video.volume || 1.0
|
|
|
|
|
+ this.playbackRate = video.playbackRate || 1.0
|
|
|
|
|
+ video.addEventListener('volumechange', this.handleVolumeChange)
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
beforeDestroy() {
|
|
beforeDestroy() {
|
|
|
|
|
+ this.cleanupEventListeners()
|
|
|
|
|
+
|
|
|
if (this.isPiPActive) {
|
|
if (this.isPiPActive) {
|
|
|
this.exitPiP()
|
|
this.exitPiP()
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const video = this.$refs.videoPlayer
|
|
|
|
|
+ if (video) {
|
|
|
|
|
+ video.removeEventListener('volumechange', this.handleVolumeChange)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.removeEventListener('keydown', this.handleKeydown)
|
|
|
|
|
+
|
|
|
|
|
+ if (this.speedToastTimer) {
|
|
|
|
|
+ clearTimeout(this.speedToastTimer)
|
|
|
|
|
+ this.speedToastTimer = null
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
methods: {
|
|
methods: {
|
|
|
togglePlay() {
|
|
togglePlay() {
|
|
@@ -148,7 +219,9 @@ export default {
|
|
|
handleVideoClick(e) {
|
|
handleVideoClick(e) {
|
|
|
if (e.target.closest('.mini-top-bar') ||
|
|
if (e.target.closest('.mini-top-bar') ||
|
|
|
e.target.closest('.mini-control-bar') ||
|
|
e.target.closest('.mini-control-bar') ||
|
|
|
- e.target.closest('.center-play-btn')) {
|
|
|
|
|
|
|
+ e.target.closest('.center-play-btn') ||
|
|
|
|
|
+ e.target.closest('.speed-menu') ||
|
|
|
|
|
+ e.target.closest('.volume-menu')) {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
this.togglePlay()
|
|
this.togglePlay()
|
|
@@ -187,16 +260,140 @@ export default {
|
|
|
},
|
|
},
|
|
|
toggleMute() {
|
|
toggleMute() {
|
|
|
const video = this.$refs.videoPlayer
|
|
const video = this.$refs.videoPlayer
|
|
|
|
|
+ if (!video) return
|
|
|
|
|
+
|
|
|
if (this.isMuted) {
|
|
if (this.isMuted) {
|
|
|
video.muted = false
|
|
video.muted = false
|
|
|
this.isMuted = false
|
|
this.isMuted = false
|
|
|
- video.volume = this.previousVolume
|
|
|
|
|
- this.volume = this.previousVolume
|
|
|
|
|
|
|
+ if (this.previousVolume && this.previousVolume > 0) {
|
|
|
|
|
+ video.volume = this.previousVolume
|
|
|
|
|
+ this.volume = this.previousVolume
|
|
|
|
|
+ } else {
|
|
|
|
|
+ video.volume = 1.0
|
|
|
|
|
+ this.volume = 1.0
|
|
|
|
|
+ this.previousVolume = 1.0
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
this.previousVolume = this.volume
|
|
this.previousVolume = this.volume
|
|
|
video.muted = true
|
|
video.muted = true
|
|
|
this.isMuted = true
|
|
this.isMuted = true
|
|
|
|
|
+ video.volume = 0
|
|
|
|
|
+ this.volume = 0
|
|
|
}
|
|
}
|
|
|
|
|
+ this.showVolumeToastMessage(this.isMuted ? '已静音' : `音量 ${Math.round(this.volume * 100)}%`)
|
|
|
|
|
+ },
|
|
|
|
|
+ handleSpeedMenuEnter() {
|
|
|
|
|
+ if (this.menuHideTimer) {
|
|
|
|
|
+ clearTimeout(this.menuHideTimer)
|
|
|
|
|
+ this.menuHideTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+ this.showVolumeMenu = false
|
|
|
|
|
+ this.showSpeedMenu = true
|
|
|
|
|
+ },
|
|
|
|
|
+ handleSpeedMenuLeave() {
|
|
|
|
|
+ this.menuHideTimer = setTimeout(() => {
|
|
|
|
|
+ this.showSpeedMenu = false
|
|
|
|
|
+ this.menuHideTimer = null
|
|
|
|
|
+ }, 200)
|
|
|
|
|
+ },
|
|
|
|
|
+ handleVolumeMenuEnter() {
|
|
|
|
|
+ if (this.menuHideTimer) {
|
|
|
|
|
+ clearTimeout(this.menuHideTimer)
|
|
|
|
|
+ this.menuHideTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+ this.showSpeedMenu = false
|
|
|
|
|
+ this.showVolumeMenu = true
|
|
|
|
|
+ },
|
|
|
|
|
+ handleVolumeMenuLeave() {
|
|
|
|
|
+ this.menuHideTimer = setTimeout(() => {
|
|
|
|
|
+ this.showVolumeMenu = false
|
|
|
|
|
+ this.menuHideTimer = null
|
|
|
|
|
+ }, 200)
|
|
|
|
|
+ },
|
|
|
|
|
+ setVolume(value) {
|
|
|
|
|
+ const video = this.$refs.videoPlayer
|
|
|
|
|
+ if (!video) return
|
|
|
|
|
+
|
|
|
|
|
+ value = Math.max(0, Math.min(1, value))
|
|
|
|
|
+
|
|
|
|
|
+ this.volume = value
|
|
|
|
|
+ video.volume = value
|
|
|
|
|
+
|
|
|
|
|
+ if (value === 0) {
|
|
|
|
|
+ this.isMuted = true
|
|
|
|
|
+ video.muted = true
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.isMuted = false
|
|
|
|
|
+ video.muted = false
|
|
|
|
|
+ this.previousVolume = value
|
|
|
|
|
+ }
|
|
|
|
|
+ this.showVolumeToastMessage(`音量 ${Math.round(value * 100)}%`)
|
|
|
|
|
+ },
|
|
|
|
|
+ handleVolumeChange() {
|
|
|
|
|
+ const video = this.$refs.videoPlayer
|
|
|
|
|
+ if (!video) return
|
|
|
|
|
+
|
|
|
|
|
+ this.volume = video.volume
|
|
|
|
|
+ this.isMuted = video.muted
|
|
|
|
|
+ },
|
|
|
|
|
+ onVolumeSliderClick(e) {
|
|
|
|
|
+ e.stopPropagation()
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+
|
|
|
|
|
+ const slider = this.$refs.volumeSlider
|
|
|
|
|
+ if (!slider) return
|
|
|
|
|
+
|
|
|
|
|
+ const rect = slider.getBoundingClientRect()
|
|
|
|
|
+ const pos = 1 - (e.clientY - rect.top) / rect.height
|
|
|
|
|
+ const volume = Math.max(0, Math.min(1, pos))
|
|
|
|
|
+ this.setVolume(volume)
|
|
|
|
|
+ },
|
|
|
|
|
+ onVolumeSliderMouseDown(e) {
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ e.stopPropagation()
|
|
|
|
|
+
|
|
|
|
|
+ const slider = this.$refs.volumeSlider
|
|
|
|
|
+ if (!slider) return
|
|
|
|
|
+
|
|
|
|
|
+ let isDragging = true
|
|
|
|
|
+
|
|
|
|
|
+ const updateVolume = (event) => {
|
|
|
|
|
+ if (!isDragging) return
|
|
|
|
|
+
|
|
|
|
|
+ const rect = slider.getBoundingClientRect()
|
|
|
|
|
+ const pos = 1 - (event.clientY - rect.top) / rect.height
|
|
|
|
|
+ const volume = Math.max(0, Math.min(1, pos))
|
|
|
|
|
+ this.setVolume(volume)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleMouseMove = (event) => {
|
|
|
|
|
+ event.preventDefault()
|
|
|
|
|
+ updateVolume(event)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const handleMouseUp = () => {
|
|
|
|
|
+ isDragging = false
|
|
|
|
|
+ document.removeEventListener('mousemove', handleMouseMove)
|
|
|
|
|
+ document.removeEventListener('mouseup', handleMouseUp)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('mousemove', handleMouseMove, { passive: false })
|
|
|
|
|
+ document.addEventListener('mouseup', handleMouseUp)
|
|
|
|
|
+
|
|
|
|
|
+ updateVolume(e)
|
|
|
|
|
+ },
|
|
|
|
|
+ showVolumeToastMessage(text) {
|
|
|
|
|
+ this.toastType = 'volume'
|
|
|
|
|
+ this.speedToastText = text
|
|
|
|
|
+ this.showSpeedToast = true
|
|
|
|
|
+
|
|
|
|
|
+ if (this.speedToastTimer) {
|
|
|
|
|
+ clearTimeout(this.speedToastTimer)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.speedToastTimer = setTimeout(() => {
|
|
|
|
|
+ this.showSpeedToast = false
|
|
|
|
|
+ }, 1500)
|
|
|
},
|
|
},
|
|
|
onPlay() {
|
|
onPlay() {
|
|
|
this.isPlaying = true
|
|
this.isPlaying = true
|
|
@@ -232,30 +429,47 @@ export default {
|
|
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
|
|
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
|
|
|
},
|
|
},
|
|
|
onProgressBarClick(e) {
|
|
onProgressBarClick(e) {
|
|
|
|
|
+ e.stopPropagation()
|
|
|
const progressBar = this.$refs.progressBar
|
|
const progressBar = this.$refs.progressBar
|
|
|
|
|
+ if (!progressBar) return
|
|
|
|
|
+
|
|
|
const rect = progressBar.getBoundingClientRect()
|
|
const rect = progressBar.getBoundingClientRect()
|
|
|
const pos = (e.clientX - rect.left) / rect.width
|
|
const pos = (e.clientX - rect.left) / rect.width
|
|
|
const video = this.$refs.videoPlayer
|
|
const video = this.$refs.videoPlayer
|
|
|
- video.currentTime = pos * this.duration
|
|
|
|
|
|
|
+ if (video && this.duration > 0) {
|
|
|
|
|
+ video.currentTime = pos * this.duration
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
onProgressBarMouseDown(e) {
|
|
onProgressBarMouseDown(e) {
|
|
|
|
|
+ e.stopPropagation()
|
|
|
this.isDragging = true
|
|
this.isDragging = true
|
|
|
- const handleMouseMove = (e) => {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const progressBar = this.$refs.progressBar
|
|
|
|
|
+ if (!progressBar) return
|
|
|
|
|
+
|
|
|
|
|
+ const handleMouseMove = (event) => {
|
|
|
if (!this.isDragging) return
|
|
if (!this.isDragging) return
|
|
|
- const progressBar = this.$refs.progressBar
|
|
|
|
|
|
|
+
|
|
|
const rect = progressBar.getBoundingClientRect()
|
|
const rect = progressBar.getBoundingClientRect()
|
|
|
- let pos = (e.clientX - rect.left) / rect.width
|
|
|
|
|
|
|
+ let pos = (event.clientX - rect.left) / rect.width
|
|
|
pos = Math.max(0, Math.min(1, pos))
|
|
pos = Math.max(0, Math.min(1, pos))
|
|
|
|
|
+
|
|
|
const video = this.$refs.videoPlayer
|
|
const video = this.$refs.videoPlayer
|
|
|
- video.currentTime = pos * this.duration
|
|
|
|
|
|
|
+ if (video && this.duration > 0) {
|
|
|
|
|
+ video.currentTime = pos * this.duration
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
const handleMouseUp = () => {
|
|
const handleMouseUp = () => {
|
|
|
this.isDragging = false
|
|
this.isDragging = false
|
|
|
document.removeEventListener('mousemove', handleMouseMove)
|
|
document.removeEventListener('mousemove', handleMouseMove)
|
|
|
document.removeEventListener('mouseup', handleMouseUp)
|
|
document.removeEventListener('mouseup', handleMouseUp)
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
document.addEventListener('mousemove', handleMouseMove)
|
|
document.addEventListener('mousemove', handleMouseMove)
|
|
|
document.addEventListener('mouseup', handleMouseUp)
|
|
document.addEventListener('mouseup', handleMouseUp)
|
|
|
|
|
+
|
|
|
|
|
+ handleMouseMove(e)
|
|
|
},
|
|
},
|
|
|
toggleFullscreen() {
|
|
toggleFullscreen() {
|
|
|
const container = this.$el
|
|
const container = this.$el
|
|
@@ -281,6 +495,40 @@ export default {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
+ handleFullscreenChange() {
|
|
|
|
|
+ this.isFullscreen = !!(document.fullscreenElement ||
|
|
|
|
|
+ document.webkitFullscreenElement ||
|
|
|
|
|
+ document.mozFullScreenElement ||
|
|
|
|
|
+ document.msFullscreenElement)
|
|
|
|
|
+ },
|
|
|
|
|
+ changePlaybackRate(rate) {
|
|
|
|
|
+ const video = this.$refs.videoPlayer
|
|
|
|
|
+ if (!video) return
|
|
|
|
|
+
|
|
|
|
|
+ video.playbackRate = rate
|
|
|
|
|
+ this.playbackRate = rate
|
|
|
|
|
+ this.showSpeedMenu = false
|
|
|
|
|
+ this.showSpeedToastMessage(`${rate}x 倍速播放`)
|
|
|
|
|
+ },
|
|
|
|
|
+ showSpeedToastMessage(text) {
|
|
|
|
|
+ this.toastType = 'speed'
|
|
|
|
|
+ this.speedToastText = text
|
|
|
|
|
+ this.showSpeedToast = true
|
|
|
|
|
+
|
|
|
|
|
+ if (this.speedToastTimer) {
|
|
|
|
|
+ clearTimeout(this.speedToastTimer)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.speedToastTimer = setTimeout(() => {
|
|
|
|
|
+ this.showSpeedToast = false
|
|
|
|
|
+ }, 2000)
|
|
|
|
|
+ },
|
|
|
|
|
+ cleanupEventListeners() {
|
|
|
|
|
+ document.removeEventListener('fullscreenchange', this.handleFullscreenChange)
|
|
|
|
|
+ document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange)
|
|
|
|
|
+ document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange)
|
|
|
|
|
+ document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange)
|
|
|
|
|
+ },
|
|
|
handleBack() {
|
|
handleBack() {
|
|
|
if (this.isPiPActive) {
|
|
if (this.isPiPActive) {
|
|
|
this.exitPiP()
|
|
this.exitPiP()
|
|
@@ -300,10 +548,14 @@ export default {
|
|
|
this.showControls = true
|
|
this.showControls = true
|
|
|
if (this.controlsTimer) {
|
|
if (this.controlsTimer) {
|
|
|
clearTimeout(this.controlsTimer)
|
|
clearTimeout(this.controlsTimer)
|
|
|
|
|
+ this.controlsTimer = null
|
|
|
}
|
|
}
|
|
|
if (this.isPlaying) {
|
|
if (this.isPlaying) {
|
|
|
this.controlsTimer = setTimeout(() => {
|
|
this.controlsTimer = setTimeout(() => {
|
|
|
- this.showControls = false
|
|
|
|
|
|
|
+ if (!this.showSpeedMenu && !this.showVolumeMenu) {
|
|
|
|
|
+ this.showControls = false
|
|
|
|
|
+ }
|
|
|
|
|
+ this.controlsTimer = null
|
|
|
}, 3000)
|
|
}, 3000)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -311,13 +563,77 @@ export default {
|
|
|
if (this.isPlaying) {
|
|
if (this.isPlaying) {
|
|
|
this.showControls = false
|
|
this.showControls = false
|
|
|
}
|
|
}
|
|
|
|
|
+ },
|
|
|
|
|
+ handleKeydown(e) {
|
|
|
|
|
+ if (this.showSpeedMenu || this.showVolumeMenu) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const video = this.$refs.videoPlayer
|
|
|
|
|
+ if (!video) return
|
|
|
|
|
+
|
|
|
|
|
+ switch (e.key) {
|
|
|
|
|
+ case 'ArrowUp':
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ this.adjustVolume(0.05)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'ArrowDown':
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ this.adjustVolume(-0.05)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'ArrowRight':
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ this.adjustProgress(5)
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'ArrowLeft':
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ this.adjustProgress(-5)
|
|
|
|
|
+ break
|
|
|
|
|
+ case ' ':
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ this.togglePlay()
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'm':
|
|
|
|
|
+ case 'M':
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ this.toggleMute()
|
|
|
|
|
+ break
|
|
|
|
|
+ case 'f':
|
|
|
|
|
+ case 'F':
|
|
|
|
|
+ e.preventDefault()
|
|
|
|
|
+ this.toggleFullscreen()
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ adjustVolume(delta) {
|
|
|
|
|
+ const newVolume = Math.max(0, Math.min(1, this.volume + delta))
|
|
|
|
|
+ this.setVolume(newVolume)
|
|
|
|
|
+ },
|
|
|
|
|
+ adjustProgress(seconds) {
|
|
|
|
|
+ const video = this.$refs.videoPlayer
|
|
|
|
|
+ if (!video || !this.duration) return
|
|
|
|
|
+
|
|
|
|
|
+ const newTime = Math.max(0, Math.min(this.duration, this.currentTime + seconds))
|
|
|
|
|
+ video.currentTime = newTime
|
|
|
|
|
+ this.showProgressToastMessage(`${seconds > 0 ? '+' : ''}${seconds}秒`)
|
|
|
|
|
+ },
|
|
|
|
|
+ showProgressToastMessage(text) {
|
|
|
|
|
+ this.toastType = 'speed'
|
|
|
|
|
+ this.speedToastText = text
|
|
|
|
|
+ this.showSpeedToast = true
|
|
|
|
|
+
|
|
|
|
|
+ if (this.speedToastTimer) {
|
|
|
|
|
+ clearTimeout(this.speedToastTimer)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.speedToastTimer = setTimeout(() => {
|
|
|
|
|
+ this.showSpeedToast = false
|
|
|
|
|
+ }, 1000)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</script>
|
|
</script>
|
|
|
-
|
|
|
|
|
<style scoped lang="scss">
|
|
<style scoped lang="scss">
|
|
|
-
|
|
|
|
|
.video-container {
|
|
.video-container {
|
|
|
position: relative;
|
|
position: relative;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
@@ -367,6 +683,11 @@ export default {
|
|
|
.mini-controls {
|
|
.mini-controls {
|
|
|
pointer-events: none;
|
|
pointer-events: none;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .speed-menu {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ visibility: hidden;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -439,8 +760,8 @@ export default {
|
|
|
top: 0;
|
|
top: 0;
|
|
|
left: 0;
|
|
left: 0;
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
- height: calc(100% - 60px);
|
|
|
|
|
- z-index: 20;
|
|
|
|
|
|
|
+ height: calc(100% - 80px);
|
|
|
|
|
+ z-index: 10;
|
|
|
pointer-events: auto;
|
|
pointer-events: auto;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
}
|
|
}
|
|
@@ -475,7 +796,7 @@ export default {
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: 6px;
|
|
gap: 6px;
|
|
|
pointer-events: auto;
|
|
pointer-events: auto;
|
|
|
- z-index: 15;
|
|
|
|
|
|
|
+ z-index: 40;
|
|
|
transition: transform .3s ease, opacity .3s ease;
|
|
transition: transform .3s ease, opacity .3s ease;
|
|
|
transform: translateY(0)
|
|
transform: translateY(0)
|
|
|
}
|
|
}
|
|
@@ -493,6 +814,8 @@ export default {
|
|
|
|
|
|
|
|
.mini-controls .mini-control-bar .play-btn,
|
|
.mini-controls .mini-control-bar .play-btn,
|
|
|
.mini-controls .mini-control-bar .pip-btn,
|
|
.mini-controls .mini-control-bar .pip-btn,
|
|
|
|
|
+.mini-controls .mini-control-bar .speed-btn,
|
|
|
|
|
+.mini-controls .mini-control-bar .volume-btn,
|
|
|
.mini-controls .mini-control-bar .fullscreen-btn {
|
|
.mini-controls .mini-control-bar .fullscreen-btn {
|
|
|
width: 32px;
|
|
width: 32px;
|
|
|
height: 32px;
|
|
height: 32px;
|
|
@@ -502,11 +825,20 @@ export default {
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
border-radius: 50%;
|
|
border-radius: 50%;
|
|
|
transition: background .2s;
|
|
transition: background .2s;
|
|
|
- flex-shrink: 0
|
|
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ position: relative
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.mini-controls .mini-control-bar .speed-btn,
|
|
|
|
|
+.mini-controls .mini-control-bar .volume-btn {
|
|
|
|
|
+ width: auto;
|
|
|
|
|
+ gap: 4px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.mini-controls .mini-control-bar .play-btn img,
|
|
.mini-controls .mini-control-bar .play-btn img,
|
|
|
.mini-controls .mini-control-bar .pip-btn img,
|
|
.mini-controls .mini-control-bar .pip-btn img,
|
|
|
|
|
+.mini-controls .mini-control-bar .speed-btn img,
|
|
|
|
|
+.mini-controls .mini-control-bar .volume-btn img,
|
|
|
.mini-controls .mini-control-bar .fullscreen-btn img {
|
|
.mini-controls .mini-control-bar .fullscreen-btn img {
|
|
|
width: 20px;
|
|
width: 20px;
|
|
|
height: 20px;
|
|
height: 20px;
|
|
@@ -516,6 +848,8 @@ export default {
|
|
|
@media(hover: hover) {
|
|
@media(hover: hover) {
|
|
|
.mini-controls .mini-control-bar .play-btn:hover,
|
|
.mini-controls .mini-control-bar .play-btn:hover,
|
|
|
.mini-controls .mini-control-bar .pip-btn:hover,
|
|
.mini-controls .mini-control-bar .pip-btn:hover,
|
|
|
|
|
+ .mini-controls .mini-control-bar .speed-btn:hover,
|
|
|
|
|
+ .mini-controls .mini-control-bar .volume-btn:hover,
|
|
|
.mini-controls .mini-control-bar .fullscreen-btn:hover {
|
|
.mini-controls .mini-control-bar .fullscreen-btn:hover {
|
|
|
background: rgba(255, 255, 255, .15)
|
|
background: rgba(255, 255, 255, .15)
|
|
|
}
|
|
}
|
|
@@ -575,4 +909,225 @@ export default {
|
|
|
box-shadow: 0 0 4px rgba(0, 0, 0, .3)
|
|
box-shadow: 0 0 4px rgba(0, 0, 0, .3)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.speed-menu {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 60px;
|
|
|
|
|
+ right: 80px;
|
|
|
|
|
+ background: rgba(28, 28, 28, 0.3);
|
|
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 6px;
|
|
|
|
|
+ min-width: 60px;
|
|
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ visibility: visible;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ transform-origin: bottom right;
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+
|
|
|
|
|
+ .speed-option {
|
|
|
|
|
+ padding: 5px 10px;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ background: rgb(255 255 255 / 15%);
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:first-child {
|
|
|
|
|
+ border-radius: 8px 8px 0 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ border-radius: 0 0 8px 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.speed-menu-wrapper {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ z-index: 99;
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.controls-hidden .speed-menu {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ visibility: hidden;
|
|
|
|
|
+ transform: scale(0.9) translateY(10px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+.speed-toast {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 80px;
|
|
|
|
|
+ left: 20px;
|
|
|
|
|
+ background: rgb(28 28 28 / 40%);
|
|
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ padding: 5px 10px;
|
|
|
|
|
+ display: -webkit-box;
|
|
|
|
|
+ display: -ms-flexbox;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ -webkit-box-align: center;
|
|
|
|
|
+ -ms-flex-align: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ animation: toastSlideIn-data-v-4d44248a 0.3s ease;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+
|
|
|
|
|
+ .toast-icon {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .toast-text {
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes toastSlideIn {
|
|
|
|
|
+ from {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transform: translateX(-20px) translateY(10px);
|
|
|
|
|
+ }
|
|
|
|
|
+ to {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ transform: translateX(0) translateY(0);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.controls-hidden .speed-toast {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ visibility: hidden;
|
|
|
|
|
+ transform: translateX(-20px) translateY(10px);
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.controls-hidden .speed-menu {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ visibility: hidden;
|
|
|
|
|
+ transform: scale(0.9) translateY(10px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.volume-menu {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 60px;
|
|
|
|
|
+ right: 50px;
|
|
|
|
|
+ height: 150px;
|
|
|
|
|
+ background: rgba(28, 28, 28, 0.3);
|
|
|
|
|
+ backdrop-filter: blur(10px);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 6px 0;
|
|
|
|
|
+ min-width: 40px;
|
|
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ visibility: visible;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ transform-origin: bottom right;
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+
|
|
|
|
|
+ .volume-percent {
|
|
|
|
|
+ cursor: default;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 10px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .volume-slider-vertical {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ height: 100px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .volume-slider-track {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: rgba(28, 28, 28, 0.5);
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+
|
|
|
|
|
+ .volume-slider-fill {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 2px;
|
|
|
|
|
+ transition: height 0.1s ease;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .volume-slider-thumb {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translate(-50%, 50%);
|
|
|
|
|
+ transition: bottom 0.1s ease;
|
|
|
|
|
+ z-index: 2;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ transform: translate(-50%, 50%) scale(1.2);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.controls-hidden .volume-menu {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ visibility: hidden;
|
|
|
|
|
+ transform: scale(0.9) translateY(10px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+div[data-title] {
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ &:after {
|
|
|
|
|
+ content: attr(data-title);
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 100%;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background-color: #00000080;
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ font-style: normal; //本例中data-title为i标签属性
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ padding: 5px 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|