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