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.
 
 
 
 

543 lines
12 KiB

<template>
<view class="player-container">
<!-- 小说封面和标题区域 -->
<view class="novel-info">
<image
class="novel-cover"
src="https://picsum.photos/300/400"
mode="widthFix"
alt="小说封面图"
></image>
<view class="novel-title">{{ currentNovel.title }}</view>
<view class="chapter-title">{{ currentChapter.title }}</view>
</view>
<!-- 进度条区域 -->
<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>
<!-- 控制按钮区域 -->
<view class="controls">
<button
class="control-btn prev-btn"
@click="prevChapter"
:disabled="currentChapterIndex <= 0"
>
<i class="iconfont icon-prev"></i>
<text>上一章</text>
</button>
<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 {
data() {
return {
// 小说数据
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.dayunyuanjian.cn/data/2025/09/05/286e6a8d-4433-4d69-b705-74b3f4237667.MP3" },
{ id: 3, title: "第3章 疯狂年代", audioUrl: "https://des.dayunyuanjian.cn/data/2025/09/05/5d7caee5-ce7f-4e55-bf71-e574b486473c.MP3" },
{ id: 4, title: "第4章 叶文洁", audioUrl: "https://des.dayunyuanjian.cn/data/2025/09/05/9875a62d-14ef-481e-b88f-19c061478ce6.MP3" },
{ id: 5, title: "第5章 宇宙闪烁", audioUrl: "https://des.dayunyuanjian.cn/data/2025/09/05/fac61c02-6cfd-41bf-9270-0ecd69881da2.MP3" }
]
},
currentChapterIndex: 0,
isPlaying: false,
currentTime: 0,
duration: 0,
progress: 0,
audioContext: null,
isDragging: false,
showChapterList: false
};
},
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() {
// 页面卸载时销毁音频上下文
this.audioContext.destroy();
},
methods: {
// 设置音频源
setAudioSource() {
const wasPlaying = this.isPlaying;
this.audioContext.src = this.currentChapter.audioUrl;
// 如果之前是播放状态,设置完源后继续播放
if (wasPlaying) {
this.audioContext.play();
}
},
// 切换播放/暂停状态
togglePlay() {
if (this.isPlaying) {
this.audioContext.pause();
} else {
this.audioContext.play();
}
},
// 上一章
prevChapter() {
if (this.currentChapterIndex > 0) {
this.currentChapterIndex--;
this.setAudioSource();
this.audioContext.play();
}
},
// 下一章
nextChapter() {
if (this.currentChapterIndex < this.novelData.chapters.length - 1) {
this.currentChapterIndex++;
this.setAudioSource();
this.audioContext.play();
}
},
// 切换到指定章节
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')}`;
},
// 开始拖动进度条
startDrag() {
this.isDragging = true;
},
// 拖动进度条
dragProgress(e) {
if (!this.isDragging) return;
// 获取进度条宽度和点击位置
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;
},
// 结束拖动进度条
endDrag() {
if (!this.isDragging) return;
this.isDragging = false;
// 设置音频进度
this.audioContext.seek(this.currentTime);
// 如果之前是播放状态,继续播放
if (this.isPlaying) {
this.audioContext.play();
}
},
// 点击进度条改变进度
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>
.player-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
box-sizing: border-box;
}
.novel-info {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 60rpx;
width: 100%;
}
.novel-cover {
width: 240rpx;
height: 320rpx;
border-radius: 16rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
margin-bottom: 30rpx;
}
.novel-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
text-align: center;
}
.chapter-title {
font-size: 28rpx;
color: #666;
margin-bottom: 60rpx;
text-align: center;
max-width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.progress-container {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20rpx;
box-sizing: border-box;
margin-bottom: 80rpx;
}
.time {
font-size: 24rpx;
color: #999;
width: 80rpx;
}
.progress-bar {
flex: 1;
padding: 0 20rpx;
}
.progress-track {
height: 8rpx;
background-color: #e0e0e0;
border-radius: 4rpx;
position: relative;
}
.progress-played {
height: 100%;
background-color: #007aff;
border-radius: 4rpx;
position: absolute;
left: 0;
top: 0;
}
.progress-thumb {
width: 24rpx;
height: 24rpx;
background-color: #007aff;
border-radius: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 2rpx 8rpx rgba(0, 122, 255, 0.5);
}
.controls {
display: flex;
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;
}
.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;
justify-content: center;
}
.iconfont {
font-size: 48rpx;
}
.play-btn .iconfont {
font-size: 56rpx;
}
.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;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3);
border: none;
}
.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;
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;
}
.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>