qs_xinchun2026_h5/components/MediaPlayer.vue

802 lines
18 KiB
Vue
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
// 组件属性
const props = defineProps({
// 音频URL
audioUrl: {
type: String,
default: ''
},
// 视频URL
videoUrl: {
type: String,
default: ''
},
// 场景名称
sceneName: {
type: String,
default: '场景媒体'
},
// 是否自动播放
autoPlay: {
type: Boolean,
default: false
},
// 是否循环播放
loop: {
type: Boolean,
default: false
},
// 默认音量
defaultVolume: {
type: Number,
default: 0.7
},
// 显示模式: 'both'同时显示音视频, 'audio'只显示音频, 'video'只显示视频
displayMode: {
type: String,
default: 'both'
}
})
// 组件事件
const emit = defineEmits(['play', 'pause', 'ended', 'error', 'timeupdate', 'volumechange'])
// 媒体元素引用
const audioElement = ref(null)
const videoElement = ref(null)
// 用于Uniapp组件的状态同步
const uniappAudioState = ref({ playing: false, currentTime: 0, duration: 0 })
// 播放状态
const isPlaying = ref(false)
const isLoading = ref(false)
const isError = ref(false)
// 播放进度
const currentTime = ref(0)
const duration = ref(0)
const progress = ref(0)
// 音量控制
const volume = ref(props.defaultVolume)
const isMuted = ref(false)
// 显示控制
const showAudioPlayer = computed(() => {
return props.displayMode !== 'video' && props.audioUrl
})
const showVideoPlayer = computed(() => {
return props.displayMode !== 'audio' && props.videoUrl
})
// 获取当前活动的媒体元素
const activeMediaElement = computed(() => {
if (showVideoPlayer.value && videoElement.value) {
return videoElement.value
}
// 对于音频在Uniapp中我们直接使用状态管理而不是DOM元素
return null
})
// 播放/暂停控制
const togglePlay = () => {
if (showVideoPlayer.value) {
// 视频播放控制
const media = activeMediaElement.value
if (!media) return
if (isPlaying.value) {
pause()
} else {
play()
}
} else if (showAudioPlayer.value) {
// 音频播放控制
if (isPlaying.value) {
pauseAudio()
} else {
playAudio()
}
}
}
// 播放视频
const play = () => {
const media = activeMediaElement.value
if (!media) return
try {
isLoading.value = true
media.play().then(() => {
isPlaying.value = true
isLoading.value = false
emit('play')
}).catch(error => {
handleError(error)
})
} catch (error) {
handleError(error)
}
}
// 暂停视频
const pause = () => {
const media = activeMediaElement.value
if (!media) return
media.pause()
isPlaying.value = false
emit('pause')
}
// 播放音频使用Uniapp API
const playAudio = () => {
if (!audioElement.value) return
isLoading.value = true
// 使用Uniapp的音频播放API
uni.createInnerAudioContext({
src: props.audioUrl,
loop: props.loop,
volume: props.defaultVolume,
autoplay: true
})
// 模拟播放成功在实际应用中应该使用Uniapp的API回调
setTimeout(() => {
isPlaying.value = true
isLoading.value = false
emit('play')
// 模拟播放进度更新
simulateAudioProgress()
}, 500)
}
// 暂停音频使用Uniapp API
const pauseAudio = () => {
if (!audioElement.value) return
// 使用Uniapp的音频暂停API
uni.pauseVoice()
isPlaying.value = false
emit('pause')
}
// 模拟音频播放进度更新(仅用于演示)
const simulateAudioProgress = () => {
if (!isPlaying.value) return
setTimeout(() => {
currentTime.value += 1
if (currentTime.value >= duration.value) {
currentTime.value = 0
isPlaying.value = false
emit('ended')
return
}
progress.value = (currentTime.value / duration.value) * 100
emit('timeupdate', { currentTime: currentTime.value, duration: duration.value, progress: progress.value })
// 继续模拟
simulateAudioProgress()
}, 1000)
}
// 跳转到指定时间
const seekTo = (time) => {
if (showVideoPlayer.value) {
// 视频跳转
const media = activeMediaElement.value
if (!media) return
media.currentTime = time
currentTime.value = time
updateProgress()
} else if (showAudioPlayer.value) {
// 音频跳转在实际应用中应该使用Uniapp的API
currentTime.value = time
progress.value = (time / duration.value) * 100
emit('timeupdate', { currentTime: time, duration: duration.value, progress: progress.value })
}
}
// 更新进度
const updateProgress = () => {
if (showVideoPlayer.value) {
// 视频进度更新
const media = activeMediaElement.value
if (!media) return
currentTime.value = media.currentTime
progress.value = (media.currentTime / media.duration) * 100
emit('timeupdate', { currentTime: media.currentTime, duration: media.duration, progress: progress.value })
}
}
// 处理播放结束
const handleEnded = () => {
isPlaying.value = false
currentTime.value = 0
progress.value = 0
emit('ended')
// 如果循环播放,重新开始
if (props.loop) {
setTimeout(() => {
play()
}, 1000)
}
}
// 处理错误
const handleError = (error) => {
isLoading.value = false
isError.value = true
isPlaying.value = false
console.error('媒体播放错误:', error)
showFailToast({
message: '媒体播放失败',
duration: 2000
})
emit('error', error)
}
// 设置音量
const setVolume = (newVolume) => {
if (showVideoPlayer.value) {
// 视频音量控制
const media = activeMediaElement.value
if (!media) return
volume.value = newVolume
media.volume = newVolume
media.muted = newVolume === 0
isMuted.value = newVolume === 0
emit('volumechange', { volume: newVolume, muted: media.muted })
} else if (showAudioPlayer.value) {
// 音频音量控制使用Uniapp API
volume.value = newVolume
isMuted.value = newVolume === 0
// 使用Uniapp的音频音量API
uni.setVoiceVolume(newVolume)
emit('volumechange', { volume: newVolume, muted: isMuted.value })
}
}
// 切换静音
const toggleMute = () => {
if (showVideoPlayer.value) {
// 视频静音控制
const media = activeMediaElement.value
if (!media) return
if (isMuted.value) {
// 取消静音,恢复之前的音量
media.muted = false
media.volume = volume.value
isMuted.value = false
} else {
// 静音,保存当前音量
media.muted = true
isMuted.value = true
}
emit('volumechange', { volume: media.muted ? 0 : volume.value, muted: media.muted })
} else if (showAudioPlayer.value) {
// 音频静音控制使用Uniapp API
if (isMuted.value) {
// 取消静音
isMuted.value = false
uni.setVoiceVolume(volume.value)
} else {
// 静音
isMuted.value = true
uni.setVoiceVolume(0)
}
emit('volumechange', { volume: isMuted.value ? 0 : volume.value, muted: isMuted.value })
}
}
// 格式化时间
const formatTime = (seconds) => {
if (isNaN(seconds) || seconds < 0) return '0:00'
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}
// 初始化媒体元素
const initMediaElements = () => {
// 初始化音频元素
if (audioElement.value) {
audioElement.value.volume = props.defaultVolume
audioElement.value.loop = props.loop
if (props.autoPlay && showAudioPlayer.value) {
play()
}
}
// 初始化视频元素
if (videoElement.value) {
videoElement.value.volume = props.defaultVolume
videoElement.value.loop = props.loop
if (props.autoPlay && showVideoPlayer.value) {
play()
}
}
}
// 组件挂载后初始化
onMounted(() => {
initMediaElements()
})
// 组件卸载前清理
onUnmounted(() => {
// 停止播放
pause()
// 移除事件监听器
if (audioElement.value) {
audioElement.value.removeEventListener('play', handleMediaPlay)
audioElement.value.removeEventListener('pause', handleMediaPause)
audioElement.value.removeEventListener('ended', handleEnded)
audioElement.value.removeEventListener('timeupdate', updateProgress)
audioElement.value.removeEventListener('loadedmetadata', handleLoadedMetadata)
audioElement.value.removeEventListener('error', handleError)
}
if (videoElement.value) {
videoElement.value.removeEventListener('play', handleMediaPlay)
videoElement.value.removeEventListener('pause', handleMediaPause)
videoElement.value.removeEventListener('ended', handleEnded)
videoElement.value.removeEventListener('timeupdate', updateProgress)
videoElement.value.removeEventListener('loadedmetadata', handleLoadedMetadata)
videoElement.value.removeEventListener('error', handleError)
}
})
// 处理媒体播放事件
const handleMediaPlay = () => {
isPlaying.value = true
isLoading.value = false
isError.value = false
emit('play')
}
// 处理媒体暂停事件
const handleMediaPause = () => {
isPlaying.value = false
isLoading.value = false
emit('pause')
}
// 处理媒体加载完成事件
const handleLoadedMetadata = (e) => {
const media = e.target
duration.value = media.duration
isLoading.value = false
}
// 处理进度条点击
const handleProgressClick = (e) => {
const progressBar = e.currentTarget
const rect = progressBar.getBoundingClientRect()
const clickX = e.clientX - rect.left
const percentage = clickX / rect.width
const newTime = duration.value * percentage
seekTo(newTime)
}
// 处理音量条点击
const handleVolumeClick = (e) => {
const volumeBar = e.currentTarget
const rect = volumeBar.getBoundingClientRect()
const clickX = e.clientX - rect.left
const percentage = clickX / rect.width
const newVolume = Math.max(0, Math.min(1, percentage))
setVolume(newVolume)
}
// 监听显示模式变化
watch(() => props.displayMode, () => {
// 切换显示模式时,暂停当前播放的媒体
pause()
})
// 监听音视频URL变化
watch([() => props.audioUrl, () => props.videoUrl], () => {
// URL变化时重置播放状态
resetPlayer()
initMediaElements()
})
// 重置播放器
const resetPlayer = () => {
pause()
isPlaying.value = false
isLoading.value = false
isError.value = false
currentTime.value = 0
duration.value = 0
progress.value = 0
}
</script>
<template>
<div class="media-player-container">
<!-- 媒体播放器标题 -->
<div class="media-player-header">
<h3 class="media-title">{{ sceneName }}</h3>
</div>
<!-- 视频播放器 -->
<div class="video-player-wrapper" v-if="showVideoPlayer">
<div class="video-container">
<video
ref="videoElement"
class="video-element"
:src="videoUrl"
:loop="loop"
@play="handleMediaPlay"
@pause="handleMediaPause"
@ended="handleEnded"
@timeupdate="updateProgress"
@loadedmetadata="handleLoadedMetadata"
@error="handleError"
>
您的浏览器不支持视频播放
</video>
<!-- 视频加载状态 -->
<div class="loading-overlay" v-if="isLoading">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<!-- 视频错误状态 -->
<div class="error-overlay" v-if="isError">
<div class="error-icon">❌</div>
<div class="error-text">视频加载失败</div>
<button class="retry-btn" @click="play">重试</button>
</div>
</div>
</div>
<!-- 音频播放器 -->
<div class="audio-player-wrapper" v-if="showAudioPlayer">
<div class="audio-container">
<div class="audio-info">
<div class="audio-icon">🎵</div>
<div class="audio-text">
<div class="audio-name">{{ sceneName }}音频</div>
<div class="audio-time">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</div>
</div>
</div>
<div class="audio-controls">
<button class="play-btn" @click="togglePlay" :disabled="isLoading">
<span v-if="isPlaying"></span>
<span v-else></span>
</button>
</div>
</div>
</div>
<!-- 通用控制栏 -->
<div class="control-bar">
<!-- 进度条 -->
<div class="progress-container">
<div class="progress-bar" @click="handleProgressClick">
<div class="progress-filled" :style="{ width: `${progress}%` }"></div>
<div class="progress-handle" :style="{ left: `${progress}%` }"></div>
</div>
</div>
<!-- 音量控制 -->
<div class="volume-container">
<button class="volume-btn" @click="toggleMute">
<span v-if="isMuted">🔇</span>
<span v-else-if="volume < 0.5">🔉</span>
<span v-else>🔊</span>
</button>
<div class="volume-bar-container">
<div class="volume-bar" @click="handleVolumeClick">
<div class="volume-filled" :style="{ width: `${isMuted ? 0 : volume * 100}%` }"></div>
</div>
</div>
</div>
</div>
<!-- 隐藏的音频元素 -->
<view v-if="props.audioUrl" style="display: none;">
<!-- 使用uni-audio组件 -->
<uni-audio
ref="audioElement"
:src="audioUrl"
:loop="loop"
@play="handleMediaPlay"
@pause="handleMediaPause"
@ended="handleEnded"
@timeupdate="updateProgress"
@loadedmetadata="handleLoadedMetadata"
@error="handleError"
:controls="false"
:autoplay="props.autoPlay"
:muted="false"
/>
</view>
</div>
</template>
<style scoped>
.media-player-container {
width: 100%;
background-color: #f5f5f5;
border-radius: 0.15rem;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.media-player-header {
padding: 0.2rem;
background-color: #fff;
border-bottom: 1px solid #eee;
}
.media-title {
font-size: 0.3rem;
font-weight: bold;
color: #333;
margin: 0;
text-align: center;
}
/* 视频播放器样式 */
.video-player-wrapper {
width: 100%;
background-color: #000;
}
.video-container {
position: relative;
width: 100%;
padding-top: 56.25%; /* 16:9 宽高比 */
}
.video-element {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
/* 音频播放器样式 */
.audio-player-wrapper {
width: 100%;
padding: 0.2rem;
background-color: #fff;
}
.audio-container {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.2rem;
}
.audio-info {
flex: 1;
display: flex;
align-items: center;
gap: 0.2rem;
}
.audio-icon {
font-size: 0.4rem;
}
.audio-text {
flex: 1;
min-width: 0;
}
.audio-name {
font-size: 0.28rem;
color: #333;
margin-bottom: 0.05rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.audio-time {
font-size: 0.24rem;
color: #999;
}
.audio-controls {
display: flex;
align-items: center;
}
/* 控制栏样式 */
.control-bar {
display: flex;
align-items: center;
padding: 0.2rem;
background-color: #fff;
border-top: 1px solid #eee;
}
/* 进度条样式 */
.progress-container {
flex: 1;
margin-right: 0.2rem;
}
.progress-bar {
position: relative;
width: 100%;
height: 0.1rem;
background-color: #e0e0e0;
border-radius: 0.05rem;
cursor: pointer;
overflow: hidden;
}
.progress-filled {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #ff6b35;
border-radius: 0.05rem;
transition: width 0.1s linear;
}
.progress-handle {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 0.2rem;
height: 0.2rem;
background-color: #ff6b35;
border-radius: 50%;
box-shadow: 0 0 0.1rem rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: transform 0.1s ease;
}
.progress-handle:hover {
transform: translate(-50%, -50%) scale(1.2);
}
/* 音量控制样式 */
.volume-container {
display: flex;
align-items: center;
gap: 0.1rem;
width: 1.5rem;
}
.volume-btn {
background: none;
border: none;
font-size: 0.3rem;
cursor: pointer;
padding: 0.05rem;
}
.volume-bar-container {
flex: 1;
}
.volume-bar {
position: relative;
width: 100%;
height: 0.08rem;
background-color: #e0e0e0;
border-radius: 0.04rem;
cursor: pointer;
overflow: hidden;
}
.volume-filled {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: #666;
border-radius: 0.04rem;
transition: width 0.1s linear;
}
/* 按钮样式 */
.play-btn {
background: none;
border: none;
font-size: 0.4rem;
cursor: pointer;
padding: 0.1rem;
display: flex;
align-items: center;
justify-content: center;
}
.play-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 加载和错误状态 */
.loading-overlay,
.error-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
}
.loading-spinner {
width: 0.6rem;
height: 0.6rem;
border: 0.08rem solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 0.2rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text,
.error-text {
font-size: 0.28rem;
margin-top: 0.1rem;
}
.error-icon {
font-size: 0.8rem;
margin-bottom: 0.2rem;
}
.retry-btn {
margin-top: 0.3rem;
padding: 0.15rem 0.3rem;
font-size: 0.28rem;
color: #fff;
background-color: #ff6b35;
border: none;
border-radius: 0.15rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.retry-btn:hover {
background-color: #ff5216;
}
</style>