You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
448 lines
12 KiB
448 lines
12 KiB
<template>
|
|
<view class="novel-player-container">
|
|
<!-- 1. 小说封面图区域(适配多端屏幕比例) -->
|
|
<view class="novel-cover-wrapper">
|
|
<!-- 封面图:加载态+失败备用 -->
|
|
<image
|
|
class="novel-cover"
|
|
:class="{ 'cover-loading': isCoverLoading }"
|
|
:src="showImg('/uploads/20250918/478322390dfe8befd6fb30643e1b5cb1.png')"
|
|
mode="widthFix"
|
|
@load="isCoverLoading = false"
|
|
@error="handleCoverError"
|
|
lazy-load
|
|
></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>
|
|
|
|
<!-- 2. 小说信息区域 -->
|
|
<view class="novel-info">
|
|
<text class="novel-title">园界修真传</text>
|
|
<text class="novel-chapter">第1章:考研失败是有原因的</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>
|
|
|
|
<!-- 播放/暂停按钮 -->
|
|
<button
|
|
class="play-btn"
|
|
:disabled="isAudioLoading"
|
|
@click="togglePlayPause"
|
|
hover-class="play-btn-hover"
|
|
>
|
|
<uni-icons
|
|
:type="isPlaying ? 'pause' : 'play'"
|
|
size="24"
|
|
color="#fff"
|
|
></uni-icons>
|
|
</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>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
name: 'PlayNovel',
|
|
data() {
|
|
return {
|
|
// 图片配置
|
|
coverUrl: '/static/image.png', // 封面图路径(Uniapp 静态资源放static目录)
|
|
isCoverLoading: true, // 封面加载状态
|
|
isCoverError: false, // 封面加载失败状态
|
|
|
|
// 音频配置
|
|
audioUrl: 'https://des.dayunyuanjian.cn/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' // 默认高清
|
|
};
|
|
},
|
|
onReady() {
|
|
// Uniapp 组件就绪后获取音频实例
|
|
this.audioRef = this.$refs.audioRef;
|
|
// 初始化音量
|
|
this.audioRef.setVolume(this.currentVolume / 100);
|
|
// 初始化音频地址(可根据清晰度默认值设置)
|
|
this.switchAudioQuality({ detail: { value: 'medium' } });
|
|
},
|
|
onUnload() {
|
|
// 页面卸载时停止播放,释放资源
|
|
if (this.audioRef) {
|
|
this.audioRef.pause();
|
|
}
|
|
},
|
|
methods: {
|
|
// ---------------------- 图片相关方法 ----------------------
|
|
handleCoverError() {
|
|
this.isCoverLoading = false;
|
|
this.isCoverError = true;
|
|
},
|
|
|
|
// ---------------------- 音频核心控制 ----------------------
|
|
// 切换播放/暂停
|
|
togglePlayPause() {
|
|
if (this.isPlaying) {
|
|
this.audioRef.pause();
|
|
} else {
|
|
this.isAudioLoading = true;
|
|
this.audioRef.play().catch(err => {
|
|
console.error('播放失败:', err);
|
|
this.isAudioLoading = false;
|
|
uni.showToast({ title: '音频播放失败', icon: 'none' });
|
|
});
|
|
}
|
|
},
|
|
|
|
// 更新播放状态
|
|
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' });
|
|
},
|
|
|
|
// ---------------------- 进度条控制 ----------------------
|
|
// 实时更新进度
|
|
updateProgress() {
|
|
if (!this.isDragging) {
|
|
this.currentTime = this.audioRef.currentTime;
|
|
this.totalTime = this.audioRef.duration || 0;
|
|
this.progressPercent = (this.currentTime / this.totalTime) * 100 || 0;
|
|
}
|
|
},
|
|
|
|
// 点击进度条跳转
|
|
handleProgressClick(e) {
|
|
const containerWidth = e.currentTarget.offsetWidth;
|
|
const clickLeft = e.touches[0].clientX - e.currentTarget.offsetLeft;
|
|
const percent = (clickLeft / containerWidth) * 100;
|
|
this.setProgress(percent);
|
|
},
|
|
|
|
// 开始拖动进度条
|
|
startDragProgress() {
|
|
this.isDragging = true;
|
|
},
|
|
|
|
// 拖动进度条中
|
|
onDragProgress(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;
|
|
},
|
|
|
|
// 结束拖动进度条
|
|
endDragProgress() {
|
|
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];
|
|
// 切换音频后保持播放状态
|
|
if (this.isPlaying) {
|
|
this.audioRef.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')}`;
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 容器整体样式 */
|
|
.novel-player-container {
|
|
padding: 20rpx;
|
|
background: #595959;
|
|
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;
|
|
}
|
|
.cover-loading {
|
|
opacity: 0.5;
|
|
}
|
|
.cover-fallback {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background-color: #f5f5f5;
|
|
}
|
|
.fallback-text {
|
|
margin-top: 20rpx;
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
}
|
|
|
|
/* 小说信息样式 */
|
|
.novel-info {
|
|
margin-bottom: 30rpx;
|
|
text-align: center;
|
|
}
|
|
.novel-title {
|
|
font-size: 36rpx;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
margin-bottom: 10rpx;
|
|
display: block;
|
|
}
|
|
.novel-chapter {
|
|
font-size: 28rpx;
|
|
color: #fff;
|
|
display: block;
|
|
}
|
|
|
|
/* 音频播放器样式 */
|
|
.audio-player {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 25rpx;
|
|
}
|
|
|
|
/* 播放按钮样式 */
|
|
.play-btn {
|
|
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-container {
|
|
width: 100%;
|
|
height: 12rpx;
|
|
background-color: #eee;
|
|
border-radius: 6rpx;
|
|
position: relative;
|
|
touch-action: none; /* 防止移动端默认触摸行为 */
|
|
}
|
|
.progress-played {
|
|
height: 100%;
|
|
background-color: #32c5ff;
|
|
border-radius: 6rpx;
|
|
}
|
|
.progress-thumb {
|
|
width: 24rpx;
|
|
height: 24rpx;
|
|
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);
|
|
}
|
|
|
|
/* 时间显示样式 */
|
|
.time-display {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
.time-split {
|
|
margin: 0 10rpx;
|
|
}
|
|
|
|
/* 音量控制样式 */
|
|
.volume-control {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15rpx;
|
|
}
|
|
.volume-slider {
|
|
flex: 1;
|
|
height: 8rpx;
|
|
}
|
|
|
|
/* 清晰度选择样式 */
|
|
.quality-select {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15rpx;
|
|
font-size: 26rpx;
|
|
color: #666;
|
|
}
|
|
.quality-picker {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
color: #32c5ff;
|
|
}
|
|
.picker-icon {
|
|
margin-top: 4rpx;
|
|
}
|
|
</style>
|