802 lines
18 KiB
Vue
802 lines
18 KiB
Vue
<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> |