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

<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>