mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
- New endpoints: play-by-id, add-by-id, play-playlist (load all songs at once) - Playlist 'Play All' now uses server-side bulk load (no N sequential searches) - Search/Home/Playlist views use playById instead of search-by-name - Fixed updateBotStatus timing: capture prev state before mutation - Early return on song change prevents stale pause/resume logic Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
4.1 KiB
Vue
180 lines
4.1 KiB
Vue
<template>
|
|
<div class="playlist-page">
|
|
<button class="back-btn" @click="$router.back()">
|
|
<Icon icon="mdi:arrow-left" />
|
|
返回
|
|
</button>
|
|
<div v-if="loading" class="loading">加载中...</div>
|
|
|
|
<template v-else-if="playlist">
|
|
<!-- Hero Header -->
|
|
<div class="playlist-hero">
|
|
<CoverArt :url="playlist.coverUrl" :size="200" :radius="14" :show-shadow="true" />
|
|
<div class="playlist-meta">
|
|
<h1 class="playlist-title">{{ playlist.name }}</h1>
|
|
<p class="playlist-desc" v-if="playlist.description">{{ playlist.description }}</p>
|
|
<div class="playlist-stats">
|
|
{{ songs.length }} 首歌曲
|
|
</div>
|
|
<button class="play-all-btn" @click="playAll">
|
|
<Icon icon="mdi:play" />
|
|
播放全部
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Song List -->
|
|
<div class="song-list">
|
|
<SongCard
|
|
v-for="(song, i) in songs"
|
|
:key="song.id"
|
|
:song="song"
|
|
:index="i + 1"
|
|
:active="store.currentSong?.id === song.id"
|
|
@play="store.playById(song.id, song.platform)"
|
|
@add="store.addToQueueById(song.id, song.platform)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else class="loading">歌单不存在或加载失败</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { useRoute } from 'vue-router';
|
|
import { Icon } from '@iconify/vue';
|
|
import axios from 'axios';
|
|
import { usePlayerStore } from '../stores/player.js';
|
|
import CoverArt from '../components/CoverArt.vue';
|
|
import SongCard from '../components/SongCard.vue';
|
|
|
|
const store = usePlayerStore();
|
|
const route = useRoute();
|
|
|
|
interface PlaylistDetail {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
coverUrl: string;
|
|
songCount: number;
|
|
}
|
|
|
|
interface SongItem {
|
|
id: string;
|
|
name: string;
|
|
artist: string;
|
|
album: string;
|
|
duration: number;
|
|
coverUrl: string;
|
|
platform: string;
|
|
}
|
|
|
|
const playlist = ref<PlaylistDetail | null>(null);
|
|
const songs = ref<SongItem[]>([]);
|
|
const loading = ref(true);
|
|
|
|
async function playAll() {
|
|
const id = route.params.id as string;
|
|
const platform = (route.query.platform as string) || 'netease';
|
|
await store.playPlaylist(id, platform);
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const id = route.params.id as string;
|
|
const platform = (route.query.platform as string) || 'netease';
|
|
|
|
try {
|
|
const [detailRes, songsRes] = await Promise.all([
|
|
axios.get(`/api/music/playlist/${id}/detail`, { params: { platform } }),
|
|
axios.get(`/api/music/playlist/${id}`, { params: { platform } }),
|
|
]);
|
|
playlist.value = detailRes.data.playlist;
|
|
songs.value = songsRes.data.songs;
|
|
} catch (err: any) {
|
|
console.error('Failed to load playlist:', err?.response?.status, err?.message);
|
|
playlist.value = null;
|
|
songs.value = [];
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.back-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 14px;
|
|
opacity: 0.7;
|
|
margin-bottom: 16px;
|
|
transition: opacity var(--transition-fast);
|
|
&:hover { opacity: 1; }
|
|
}
|
|
|
|
.playlist-hero {
|
|
display: flex;
|
|
gap: 32px;
|
|
margin-bottom: 36px;
|
|
}
|
|
|
|
.playlist-meta {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
}
|
|
|
|
.playlist-title {
|
|
font-size: 28px;
|
|
font-weight: 800;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.playlist-desc {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 8px;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.playlist-stats {
|
|
font-size: 12px;
|
|
color: var(--text-tertiary);
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.play-all-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 10px 28px;
|
|
background: var(--color-primary);
|
|
color: white;
|
|
border-radius: var(--radius-lg);
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
width: fit-content;
|
|
transition: transform var(--transition-fast);
|
|
|
|
&:hover { transform: scale(1.04); }
|
|
&:active { transform: scale(0.96); }
|
|
}
|
|
|
|
.song-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 60px;
|
|
color: var(--text-secondary);
|
|
}
|
|
</style>
|