|
@@ -0,0 +1,347 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="page">
|
|
|
|
|
+ <view class="nav-bar">
|
|
|
|
|
+ <view class="nav-btn" @click="goBack">
|
|
|
|
|
+ <text class="nav-icon">←</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="nav-title">{{ playlist?.name || '播放列表' }}</text>
|
|
|
|
|
+ <view class="nav-btn" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <scroll-view scroll-y class="content">
|
|
|
|
|
+ <view v-if="!loading && playlist" class="playlist-header">
|
|
|
|
|
+ <view class="playlist-cover">
|
|
|
|
|
+ <text class="cover-icon">🎵</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="playlist-info">
|
|
|
|
|
+ <text class="playlist-name">{{ playlist.name }}</text>
|
|
|
|
|
+ <text class="playlist-desc">{{ playlist.description || '暂无描述' }}</text>
|
|
|
|
|
+ <text class="playlist-meta">{{ playlist.items?.length || 0 }} 首歌曲</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="action-bar">
|
|
|
|
|
+ <view class="action-btn primary" @click="playAll">
|
|
|
|
|
+ <text>播放全部</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <view class="audio-section">
|
|
|
|
|
+ <text class="section-title">歌曲列表</text>
|
|
|
|
|
+ <view v-if="!playlist?.items?.length" class="empty">
|
|
|
|
|
+ <text class="empty-icon">🎵</text>
|
|
|
|
|
+ <text class="empty-text">播放列表为空</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-else class="audio-list">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(item, index) in playlist.items"
|
|
|
|
|
+ :key="item.id"
|
|
|
|
|
+ class="audio-item"
|
|
|
|
|
+ @click="playItem(item, index)"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view class="audio-index">{{ index + 1 }}</view>
|
|
|
|
|
+ <view class="audio-content">
|
|
|
|
|
+ <text class="audio-title">{{ item.chapter?.title || '未知音频' }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="audio-delete" @click.stop="removeItem(item.id)">
|
|
|
|
|
+ <text>×</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </scroll-view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, onMounted } from 'vue';
|
|
|
|
|
+import { onLoad } from '@dcloudio/uni-app';
|
|
|
|
|
+import { useAudioStore } from '../../store/audio';
|
|
|
|
|
+import { get, post, del } from '../../utils/request';
|
|
|
|
|
+
|
|
|
|
|
+const audioStore = useAudioStore();
|
|
|
|
|
+const playlistId = ref('');
|
|
|
|
|
+const playlist = ref<any>(null);
|
|
|
|
|
+const loading = ref(false);
|
|
|
|
|
+
|
|
|
|
|
+function goBack() {
|
|
|
|
|
+ uni.navigateBack();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fetchPlaylist() {
|
|
|
|
|
+ loading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await get(`/playlists/${playlistId.value}`);
|
|
|
|
|
+ playlist.value = data;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取播放列表失败:', error);
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function playAll() {
|
|
|
|
|
+ const items = playlist.value?.items?.filter((i: any) => i.chapter?.audioUrl) || [];
|
|
|
|
|
+ if (items.length === 0) {
|
|
|
|
|
+ uni.showToast({ title: '暂无可播放的音频', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const playlistAudio = items.map((i: any) => ({
|
|
|
|
|
+ id: i.chapter.id,
|
|
|
|
|
+ title: i.chapter.title,
|
|
|
|
|
+ audioUrl: i.chapter.audioUrl,
|
|
|
|
|
+ audioDuration: i.chapter.audioDuration || 0,
|
|
|
|
|
+ wordCount: i.chapter.wordCount || 0,
|
|
|
|
|
+ }));
|
|
|
|
|
+ audioStore.setPlaylist(playlistAudio as any, 0, true);
|
|
|
|
|
+ audioStore.play(playlistAudio[0] as any);
|
|
|
|
|
+ uni.navigateTo({ url: `/pages/player/index?id=${playlistAudio[0].id}` });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function playItem(item: any, index: number) {
|
|
|
|
|
+ if (!item.chapter?.audioUrl) {
|
|
|
|
|
+ uni.showToast({ title: '该音频暂不可用', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const items = playlist.value?.items?.filter((i: any) => i.chapter?.audioUrl) || [];
|
|
|
|
|
+ const playlistAudio = items.map((i: any) => ({
|
|
|
|
|
+ id: i.chapter.id,
|
|
|
|
|
+ title: i.chapter.title,
|
|
|
|
|
+ audioUrl: i.chapter.audioUrl,
|
|
|
|
|
+ audioDuration: i.chapter.audioDuration || 0,
|
|
|
|
|
+ wordCount: i.chapter.wordCount || 0,
|
|
|
|
|
+ }));
|
|
|
|
|
+ const playIndex = items.findIndex((i: any) => i.id === item.id);
|
|
|
|
|
+ audioStore.setPlaylist(playlistAudio as any, playIndex >= 0 ? playIndex : 0, true);
|
|
|
|
|
+ audioStore.play({
|
|
|
|
|
+ id: item.chapter.id,
|
|
|
|
|
+ title: item.chapter.title,
|
|
|
|
|
+ audioUrl: item.chapter.audioUrl,
|
|
|
|
|
+ audioDuration: item.chapter.audioDuration || 0,
|
|
|
|
|
+ wordCount: item.chapter.wordCount || 0,
|
|
|
|
|
+ } as any);
|
|
|
|
|
+ uni.navigateTo({ url: `/pages/player/index?id=${item.chapter.id}` });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function removeItem(itemId: number) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '确认移除',
|
|
|
|
|
+ content: '确定要从播放列表中移除吗?',
|
|
|
|
|
+ success: async (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await del(`/playlists/${playlistId.value}/items/${itemId}`);
|
|
|
|
|
+ uni.showToast({ title: '移除成功', icon: 'success' });
|
|
|
|
|
+ await fetchPlaylist();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ uni.showToast({ title: '移除失败', icon: 'none' });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onLoad((options: any) => {
|
|
|
|
|
+ playlistId.value = options?.id || '';
|
|
|
|
|
+ if (playlistId.value) {
|
|
|
|
|
+ fetchPlaylist();
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.page {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background: #f5f5f5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ padding: 44rpx 32rpx 24rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
|
|
|
+ position: sticky;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-btn {
|
|
|
|
|
+ width: 64rpx;
|
|
|
|
|
+ height: 64rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-icon {
|
|
|
|
|
+ font-size: 40rpx;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.nav-title {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content {
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ height: calc(100vh - 160rpx);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 24rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ margin-bottom: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-cover {
|
|
|
|
|
+ width: 200rpx;
|
|
|
|
|
+ height: 200rpx;
|
|
|
|
|
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cover-icon {
|
|
|
|
|
+ font-size: 80rpx;
|
|
|
|
|
+ opacity: 0.8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-name {
|
|
|
|
|
+ font-size: 32rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ margin-bottom: 12rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-desc {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #6b7280;
|
|
|
|
|
+ margin-bottom: 8rpx;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.playlist-meta {
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-bar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ margin-bottom: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-btn {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ height: 80rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ border-radius: 40rpx;
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.action-btn.primary {
|
|
|
|
|
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audio-section {
|
|
|
|
|
+ background: #ffffff;
|
|
|
|
|
+ border-radius: 16rpx;
|
|
|
|
|
+ padding: 24rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.section-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 80rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-icon {
|
|
|
|
|
+ font-size: 80rpx;
|
|
|
|
|
+ margin-bottom: 16rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.empty-text {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audio-list {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audio-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16rpx;
|
|
|
|
|
+ padding: 16rpx;
|
|
|
|
|
+ background: #f9f9f9;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audio-index {
|
|
|
|
|
+ width: 40rpx;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 24rpx;
|
|
|
|
|
+ color: #9ca3af;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audio-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audio-title {
|
|
|
|
|
+ font-size: 28rpx;
|
|
|
|
|
+ color: #1f2937;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.audio-delete {
|
|
|
|
|
+ width: 48rpx;
|
|
|
|
|
+ height: 48rpx;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 36rpx;
|
|
|
|
|
+ color: #ef4444;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|