mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 13:02:49 +08:00
- Add play mode cycle button (seq/loop/random/rloop) to Player bar - Wire GET /api/player/:botId/history to query database instead of returning empty array - Pass database instance from server.ts to createPlayerRouter - Add back navigation button to Playlist, Lyrics, History, Search, and Settings views - SongCard already had dblclick-to-play (no change needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
182 lines
4.1 KiB
Vue
182 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.play(song.name, song.platform)"
|
|
@add="store.addToQueue(song.name, 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() {
|
|
if (songs.value.length > 0) {
|
|
const first = songs.value[0];
|
|
await store.play(first.name, first.platform);
|
|
for (let i = 1; i < songs.value.length; i++) {
|
|
await store.addToQueue(songs.value[i].name, songs.value[i].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 {
|
|
// Ignore if API not ready
|
|
} 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>
|