2 changed files with 445 additions and 350 deletions
@ -1,448 +1,543 @@ |
|||
<template> |
|||
<view class="novel-player-container"> |
|||
<!-- 1. 小说封面图区域(适配多端屏幕比例) --> |
|||
<view class="novel-cover-wrapper"> |
|||
<!-- 封面图:加载态+失败备用 --> |
|||
<view class="player-container"> |
|||
<!-- 小说封面和标题区域 --> |
|||
<view class="novel-info"> |
|||
<image |
|||
class="novel-cover" |
|||
:class="{ 'cover-loading': isCoverLoading }" |
|||
:src="showImg('/uploads/20250918/478322390dfe8befd6fb30643e1b5cb1.png')" |
|||
mode="widthFix" |
|||
@load="isCoverLoading = false" |
|||
@error="handleCoverError" |
|||
lazy-load |
|||
class="novel-cover" |
|||
src="https://picsum.photos/300/400" |
|||
mode="widthFix" |
|||
alt="小说封面图" |
|||
></image> |
|||
<!-- 封面加载失败备用视图 --> |
|||
<view class="cover-fallback" v-if="isCoverError"> |
|||
<uni-icons type="image" size="40" color="#999"></uni-icons> |
|||
<text class="fallback-text">封面加载失败</text> |
|||
</view> |
|||
<view class="novel-title">{{ currentNovel.title }}</view> |
|||
<view class="chapter-title">{{ currentChapter.title }}</view> |
|||
</view> |
|||
|
|||
<!-- 2. 小说信息区域 --> |
|||
<view class="novel-info"> |
|||
<text class="novel-title">园界修真传</text> |
|||
<text class="novel-chapter">第1章:考研失败是有原因的</text> |
|||
|
|||
<!-- 进度条区域 --> |
|||
<view class="progress-container"> |
|||
<text class="time">{{ formatTime(currentTime) }}</text> |
|||
<view class="progress-bar"> |
|||
<view |
|||
class="progress-track" |
|||
@click="changeProgress" |
|||
> |
|||
<view |
|||
class="progress-played" |
|||
:style="{ width: progress + '%' }" |
|||
></view> |
|||
<view |
|||
class="progress-thumb" |
|||
:style="{ left: progress + '%' }" |
|||
@touchstart="startDrag" |
|||
@touchmove="dragProgress" |
|||
@touchend="endDrag" |
|||
></view> |
|||
</view> |
|||
</view> |
|||
<text class="time">{{ formatTime(duration) }}</text> |
|||
</view> |
|||
|
|||
<!-- 3. 音频核心控制区域 --> |
|||
<view class="audio-player"> |
|||
<!-- Uniapp 音频组件(替代原生audio,支持多端) --> |
|||
<uni-audio |
|||
ref="audioRef" |
|||
:src="audioUrl" |
|||
:preload="preloadMode" |
|||
:initial-time="0" |
|||
@play="updatePlayStatus(true)" |
|||
@pause="updatePlayStatus(false)" |
|||
@ended="handleAudioEnd" |
|||
@timeupdate="updateProgress" |
|||
@error="handleAudioError" |
|||
hidden |
|||
></uni-audio> |
|||
|
|||
<!-- 播放/暂停按钮 --> |
|||
|
|||
<!-- 控制按钮区域 --> |
|||
<view class="controls"> |
|||
<button |
|||
class="play-btn" |
|||
:disabled="isAudioLoading" |
|||
@click="togglePlayPause" |
|||
hover-class="play-btn-hover" |
|||
class="control-btn prev-btn" |
|||
@click="prevChapter" |
|||
:disabled="currentChapterIndex <= 0" |
|||
> |
|||
<uni-icons |
|||
:type="isPlaying ? 'pause' : 'play'" |
|||
size="24" |
|||
color="#fff" |
|||
></uni-icons> |
|||
<i class="iconfont icon-prev"></i> |
|||
<text>上一章</text> |
|||
</button> |
|||
|
|||
<!-- 进度条控制(支持点击+拖动) --> |
|||
<view class="progress-container" @click="handleProgressClick"> |
|||
<!-- 已播放进度 --> |
|||
<view |
|||
class="progress-played" |
|||
:style="{ width: `${progressPercent}%` }" |
|||
></view> |
|||
<!-- 进度滑块 --> |
|||
<view |
|||
class="progress-thumb" |
|||
:style="{ left: `${progressPercent}%` }" |
|||
@touchstart="startDragProgress" |
|||
@touchmove="onDragProgress" |
|||
@touchend="endDragProgress" |
|||
></view> |
|||
</view> |
|||
|
|||
<!-- 时间显示(已播放/总时长) --> |
|||
<view class="time-display"> |
|||
<text>{{ formatTime(currentTime) }}</text> |
|||
<text class="time-split">/</text> |
|||
<text>{{ formatTime(totalTime) }}</text> |
|||
</view> |
|||
|
|||
<!-- 音量控制 --> |
|||
<view class="volume-control"> |
|||
<uni-icons |
|||
:type="isMuted ? 'volume-off' : 'volume-up'" |
|||
size="18" |
|||
color="#666" |
|||
@click="toggleMute" |
|||
></uni-icons> |
|||
<slider |
|||
class="volume-slider" |
|||
min="0" |
|||
max="100" |
|||
:value="currentVolume" |
|||
@change="adjustVolume" |
|||
activeColor="#32c5ff" |
|||
></slider> |
|||
</view> |
|||
|
|||
<!-- 清晰度选择(模拟多音质切换) --> |
|||
<view class="quality-select"> |
|||
<text class="quality-label">清晰度:</text> |
|||
<picker |
|||
class="quality-picker" |
|||
:value="selectedQuality" |
|||
:range="qualityOptions" |
|||
:range-key="'label'" |
|||
@change="switchAudioQuality" |
|||
> |
|||
<text>{{ qualityOptions.find(item => item.value === selectedQuality).label }}</text> |
|||
<uni-icons type="down" size="14" color="#666" class="picker-icon"></uni-icons> |
|||
</picker> |
|||
|
|||
<button |
|||
class="control-btn play-btn" |
|||
@click="togglePlay" |
|||
> |
|||
<i class="iconfont" :class="isPlaying ? 'icon-pause' : 'icon-play'"></i> |
|||
</button> |
|||
|
|||
<button |
|||
class="control-btn next-btn" |
|||
@click="nextChapter" |
|||
:disabled="currentChapterIndex >= novelData.chapters.length - 1" |
|||
> |
|||
<text>下一章</text> |
|||
<i class="iconfont icon-next"></i> |
|||
</button> |
|||
</view> |
|||
|
|||
<!-- 章节列表弹窗 --> |
|||
<view class="chapter-modal" v-if="showChapterList"> |
|||
<view class="modal-content"> |
|||
<view class="modal-header"> |
|||
<text>章节列表</text> |
|||
<button class="close-btn" @click="showChapterList = false"> |
|||
<i class="iconfont icon-close"></i> |
|||
</button> |
|||
</view> |
|||
<scroll-view class="chapter-list" scroll-y> |
|||
<view |
|||
class="chapter-item" |
|||
v-for="(chapter, index) in novelData.chapters" |
|||
:key="index" |
|||
@click="switchChapter(index)" |
|||
:class="{ active: index === currentChapterIndex }" |
|||
> |
|||
<text>{{ chapter.title }}</text> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 章节列表按钮 --> |
|||
<button class="chapter-list-btn" @click="showChapterList = true"> |
|||
<i class="iconfont icon-list"></i> |
|||
</button> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'PlayNovel', |
|||
data() { |
|||
return { |
|||
// 图片配置 |
|||
coverUrl: '/static/image.png', // 封面图路径(Uniapp 静态资源放static目录) |
|||
isCoverLoading: true, // 封面加载状态 |
|||
isCoverError: false, // 封面加载失败状态 |
|||
|
|||
// 音频配置 |
|||
audioUrl: 'https://des.js-dyyj.com/data/2025/09/05/9875a62d-14ef-481e-b88f-19c061478ce6.MP3', // 音频文件路径(需替换为实际地址,支持本地/CDN) |
|||
preloadMode: 'auto', // 预加载策略:auto/metadata/none |
|||
isAudioLoading: false, // 音频加载状态 |
|||
isPlaying: false, // 播放状态 |
|||
currentTime: 0, // 当前播放时间(秒) |
|||
totalTime: 0, // 音频总时长(秒) |
|||
progressPercent: 0, // 播放进度百分比(0-100) |
|||
currentVolume: 80, // 当前音量(0-100) |
|||
isMuted: false, // 静音状态 |
|||
isDragging: false, // 进度条拖动状态 |
|||
|
|||
// 清晰度配置(需后端提供对应音质的音频地址) |
|||
qualityOptions: [ |
|||
{ label: '标准', value: 'low' }, |
|||
{ label: '高清', value: 'medium' }, |
|||
{ label: '无损', value: 'high' } |
|||
], |
|||
selectedQuality: 'medium' // 默认高清 |
|||
// 小说数据 |
|||
novelData: { |
|||
title: "三体", |
|||
chapters: [ |
|||
{ id: 1, title: "第1章 科学边界", audioUrl: "https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" }, |
|||
{ id: 2, title: "第2章 射手和农场主", audioUrl: "https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" }, |
|||
{ id: 3, title: "第3章 疯狂年代", audioUrl: "https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" }, |
|||
{ id: 4, title: "第4章 叶文洁", audioUrl: "https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" }, |
|||
{ id: 5, title: "第5章 宇宙闪烁", audioUrl: "https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" } |
|||
] |
|||
}, |
|||
currentChapterIndex: 0, |
|||
isPlaying: false, |
|||
currentTime: 0, |
|||
duration: 0, |
|||
progress: 0, |
|||
audioContext: null, |
|||
isDragging: false, |
|||
showChapterList: false |
|||
}; |
|||
}, |
|||
onReady() { |
|||
// Uniapp 组件就绪后获取音频实例 |
|||
this.audioRef = this.$refs.audioRef; |
|||
// 初始化音量 |
|||
this.audioRef.setVolume(this.currentVolume / 100); |
|||
// 初始化音频地址(可根据清晰度默认值设置) |
|||
this.switchAudioQuality({ detail: { value: 'medium' } }); |
|||
computed: { |
|||
currentNovel() { |
|||
return this.novelData; |
|||
}, |
|||
currentChapter() { |
|||
return this.novelData.chapters[this.currentChapterIndex]; |
|||
} |
|||
}, |
|||
onLoad() { |
|||
// 创建音频上下文 |
|||
this.audioContext = uni.createInnerAudioContext(); |
|||
this.audioContext.autoplay = false; |
|||
|
|||
// 设置初始音频源 |
|||
this.setAudioSource(); |
|||
|
|||
// 监听音频播放事件 |
|||
this.audioContext.onPlay(() => { |
|||
this.isPlaying = true; |
|||
}); |
|||
|
|||
this.audioContext.onPause(() => { |
|||
this.isPlaying = false; |
|||
}); |
|||
|
|||
this.audioContext.onTimeUpdate(() => { |
|||
if (!this.isDragging) { |
|||
this.currentTime = this.audioContext.currentTime; |
|||
this.duration = this.audioContext.duration; |
|||
this.progress = (this.currentTime / this.duration) * 100; |
|||
} |
|||
}); |
|||
|
|||
// 监听音频结束事件,自动播放下一章 |
|||
this.audioContext.onEnded(() => { |
|||
this.nextChapter(); |
|||
}); |
|||
|
|||
// 监听音频加载事件 |
|||
this.audioContext.onCanplay(() => { |
|||
this.duration = this.audioContext.duration; |
|||
}); |
|||
}, |
|||
onUnload() { |
|||
// 页面卸载时停止播放,释放资源 |
|||
if (this.audioRef) { |
|||
this.audioRef.pause(); |
|||
} |
|||
// 页面卸载时销毁音频上下文 |
|||
this.audioContext.destroy(); |
|||
}, |
|||
methods: { |
|||
// ---------------------- 图片相关方法 ---------------------- |
|||
handleCoverError() { |
|||
this.isCoverLoading = false; |
|||
this.isCoverError = true; |
|||
// 设置音频源 |
|||
setAudioSource() { |
|||
const wasPlaying = this.isPlaying; |
|||
this.audioContext.src = this.currentChapter.audioUrl; |
|||
|
|||
// 如果之前是播放状态,设置完源后继续播放 |
|||
if (wasPlaying) { |
|||
this.audioContext.play(); |
|||
} |
|||
}, |
|||
|
|||
// ---------------------- 音频核心控制 ---------------------- |
|||
// 切换播放/暂停 |
|||
togglePlayPause() { |
|||
|
|||
// 切换播放/暂停状态 |
|||
togglePlay() { |
|||
if (this.isPlaying) { |
|||
this.audioRef.pause(); |
|||
this.audioContext.pause(); |
|||
} else { |
|||
this.isAudioLoading = true; |
|||
this.audioRef.play().catch(err => { |
|||
console.error('播放失败:', err); |
|||
this.isAudioLoading = false; |
|||
uni.showToast({ title: '音频播放失败', icon: 'none' }); |
|||
}); |
|||
this.audioContext.play(); |
|||
} |
|||
}, |
|||
|
|||
// 更新播放状态 |
|||
updatePlayStatus(isPlaying) { |
|||
this.isPlaying = isPlaying; |
|||
this.isAudioLoading = false; |
|||
}, |
|||
|
|||
// 音频播放完毕(可扩展下一章逻辑) |
|||
handleAudioEnd() { |
|||
this.isPlaying = false; |
|||
this.currentTime = 0; |
|||
this.progressPercent = 0; |
|||
uni.showToast({ title: '本章播放完毕', icon: 'none' }); |
|||
// 如需自动下一章,可在此处调用章节切换逻辑 |
|||
}, |
|||
|
|||
// 音频加载失败 |
|||
handleAudioError() { |
|||
this.isAudioLoading = false; |
|||
uni.showToast({ title: '音频加载失败', icon: 'none' }); |
|||
|
|||
// 上一章 |
|||
prevChapter() { |
|||
if (this.currentChapterIndex > 0) { |
|||
this.currentChapterIndex--; |
|||
this.setAudioSource(); |
|||
this.audioContext.play(); |
|||
} |
|||
}, |
|||
|
|||
// ---------------------- 进度条控制 ---------------------- |
|||
// 实时更新进度 |
|||
updateProgress() { |
|||
if (!this.isDragging) { |
|||
this.currentTime = this.audioRef.currentTime; |
|||
this.totalTime = this.audioRef.duration || 0; |
|||
this.progressPercent = (this.currentTime / this.totalTime) * 100 || 0; |
|||
|
|||
// 下一章 |
|||
nextChapter() { |
|||
if (this.currentChapterIndex < this.novelData.chapters.length - 1) { |
|||
this.currentChapterIndex++; |
|||
this.setAudioSource(); |
|||
this.audioContext.play(); |
|||
} |
|||
}, |
|||
|
|||
// 点击进度条跳转 |
|||
handleProgressClick(e) { |
|||
const containerWidth = e.currentTarget.offsetWidth; |
|||
const clickLeft = e.touches[0].clientX - e.currentTarget.offsetLeft; |
|||
const percent = (clickLeft / containerWidth) * 100; |
|||
this.setProgress(percent); |
|||
|
|||
// 切换到指定章节 |
|||
switchChapter(index) { |
|||
this.currentChapterIndex = index; |
|||
this.setAudioSource(); |
|||
this.audioContext.play(); |
|||
this.showChapterList = false; |
|||
}, |
|||
|
|||
|
|||
// 格式化时间(秒 -> mm:ss) |
|||
formatTime(seconds) { |
|||
if (isNaN(seconds)) return "00:00"; |
|||
const mins = Math.floor(seconds / 60); |
|||
const secs = Math.floor(seconds % 60); |
|||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
|||
}, |
|||
|
|||
// 开始拖动进度条 |
|||
startDragProgress() { |
|||
startDrag() { |
|||
this.isDragging = true; |
|||
}, |
|||
|
|||
// 拖动进度条中 |
|||
onDragProgress(e) { |
|||
|
|||
// 拖动进度条 |
|||
dragProgress(e) { |
|||
if (!this.isDragging) return; |
|||
const containerWidth = e.currentTarget.offsetWidth; |
|||
const dragLeft = e.touches[0].clientX - e.currentTarget.offsetLeft; |
|||
let percent = (dragLeft / containerWidth) * 100; |
|||
// 限制进度在0-100之间 |
|||
percent = Math.max(0, Math.min(100, percent)); |
|||
this.progressPercent = percent; |
|||
|
|||
// 获取进度条宽度和点击位置 |
|||
const progressBarWidth = e.currentTarget.offsetWidth; |
|||
const touchX = e.touches[0].clientX - e.currentTarget.offsetLeft; |
|||
|
|||
// 计算进度百分比 |
|||
let percent = (touchX / progressBarWidth) * 100; |
|||
percent = Math.max(0, Math.min(100, percent)); // 限制在0-100之间 |
|||
|
|||
this.progress = percent; |
|||
this.currentTime = (percent / 100) * this.duration; |
|||
}, |
|||
|
|||
|
|||
// 结束拖动进度条 |
|||
endDragProgress() { |
|||
endDrag() { |
|||
if (!this.isDragging) return; |
|||
|
|||
this.isDragging = false; |
|||
this.setProgress(this.progressPercent); |
|||
}, |
|||
|
|||
// 设置进度(通用方法) |
|||
setProgress(percent) { |
|||
const targetTime = (percent / 100) * this.totalTime; |
|||
this.audioRef.seek(targetTime); |
|||
this.currentTime = targetTime; |
|||
this.progressPercent = percent; |
|||
}, |
|||
|
|||
// ---------------------- 音量控制 ---------------------- |
|||
// 切换静音 |
|||
toggleMute() { |
|||
this.isMuted = !this.isMuted; |
|||
this.audioRef.setMuted(this.isMuted); |
|||
}, |
|||
|
|||
// 调节音量 |
|||
adjustVolume(e) { |
|||
this.currentVolume = e.detail.value; |
|||
this.audioRef.setVolume(this.currentVolume / 100); |
|||
// 调节音量时自动取消静音 |
|||
if (this.isMuted && this.currentVolume > 0) { |
|||
this.isMuted = false; |
|||
this.audioRef.setMuted(false); |
|||
} |
|||
}, |
|||
|
|||
// ---------------------- 清晰度切换 ---------------------- |
|||
switchAudioQuality(e) { |
|||
const quality = e.detail.value; |
|||
this.selectedQuality = quality; |
|||
// 此处需替换为实际音质对应的音频地址(示例格式) |
|||
const audioMap = { |
|||
low: '/static/audio/chapter1-low.mp3', |
|||
medium: '/static/audio/chapter1-medium.mp3', |
|||
high: '/static/audio/chapter1-high.mp3' |
|||
}; |
|||
this.audioUrl = audioMap[quality]; |
|||
// 切换音频后保持播放状态 |
|||
// 设置音频进度 |
|||
this.audioContext.seek(this.currentTime); |
|||
|
|||
// 如果之前是播放状态,继续播放 |
|||
if (this.isPlaying) { |
|||
this.audioRef.play(); |
|||
this.audioContext.play(); |
|||
} |
|||
}, |
|||
|
|||
// ---------------------- 工具方法 ---------------------- |
|||
// 格式化时间(秒 → 分:秒,如 8:16) |
|||
formatTime(seconds) { |
|||
if (!seconds) return '00:00'; |
|||
const min = Math.floor(seconds / 60); |
|||
const sec = Math.floor(seconds % 60); |
|||
// 补零处理(如 1 → 01) |
|||
return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; |
|||
|
|||
// 点击进度条改变进度 |
|||
changeProgress(e) { |
|||
if (this.isDragging) return; |
|||
|
|||
// 获取进度条宽度和点击位置 |
|||
const progressBarWidth = e.currentTarget.offsetWidth; |
|||
const touchX = e.touches ? e.touches[0].clientX : e.clientX; |
|||
const clickX = touchX - e.currentTarget.offsetLeft; |
|||
|
|||
// 计算进度百分比 |
|||
let percent = (clickX / progressBarWidth) * 100; |
|||
percent = Math.max(0, Math.min(100, percent)); // 限制在0-100之间 |
|||
|
|||
this.progress = percent; |
|||
this.currentTime = (percent / 100) * this.duration; |
|||
this.audioContext.seek(this.currentTime); |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* 容器整体样式 */ |
|||
.novel-player-container { |
|||
padding: 20rpx; |
|||
background: #595959; |
|||
.player-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
/* 封面图样式 */ |
|||
.novel-cover-wrapper { |
|||
width: 100%; |
|||
border-radius: 20rpx; |
|||
overflow: hidden; |
|||
margin-bottom: 30rpx; |
|||
position: relative; |
|||
} |
|||
.novel-cover { |
|||
width: 100%; |
|||
background-color: #f5f5f5; |
|||
padding: 20rpx; |
|||
box-sizing: border-box; |
|||
} |
|||
.cover-loading { |
|||
opacity: 0.5; |
|||
} |
|||
.cover-fallback { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
|
|||
.novel-info { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background-color: #f5f5f5; |
|||
} |
|||
.fallback-text { |
|||
margin-top: 20rpx; |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
margin-top: 60rpx; |
|||
width: 100%; |
|||
} |
|||
|
|||
/* 小说信息样式 */ |
|||
.novel-info { |
|||
.novel-cover { |
|||
width: 240rpx; |
|||
height: 320rpx; |
|||
border-radius: 16rpx; |
|||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15); |
|||
margin-bottom: 30rpx; |
|||
text-align: center; |
|||
} |
|||
|
|||
.novel-title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #fff; |
|||
color: #333; |
|||
margin-bottom: 10rpx; |
|||
display: block; |
|||
text-align: center; |
|||
} |
|||
.novel-chapter { |
|||
|
|||
.chapter-title { |
|||
font-size: 28rpx; |
|||
color: #fff; |
|||
display: block; |
|||
color: #666; |
|||
margin-bottom: 60rpx; |
|||
text-align: center; |
|||
max-width: 80%; |
|||
white-space: nowrap; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
/* 音频播放器样式 */ |
|||
.audio-player { |
|||
.progress-container { |
|||
width: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 25rpx; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 0 20rpx; |
|||
box-sizing: border-box; |
|||
margin-bottom: 80rpx; |
|||
} |
|||
|
|||
/* 播放按钮样式 */ |
|||
.play-btn { |
|||
.time { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
background-color: #32c5ff; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
.play-btn-hover { |
|||
background-color: #28a4e0; |
|||
|
|||
.progress-bar { |
|||
flex: 1; |
|||
padding: 0 20rpx; |
|||
} |
|||
|
|||
/* 进度条样式 */ |
|||
.progress-container { |
|||
width: 100%; |
|||
height: 12rpx; |
|||
background-color: #eee; |
|||
border-radius: 6rpx; |
|||
.progress-track { |
|||
height: 8rpx; |
|||
background-color: #e0e0e0; |
|||
border-radius: 4rpx; |
|||
position: relative; |
|||
touch-action: none; /* 防止移动端默认触摸行为 */ |
|||
} |
|||
|
|||
.progress-played { |
|||
height: 100%; |
|||
background-color: #32c5ff; |
|||
border-radius: 6rpx; |
|||
background-color: #007aff; |
|||
border-radius: 4rpx; |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
} |
|||
|
|||
.progress-thumb { |
|||
width: 24rpx; |
|||
height: 24rpx; |
|||
background-color: #007aff; |
|||
border-radius: 50%; |
|||
background-color: #32c5ff; |
|||
position: absolute; |
|||
top: 50%; |
|||
transform: translateY(-50%); |
|||
margin-left: -12rpx; |
|||
box-shadow: 0 0 10rpx rgba(50, 197, 255, 0.5); |
|||
transform: translate(-50%, -50%); |
|||
box-shadow: 0 2rpx 8rpx rgba(0, 122, 255, 0.5); |
|||
} |
|||
|
|||
/* 时间显示样式 */ |
|||
.time-display { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
.controls { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
justify-content: space-around; |
|||
width: 100%; |
|||
margin-bottom: 120rpx; |
|||
} |
|||
.time-split { |
|||
margin: 0 10rpx; |
|||
|
|||
.control-btn { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: none; |
|||
border: none; |
|||
padding: 0; |
|||
margin: 0 20rpx; |
|||
} |
|||
|
|||
.control-btn::after { |
|||
border: none; |
|||
} |
|||
|
|||
/* 音量控制样式 */ |
|||
.volume-control { |
|||
.prev-btn, .next-btn { |
|||
color: #666; |
|||
font-size: 24rpx; |
|||
} |
|||
|
|||
.play-btn { |
|||
width: 120rpx; |
|||
height: 120rpx; |
|||
background-color: #007aff; |
|||
border-radius: 50%; |
|||
color: white; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15rpx; |
|||
justify-content: center; |
|||
} |
|||
.volume-slider { |
|||
flex: 1; |
|||
height: 8rpx; |
|||
|
|||
.iconfont { |
|||
font-size: 48rpx; |
|||
} |
|||
|
|||
.play-btn .iconfont { |
|||
font-size: 56rpx; |
|||
} |
|||
|
|||
/* 清晰度选择样式 */ |
|||
.quality-select { |
|||
.prev-btn .iconfont, .next-btn .iconfont { |
|||
margin: 8rpx 0; |
|||
} |
|||
|
|||
.chapter-list-btn { |
|||
position: fixed; |
|||
bottom: 40rpx; |
|||
right: 40rpx; |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
background-color: #007aff; |
|||
color: white; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15rpx; |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
justify-content: center; |
|||
box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3); |
|||
border: none; |
|||
} |
|||
.quality-picker { |
|||
|
|||
.chapter-list-btn::after { |
|||
border: none; |
|||
} |
|||
|
|||
.chapter-modal { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
z-index: 999; |
|||
} |
|||
|
|||
.modal-content { |
|||
width: 70%; |
|||
max-width: 500rpx; |
|||
height: 100%; |
|||
background-color: white; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.modal-header { |
|||
height: 80rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8rpx; |
|||
color: #32c5ff; |
|||
justify-content: space-between; |
|||
padding: 0 30rpx; |
|||
border-bottom: 1rpx solid #eee; |
|||
font-size: 30rpx; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.close-btn { |
|||
background: none; |
|||
border: none; |
|||
color: #999; |
|||
font-size: 36rpx; |
|||
} |
|||
.picker-icon { |
|||
margin-top: 4rpx; |
|||
|
|||
.close-btn::after { |
|||
border: none; |
|||
} |
|||
|
|||
.chapter-list { |
|||
flex: 1; |
|||
padding: 20rpx 0; |
|||
} |
|||
|
|||
.chapter-item { |
|||
padding: 24rpx 30rpx; |
|||
font-size: 28rpx; |
|||
color: #333; |
|||
border-bottom: 1rpx solid #f5f5f5; |
|||
} |
|||
|
|||
.chapter-item.active { |
|||
background-color: #f0f7ff; |
|||
color: #007aff; |
|||
} |
|||
|
|||
/* 图标字体样式 */ |
|||
@font-face { |
|||
font-family: 'iconfont'; |
|||
src: url('//at.alicdn.com/t/font_2493751_2x4w6q4k59l.ttf') format('truetype'); |
|||
} |
|||
|
|||
.iconfont { |
|||
font-family: "iconfont" !important; |
|||
font-size: 16px; |
|||
font-style: normal; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
} |
|||
|
|||
.icon-play:before { |
|||
content: "\e61c"; |
|||
} |
|||
|
|||
.icon-pause:before { |
|||
content: "\e61d"; |
|||
} |
|||
|
|||
.icon-prev:before { |
|||
content: "\e61e"; |
|||
} |
|||
|
|||
.icon-next:before { |
|||
content: "\e61f"; |
|||
} |
|||
|
|||
.icon-list:before { |
|||
content: "\e620"; |
|||
} |
|||
|
|||
.icon-close:before { |
|||
content: "\e621"; |
|||
} |
|||
</style> |
|||
</style> |
|||
|
Loading…
Reference in new issue