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