Files
teamspeak-music-bot/web/src/views/Playlist.vue
T
saopig1andClaude Opus 4.6 ee5a75ab5e fix: add play mode toggle, wire play history API, add back navigation buttons
- 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>
2026-03-30 02:16:18 +08:00

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>