|
@@ -51,13 +51,13 @@
|
|
|
|
|
|
|
|
<!-- 播放控制 -->
|
|
<!-- 播放控制 -->
|
|
|
<view class="controls">
|
|
<view class="controls">
|
|
|
- <!-- 播放速度 -->
|
|
|
|
|
- <view class="control-btn" @click="showRatePicker = true">
|
|
|
|
|
- <text class="control-text">{{ playRate }}x</text>
|
|
|
|
|
|
|
+ <!-- 随机播放 -->
|
|
|
|
|
+ <view class="control-btn" :class="{ active: shuffleMode }" @click="toggleShuffle">
|
|
|
|
|
+ <text class="control-icon">🔀</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 上一首 -->
|
|
<!-- 上一首 -->
|
|
|
- <view class="control-btn" :class="{ disabled: !hasPrev }" @click="playPrev">
|
|
|
|
|
|
|
+ <view class="control-btn" :class="{ disabled: !hasPrev && !shuffleMode }" @click="playPrev">
|
|
|
<text class="control-icon">⏮</text>
|
|
<text class="control-icon">⏮</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
@@ -67,37 +67,59 @@
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 下一首 -->
|
|
<!-- 下一首 -->
|
|
|
- <view class="control-btn" :class="{ disabled: !hasNext }" @click="playNext">
|
|
|
|
|
|
|
+ <view class="control-btn" :class="{ disabled: !hasNext && !shuffleMode }" @click="playNext">
|
|
|
<text class="control-icon">⏭</text>
|
|
<text class="control-icon">⏭</text>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 收藏 -->
|
|
|
|
|
- <view class="control-btn" @click="toggleFavorite">
|
|
|
|
|
- <text class="control-icon">{{ audio?.isFavorite ? '❤️' : '🤍' }}</text>
|
|
|
|
|
- </view>
|
|
|
|
|
-
|
|
|
|
|
- <!-- 音质 -->
|
|
|
|
|
- <view class="control-btn" @click="showQualityPicker = true">
|
|
|
|
|
- <text class="control-text">{{ audioQuality }}</text>
|
|
|
|
|
|
|
+ <!-- 循环模式 -->
|
|
|
|
|
+ <view class="control-btn" :class="{ active: loopMode !== 'off' }" @click="toggleLoop">
|
|
|
|
|
+ <text class="control-icon">{{ loopMode === 'single' ? '🔂' : '🔁' }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 底部操作 -->
|
|
<!-- 底部操作 -->
|
|
|
<view class="bottom-actions">
|
|
<view class="bottom-actions">
|
|
|
- <button class="action-btn" @click="showTimerPicker = true">
|
|
|
|
|
- <text class="action-icon">⏰</text>
|
|
|
|
|
- <text class="action-text">{{ timerRemaining > 0 ? formatTimer(timerRemaining) : '定时' }}</text>
|
|
|
|
|
|
|
+ <button class="action-btn" @click="showRatePicker = true">
|
|
|
|
|
+ <text class="action-icon">⏱</text>
|
|
|
|
|
+ <text class="action-text">{{ playRate }}x</text>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button class="action-btn" @click="toggleFavorite">
|
|
|
|
|
+ <text class="action-icon">{{ audio?.isFavorite ? '❤️' : '🤍' }}</text>
|
|
|
|
|
+ <text class="action-text">收藏</text>
|
|
|
</button>
|
|
</button>
|
|
|
<button class="action-btn" @click="handleDownload">
|
|
<button class="action-btn" @click="handleDownload">
|
|
|
<text class="action-icon">⬇</text>
|
|
<text class="action-icon">⬇</text>
|
|
|
<text class="action-text">下载</text>
|
|
<text class="action-text">下载</text>
|
|
|
</button>
|
|
</button>
|
|
|
- <button class="action-btn" @click="handleShare">
|
|
|
|
|
- <text class="action-icon">↗</text>
|
|
|
|
|
- <text class="action-text">分享</text>
|
|
|
|
|
|
|
+ <button class="action-btn" @click="showTimerPicker = true">
|
|
|
|
|
+ <text class="action-icon">⏰</text>
|
|
|
|
|
+ <text class="action-text">{{ timerRemaining > 0 ? formatTimer(timerRemaining) : '定时' }}</text>
|
|
|
</button>
|
|
</button>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
|
|
+ <!-- 播放列表 -->
|
|
|
|
|
+ <view class="playlist-section">
|
|
|
|
|
+ <view class="playlist-header" @click="showPlaylist = !showPlaylist">
|
|
|
|
|
+ <text class="playlist-title">📋 播放列表 ({{ playlist.length }})</text>
|
|
|
|
|
+ <text class="playlist-toggle">{{ showPlaylist ? '▼' : '▶' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="showPlaylist" class="playlist-content">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(item, index) in playlist"
|
|
|
|
|
+ :key="item.id || index"
|
|
|
|
|
+ class="playlist-item"
|
|
|
|
|
+ :class="{ playing: index === currentIndex }"
|
|
|
|
|
+ @click="playFromPlaylist(index)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text class="playlist-item-icon">{{ index === currentIndex ? (isPlaying ? '▶' : '⏸') : '🎵' }}</text>
|
|
|
|
|
+ <text class="playlist-item-title">{{ item.title }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="playlist.length === 0" class="playlist-empty">
|
|
|
|
|
+ <text>播放列表为空</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
<!-- 定时关闭选择器 -->
|
|
<!-- 定时关闭选择器 -->
|
|
|
<view v-if="showTimerPicker" class="rate-picker" @click="showTimerPicker = false">
|
|
<view v-if="showTimerPicker" class="rate-picker" @click="showTimerPicker = false">
|
|
|
<view class="rate-content" @click.stop>
|
|
<view class="rate-content" @click.stop>
|
|
@@ -217,6 +239,7 @@ const showRatePicker = ref(false);
|
|
|
const showTimerPicker = ref(false);
|
|
const showTimerPicker = ref(false);
|
|
|
const showSharePanel = ref(false);
|
|
const showSharePanel = ref(false);
|
|
|
const showQualityPicker = ref(false);
|
|
const showQualityPicker = ref(false);
|
|
|
|
|
+const showPlaylist = ref(false); // 播放列表展开/收起
|
|
|
const savedProgress = ref(0); // 保存的历史进度
|
|
const savedProgress = ref(0); // 保存的历史进度
|
|
|
const showResumePrompt = ref(false); // 显示继续播放提示
|
|
const showResumePrompt = ref(false); // 显示继续播放提示
|
|
|
const shareCard = ref({ title: '', description: '', imageUrl: '', link: '' });
|
|
const shareCard = ref({ title: '', description: '', imageUrl: '', link: '' });
|
|
@@ -247,6 +270,10 @@ const duration = computed(() => audioStore.duration);
|
|
|
const playRate = computed(() => audioStore.playRate);
|
|
const playRate = computed(() => audioStore.playRate);
|
|
|
const hasPrev = computed(() => audioStore.hasPrev);
|
|
const hasPrev = computed(() => audioStore.hasPrev);
|
|
|
const hasNext = computed(() => audioStore.hasNext);
|
|
const hasNext = computed(() => audioStore.hasNext);
|
|
|
|
|
+const loopMode = computed(() => audioStore.loopMode);
|
|
|
|
|
+const shuffleMode = computed(() => audioStore.shuffleMode);
|
|
|
|
|
+const playlist = computed(() => audioStore.playlist);
|
|
|
|
|
+const currentIndex = computed(() => audioStore.currentIndex);
|
|
|
|
|
|
|
|
// 播放进度自动保存定时器
|
|
// 播放进度自动保存定时器
|
|
|
let progressSaveTimer: number | null = null;
|
|
let progressSaveTimer: number | null = null;
|
|
@@ -406,7 +433,7 @@ async function fetchPlaylist() {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 设置播放列表(不自动播放,因为已经在 fetchAudio 中播放了当前音频)
|
|
// 设置播放列表(不自动播放,因为已经在 fetchAudio 中播放了当前音频)
|
|
|
- const index = result.list.findIndex(item => item._id === audioId.value);
|
|
|
|
|
|
|
+ const index = result.list.findIndex(item => (item.id || item._id) === audioId.value);
|
|
|
if (index >= 0) {
|
|
if (index >= 0) {
|
|
|
audioStore.setPlaylist(result.list, index, false);
|
|
audioStore.setPlaylist(result.list, index, false);
|
|
|
}
|
|
}
|
|
@@ -447,12 +474,28 @@ function playPrev() {
|
|
|
|
|
|
|
|
// 下一首
|
|
// 下一首
|
|
|
function playNext() {
|
|
function playNext() {
|
|
|
- if (hasNext.value) {
|
|
|
|
|
|
|
+ if (hasNext.value || shuffleMode.value) {
|
|
|
audioStore.playNext();
|
|
audioStore.playNext();
|
|
|
audio.value = audioStore.currentAudio;
|
|
audio.value = audioStore.currentAudio;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 切换循环模式
|
|
|
|
|
+function toggleLoop() {
|
|
|
|
|
+ audioStore.toggleLoopMode();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 切换随机播放
|
|
|
|
|
+function toggleShuffle() {
|
|
|
|
|
+ audioStore.toggleShuffleMode();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 从播放列表选择播放
|
|
|
|
|
+function playFromPlaylist(index: number) {
|
|
|
|
|
+ audioStore.setPlaylist(playlist.value, index, true);
|
|
|
|
|
+ audio.value = audioStore.currentAudio;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 设置播放速度
|
|
// 设置播放速度
|
|
|
async function handleSetRate(rate: number) {
|
|
async function handleSetRate(rate: number) {
|
|
|
audioStore.setPlayRate(rate);
|
|
audioStore.setPlayRate(rate);
|
|
@@ -704,6 +747,22 @@ function copyShareLink() {
|
|
|
background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
|
|
background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
|
|
|
padding-bottom: constant(safe-area-inset-bottom);
|
|
padding-bottom: constant(safe-area-inset-bottom);
|
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
padding-bottom: env(safe-area-inset-bottom);
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 毛玻璃背景效果 */
|
|
|
|
|
+.page::before {
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background: inherit;
|
|
|
|
|
+ filter: blur(100px);
|
|
|
|
|
+ opacity: 0.3;
|
|
|
|
|
+ z-index: -1;
|
|
|
|
|
+ pointer-events: none;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.nav-bar {
|
|
.nav-bar {
|
|
@@ -839,6 +898,10 @@ function copyShareLink() {
|
|
|
opacity: 0.3;
|
|
opacity: 0.3;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.control-btn.active {
|
|
|
|
|
+ color: #4F46E5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.control-text {
|
|
.control-text {
|
|
|
font-size: 24rpx;
|
|
font-size: 24rpx;
|
|
|
color: #ffffff;
|
|
color: #ffffff;
|
|
@@ -1130,4 +1193,78 @@ function copyShareLink() {
|
|
|
.share-action-icon {
|
|
.share-action-icon {
|
|
|
font-size: 32rpx;
|
|
font-size: 32rpx;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 播放列表 */
|
|
|
|
|
+.playlist-section {
|
|
|
|
|
+ margin: 0 32rpx;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-toggle {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-content {
|
|
|
|
|
+ max-height: 400rpx;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
|
|
+ border-top: 1rpx solid rgba(255, 255, 255, 0.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-item.playing {
|
|
|
|
|
+ background: rgba(79, 70, 229, 0.2);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-item-icon {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ width: 40rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-item-title {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-empty {
|
|
|
|
|
+ padding: 40rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 按钮点击动画 */
|
|
|
|
|
+.control-btn, .play-btn, .action-btn {
|
|
|
|
|
+ transition: transform 0.1s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.control-btn:active, .play-btn:active, .action-btn:active {
|
|
|
|
|
+ transform: scale(0.9);
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|