mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 10:48:05 +08:00
feat: 跨平台持久化与版本管理优化
- Cookie 存储从 temp_dir 迁移至 Tauri app_data_dir,兼容 Linux - 简单统一风格,UI优化 - recentLocal 播放历史持久化到 localStorage - 添加设置界面可以修改简单的设置
This commit is contained in:
@ -1,83 +1,138 @@
|
||||
<template>
|
||||
<div class="p-8 text-white">
|
||||
<button @click="$router.back()" class="mb-4 text-gray-400 hover:text-white transition">
|
||||
<div class="p-8 text-content">
|
||||
<button @click="$router.back()" class="mb-4 text-content-2 hover:text-content transition">
|
||||
← 返回
|
||||
</button>
|
||||
|
||||
<!-- 歌单信息 -->
|
||||
<div v-if="playlist" class="flex gap-6 mb-8">
|
||||
<img :src="playlist.coverImgUrl" class="w-40 h-40 rounded-xl object-cover shadow-lg" />
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ playlist.name }}</h1>
|
||||
<p class="text-sm text-gray-400 mt-2">{{ playlist.description }}</p>
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{ playlist.trackCount }} 首歌曲 · 播放 {{ playlist.playCount }} 次
|
||||
</p>
|
||||
<button
|
||||
@click="playAll"
|
||||
class="mt-4 px-4 py-2 bg-green-500 hover:bg-green-600 rounded-full text-white font-medium transition"
|
||||
>
|
||||
播放全部
|
||||
</button>
|
||||
<img :src="playlist.coverImgUrl" class="w-44 h-44 rounded-xl object-cover shadow-lg flex-shrink-0" />
|
||||
<div class="flex flex-col justify-between min-w-0">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold leading-tight">{{ playlist.name }}</h1>
|
||||
<div v-if="playlist.creator" class="flex items-center gap-2 mt-2">
|
||||
<img :src="playlist.creator.avatarUrl" class="w-5 h-5 rounded-full" />
|
||||
<span class="text-sm text-content-2">{{ playlist.creator.nickname }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-content-2 mt-2 line-clamp-2">{{ playlist.description }}</p>
|
||||
<p class="text-xs text-content-3 mt-2">
|
||||
{{ playlist.trackCount }} 首歌曲 · 播放 {{ formatPlayCount(playlist.playCount) }} 次
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mt-4">
|
||||
<button
|
||||
@click="playAll"
|
||||
class="px-5 py-2 bg-accent hover:bg-accent-hover rounded-full text-white font-medium transition flex items-center gap-2"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2.5v11l9-5.5z"/></svg>
|
||||
播放全部
|
||||
</button>
|
||||
<button
|
||||
v-if="!isOwnPlaylist"
|
||||
@click="toggleSubscribe"
|
||||
class="px-4 py-2 bg-muted hover:bg-emphasis rounded-full text-sm transition flex items-center gap-2"
|
||||
:class="subscribed ? 'text-accent-text' : 'text-content/70'"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path v-if="subscribed" d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/>
|
||||
<path v-else d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/>
|
||||
</svg>
|
||||
{{ subscribed ? '已收藏' : '收藏歌单' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="text-gray-400">加载中...</div>
|
||||
<div v-if="loading" class="text-content-2">加载中...</div>
|
||||
|
||||
<!-- 歌曲列表 -->
|
||||
<div v-else class="space-y-2">
|
||||
<div v-else class="space-y-1">
|
||||
<div
|
||||
v-for="(song, index) in songs"
|
||||
:key="song.id"
|
||||
@click="playSingle(song)"
|
||||
class="flex items-center gap-4 p-3 rounded-xl hover:bg-white/5 transition cursor-pointer"
|
||||
class="flex items-center gap-4 p-3 rounded-xl hover:bg-subtle transition cursor-pointer group"
|
||||
:class="{ 'bg-accent-dim': isCurrentSong(song.id) }"
|
||||
>
|
||||
<span class="text-xs text-gray-500 w-6 text-right">{{ index + 1 }}</span>
|
||||
<div class="w-6 text-right flex-shrink-0 flex items-center justify-end h-5">
|
||||
<div v-if="isCurrentSong(song.id)" class="flex items-center justify-end">
|
||||
<div class="flex items-center gap-[3px] h-4">
|
||||
<span class="w-[3px] bg-accent-text rounded-full animate-bounce" style="height: 50%; animation-delay: 0ms"></span>
|
||||
<span class="w-[3px] bg-accent-text rounded-full animate-bounce" style="height: 100%; animation-delay: 150ms"></span>
|
||||
<span class="w-[3px] bg-accent-text rounded-full animate-bounce" style="height: 35%; animation-delay: 300ms"></span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<span class="text-xs text-content-3 group-hover:hidden">{{ index + 1 }}</span>
|
||||
<svg class="hidden group-hover:block text-content" width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 2.5v11l9-5.5z"/></svg>
|
||||
</template>
|
||||
</div>
|
||||
<img :src="song.al?.picUrl" class="w-10 h-10 rounded object-cover flex-shrink-0" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium truncate">{{ song.name }}</p>
|
||||
<p class="text-xs text-gray-400 truncate">
|
||||
<p class="text-sm font-medium truncate" :class="isCurrentSong(song.id) ? 'text-accent-text' : ''">{{ song.name }}</p>
|
||||
<p class="text-xs text-content-2 truncate">
|
||||
{{ song.ar?.map((a: any) => a.name).join(' / ') }}
|
||||
</p>
|
||||
</div>
|
||||
<span class="text-xs text-gray-500">{{ formatDuration(song.dt) }}</span>
|
||||
<button @click.stop="player.toggleLike(song.id)" class="text-content-3 hover:text-danger transition flex-shrink-0">
|
||||
<svg v-if="player.isLiked(song.id)" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" class="text-danger"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
|
||||
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
|
||||
</button>
|
||||
<span class="text-xs text-content-3">{{ formatDuration(song.dt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { usePlayerStore } from '../stores/player';
|
||||
import { useUserStore } from '../stores/user';
|
||||
import { showToast } from '../composables/useToast';
|
||||
import { formatDuration, formatPlayCount } from '../utils/format';
|
||||
|
||||
const route = useRoute();
|
||||
const player = usePlayerStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const playlist = ref<any>(null);
|
||||
const songs = ref<any[]>([]);
|
||||
const loading = ref(true);
|
||||
const subscribed = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
const id = Number(route.params.id);
|
||||
const isOwnPlaylist = computed(() => {
|
||||
if (!playlist.value || !userStore.user) return false;
|
||||
return playlist.value.creator?.userId === userStore.user.userId;
|
||||
});
|
||||
|
||||
async function fetchPlaylist(id: number) {
|
||||
loading.value = true;
|
||||
playlist.value = null;
|
||||
songs.value = [];
|
||||
try {
|
||||
const jsonStr: string = await invoke('get_playlist_detail', { id });
|
||||
const data = JSON.parse(jsonStr);
|
||||
playlist.value = data.playlist;
|
||||
songs.value = data.playlist.tracks || [];
|
||||
subscribed.value = data.playlist.subscribed || false;
|
||||
} catch (e) {
|
||||
console.error('获取歌单详情失败', e);
|
||||
console.error(e);
|
||||
showToast('获取歌单详情失败', 'error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchPlaylist(Number(route.params.id));
|
||||
});
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const sec = Math.floor(ms / 1000);
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = sec % 60;
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
watch(() => route.params.id, (newId) => {
|
||||
if (newId) fetchPlaylist(Number(newId));
|
||||
});
|
||||
|
||||
function isCurrentSong(songId: number): boolean {
|
||||
return player.currentSong?.id === songId;
|
||||
}
|
||||
|
||||
async function playSingle(song: any) {
|
||||
@ -88,4 +143,16 @@ function playAll() {
|
||||
if (songs.value.length === 0) return;
|
||||
player.playAll(songs.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
async function toggleSubscribe() {
|
||||
if (!playlist.value) return;
|
||||
const newSubscribed = !subscribed.value;
|
||||
try {
|
||||
await invoke('playlist_subscribe', { query: { id: Number(playlist.value.id), subscribe: newSubscribed } });
|
||||
subscribed.value = newSubscribed;
|
||||
showToast(subscribed.value ? '已收藏歌单' : '已取消收藏', 'success');
|
||||
} catch {
|
||||
showToast('操作失败,请稍后重试', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user