feat(api): 新增专辑和音乐API模块

新增AlbumApi模块,包含获取专辑详情功能
新增MusicApi模块,包含封面代理、音频探测、歌词获取、搜索、流媒体代理、音源切换等功能
完善PlayListApi模块的JSDoc注释和方法重命名

feat(player): 实现基于HTML5 Audio的播放器功能

使用HTML5 Audio元素替换原有的定时器机制
实现播放/暂停、进度控制、音量同步、时间更新等功能
添加播放结束自动下一首逻辑
优化播放器状态管理

refactor(song-table): 优化组件事件通信机制

移除props中的songs传递
改用emit事件方式通知父组件播放操作
提升组件复用性和解耦效果

feat(discover-view): 重构发现页面功能结构

整合歌单分类和推荐歌单布局
优化分类加载和展示逻辑
调整UI组件结构和样式
修复推荐歌单显示问题

feat(library-view): 优化个人库播放体验

集成歌曲表格的播放事件处理
实现点击播放和队列管理功能

feat(playlist-detail): 实现歌单详情播放功能

集成音乐API获取真实播放链接
实现播放链接获取失败的降级处理
优化歌单详情数据加载逻辑
添加播放全部功能

feat(search-view): 完善搜索结果播放功能

集成歌曲搜索结果的播放处理
实现搜索歌曲的队列播放功能
This commit is contained in:
Yuhang Wu 2026-08-12 11:31:47 +08:00
parent 98083829cb
commit 32ca1b4f74
11 changed files with 399 additions and 152 deletions

17
src/api/album.ts Normal file
View File

@ -0,0 +1,17 @@
import { get } from "@/utils/request"
import Source from "@/types/global"
const AlbumApi = {
/**
*
* @description ID
* @param id ID
* @param source
* @returns
*/
detail: (id: string, source: Source) => {
return get<any>(`/api/v1/album/detail?id=${id}&source=${source}`)
},
}
export default AlbumApi

101
src/api/music.ts Normal file
View File

@ -0,0 +1,101 @@
import { get } from "@/utils/request"
import Source from "@/types/global"
/** 音乐直链 */
export interface MusicUrl {
url: string
}
const MusicApi = {
/**
*
* @description QQ 403
* @param url URL ( urlencode)
* @param name ()
* @param artist ()
* @returns
*/
cover: (url: string, name?: string, artist?: string) => {
return get<any>(`/api/v1/music/cover?url=${url}&name=${name}&artist=${artist}`)
},
/**
*
* @description 访 Content-Range
* @param id ID
* @param source
* @param duration ()(kbps)
*/
inspect: (id: string, source: Source, duration?: string) => {
return get<any>(`/api/v1/music/inspect?id=${id}&source=${source}&duration=${duration}`)
},
/**
* JSON
* @description LRC JSON
* @param id ID
* @param source
* @returns
*/
lyric: (id: string, source: Source) => {
return get<any>(`/api/v1/music/lyric?id=${id}&source=${source}`)
},
/**
* LRC
* @description .lrc
* @param id ID
* @param source
* @param name ()
* @param artist ()
* @returns
*/
lyricFile: (id: string, source: Source, name?: string, artist?: string) => {
return get<any>(`/api/v1/music/lyric/file?id=${id}&source=${source}&name=${name}&artist=${artist}`)
},
/**
*
* @description
* @param keyword
* @param type 搜索类型: song ()playlist () album ()
* @param sources
* @returns
*/
search: (keyword: string, type: string, sources?: Source[]) => {
return get<any>(`/api/v1/music/search?q=${keyword}&type=${type}&sources=${sources}`)
},
/**
*
* @description Soda()
* @param id ID
* @param source
* @param name ()
* @param artist ()
* @returns
*/
stream: (id: string, source: Source, name?: string, artist?: string) => {
return get<any>(`/api/v1/music/stream?id=${id}&source=${source}&name=${name}&artist=${artist}`)
},
/**
*
* @description
* @param name
* @param source ()
* @param artist
* @param target
* @param duration ()
* @returns
*/
switch: (name: string, source: Source, artist?: string, target?: string, duration?: string) => {
return get<any>(`/api/v1/music/switch?name=${name}&source=${source}&artist=${artist}&target=${target}&duration=${duration}`)
},
/**
*
* @description header
* @param id ID
* @param source
* @returns
*/
url: (id: string, source: Source) => {
return get<MusicUrl>(`/api/v1/music/url?id=${id}&source=${source}`)
},
}
export default MusicApi

View File

@ -1,6 +1,7 @@
import { get } from "@/utils/request"; import { get } from "@/utils/request"
import Source from "@/types/global"; import Source from "@/types/global"
/** 歌单分类 */
interface PlayListCategory { interface PlayListCategory {
id: string id: string
name: string name: string
@ -9,6 +10,7 @@ interface PlayListCategory {
count: number count: number
} }
/** 分类歌单接口返回的数据结构 */
interface PlayListCategoryRes { interface PlayListCategoryRes {
source: string source: string
name: string name: string
@ -81,39 +83,57 @@ interface PlayListSong {
} }
/** 歌单摘要缓存:详情页通过它读取歌单标题、封面等元信息 */ /** 歌单摘要缓存:详情页通过它读取歌单标题、封面等元信息 */
const playListSummaryCache = new Map<string, PlayListSummary>(); const playListSummaryCache = new Map<string, PlayListSummary>()
export function cachePlayListSummary(summary: PlayListSummary) { export function cachePlayListSummary(summary: PlayListSummary) {
playListSummaryCache.set(summary.id, summary); playListSummaryCache.set(summary.id, summary)
} }
export function getCachedPlayListSummary(id: string): PlayListSummary | undefined { export function getCachedPlayListSummary(id: string): PlayListSummary | undefined {
return playListSummaryCache.get(id); return playListSummaryCache.get(id)
} }
const PlayListApi = { const PlayListApi = {
// 获取歌单分类 /**
getCategoryList: (sources?: Source[]) => { *
return get<PlayListCategoryRes[]>(`/api/v1/playlist/categories?sources=${sources}`); * @param sources
* @returns
*/
categoryList: (sources?: Source[]) => {
return get<PlayListCategoryRes[]>(`/api/v1/playlist/categories?sources=${sources}`)
}, },
// 获取分类歌单 /**
getCategoryPlayList: ( *
categoryId: string, * @description ID
source?: Source, * @param categoryId ID
page?: number, * @param source
limit?: number, * @param page
) => { * @param limit
return get<CategoryPlayListRes>( * @returns
`/api/v1/playlist/category?source=${source}&category_id=${categoryId}&page=${page}&limit=${limit}`, */
); categoryPlayList: (categoryId: string, source?: Source, page?: number, limit?: number) => {
return get<CategoryPlayListRes>(`/api/v1/playlist/category?source=${source}&category_id=${categoryId}&page=${page}&limit=${limit}`)
}, },
// 获取歌单详情 /**
getPlayListDetail: (source: Source, id: string) => { *
return get<PlayListSong[]>( * @param source
`/api/v1/playlist/detail?source=${source}&id=${id}`, * @param id ID
); * @returns
*/
detail: (source: Source, id: string) => {
return get<PlayListSong[]>(`/api/v1/playlist/detail?source=${source}&id=${id}`)
}, },
};
export default PlayListApi; /**
*
* @description
* @param sources (使)
* @returns
*/
recommend: (sources?: Source[]) => {
return get<PlayListSummary[]>(`/api/v1/playlist/recommend?sources=${sources}`)
},
}
export default PlayListApi
export type { PlayListCategory, PlayListCategoryRes, PlayListSummary, CategoryPlayListRes, PlayListSong } export type { PlayListCategory, PlayListCategoryRes, PlayListSummary, CategoryPlayListRes, PlayListSong }

49
src/api/system.ts Normal file
View File

@ -0,0 +1,49 @@
import { get, post } from "@/utils/request"
import Source from "@/types/global"
const SystemApi = {
/**
* Cookies
* @description JSON Cookies
* @returns
*/
cookies: () => {
return get<any>(`/api/v1/system/cookies`)
},
/**
* Cookies
* @description JSON cookie
* @returns
*/
setCookies: (data: Record<string, string>) => {
return post<any>(`/api/v1/system/cookies`, data)
},
/**
*
* @description API
* @returns
*/
qrLoginSource: () => {
return get<any>(`/api/v1/system/qr_login/sources`)
},
/**
*
* @description 使 key cookies.json
* @param source
* @returns
*/
loginStatus: (source: Source) => {
return get<any>(`/api/v1/system/qr_login/${source}`)
},
/**
*
* @description URL key
* @param source
* @returns
*/
qrLogin: (source: Source) => {
return post<any>(`/api/v1/system/qr_login/${source}`)
},
}
export default SystemApi

View File

@ -1,5 +1,13 @@
<template> <template>
<footer class="player-bar"> <footer class="player-bar">
<audio
ref="audioRef"
class="audio-hidden"
preload="auto"
@timeupdate="handleTimeUpdate"
@ended="handleEnded"
/>
<div class="player-left"> <div class="player-left">
<div <div
class="cover-mini" class="cover-mini"
@ -147,7 +155,7 @@ const liked = ref(new Set<string | number>());
const shuffle = ref(false); const shuffle = ref(false);
const repeat = ref(false); const repeat = ref(false);
let timer: number | undefined; const audioRef = ref<HTMLAudioElement | null>(null);
const current = computed(() => playerState.current); const current = computed(() => playerState.current);
@ -219,31 +227,78 @@ function openFullscreen() {
showFullscreen.value = true; showFullscreen.value = true;
} }
function handleTimeUpdate() {
if (audioRef.value) {
playerState.currentTime = audioRef.value.currentTime;
}
}
function handleEnded() {
next();
}
//
watch(
() => playerState.src,
(src) => {
const audio = audioRef.value;
if (!audio) return;
if (src) {
audio.src = src;
audio.load();
if (playerState.isPlaying) {
audio.play().catch(() => {});
}
} else {
audio.removeAttribute("src");
audio.load();
}
},
);
// /
watch( watch(
() => playerState.isPlaying, () => playerState.isPlaying,
(playing) => { (playing) => {
const audio = audioRef.value;
if (!audio || !playerState.src) return;
if (playing) { if (playing) {
timer = window.setInterval(() => { audio.play().catch(() => {});
if (!playerState.current) return;
playerState.currentTime += 1;
if (playerState.currentTime >= playerState.current.duration) {
if (repeat.value) {
playerState.currentTime = 0;
} else { } else {
next(); audio.pause();
} }
},
);
//
watch(
() => playerState.volume,
(vol) => {
if (audioRef.value) {
audioRef.value.volume = vol / 100;
} }
}, 1000); },
} else if (timer !== undefined) { { immediate: true },
window.clearInterval(timer); );
timer = undefined;
// audio
watch(
() => playerState.currentTime,
(time) => {
const audio = audioRef.value;
if (!audio || !Number.isFinite(audio.duration)) return;
if (Math.abs(audio.currentTime - time) > 0.8) {
audio.currentTime = time;
} }
}, },
); );
onUnmounted(() => { onUnmounted(() => {
if (timer !== undefined) { const audio = audioRef.value;
window.clearInterval(timer); if (audio) {
audio.pause();
audio.removeAttribute("src");
audio.load();
} }
}); });
</script> </script>
@ -261,6 +316,10 @@ onUnmounted(() => {
border-top: 1px solid var(--app-border); border-top: 1px solid var(--app-border);
} }
.audio-hidden {
display: none;
}
.player-left { .player-left {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -51,11 +51,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive } from "vue"; import { reactive } from "vue";
import type { Song } from "@/mocks/music"; import type { Song } from "@/mocks/music";
import { playerState, playSong } from "@/stores/player"; import { playerState } from "@/stores/player";
import { formatTime } from "@/utils/time"; import { formatTime } from "@/utils/time";
import AppIcon from "./AppIcon.vue"; import AppIcon from "./AppIcon.vue";
const props = defineProps<{ songs: Song[] }>(); defineProps<{ songs: Song[] }>();
const emit = defineEmits<{ play: [song: Song] }>();
const liked = reactive(new Set<string | number>()); const liked = reactive(new Set<string | number>());
@ -73,7 +74,7 @@ function isCurrent(song: Song) {
} }
function play(song: Song) { function play(song: Song) {
playSong(song, props.songs); emit("play", song);
} }
</script> </script>

View File

@ -7,6 +7,8 @@ interface PlayerState {
isPlaying: boolean; isPlaying: boolean;
currentTime: number; currentTime: number;
volume: number; volume: number;
/** 当前歌曲的播放直链 */
src: string;
} }
/** 全局播放器状态(轻量版,后续可平滑迁移到 Pinia */ /** 全局播放器状态(轻量版,后续可平滑迁移到 Pinia */
@ -16,11 +18,13 @@ export const playerState = reactive<PlayerState>({
isPlaying: false, isPlaying: false,
currentTime: 0, currentTime: 0,
volume: 70, volume: 70,
src: "",
}); });
export function playSong(song: Song, queue?: Song[]) { export function playSong(song: Song, queue?: Song[], src = "") {
playerState.current = song; playerState.current = song;
playerState.currentTime = 0; playerState.currentTime = 0;
playerState.src = src;
if (queue && queue.length > 0) { if (queue && queue.length > 0) {
playerState.queue = [...queue]; playerState.queue = [...queue];
} }

View File

@ -16,19 +16,13 @@
<span class="section-sub">歌单分类</span> <span class="section-sub">歌单分类</span>
</div> </div>
<n-scrollbar x-scrollable class="category-scrollbar"> <n-scrollbar x-scrollable class="category-scrollbar">
<n-radio-group <n-radio-group v-model:value="activeCategory" name="playlist-category" @update:value="playList">
v-model:value="activeCategory" <n-radio-button v-for="category in categories" :key="category.id" :value="category.id" :label="category.name" />
name="playlist-category"
@update:value="playList"
>
<n-radio-button
v-for="category in categories"
:key="category.id"
:value="category.id"
:label="category.name"
/>
</n-radio-group> </n-radio-group>
</n-scrollbar> </n-scrollbar>
<div class="card-grid">
<MusicCard v-for="playlist in recommendedPlaylists" :key="playlist.id" :playlist="playlist" />
</div>
</section> </section>
<section class="section"> <section class="section">
@ -36,9 +30,9 @@
<h3>推荐歌单</h3> <h3>推荐歌单</h3>
<span class="section-sub">为你精选</span> <span class="section-sub">为你精选</span>
</div> </div>
<div class="card-grid"> <!-- <div class="card-grid">
<MusicCard v-for="playlist in recommendedPlaylists" :key="playlist.id" :playlist="playlist" /> <MusicCard v-for="playlist in recommendedPlaylists" :key="playlist.id" :playlist="playlist" />
</div> </div> -->
</section> </section>
<section class="section"> <section class="section">
@ -101,25 +95,25 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { NCarousel, useMessage } from "naive-ui"; import { NCarousel, useMessage } from "naive-ui"
import { useRouter } from "vue-router"; import { useRouter } from "vue-router"
import AppIcon from "@/components/AppIcon.vue"; import AppIcon from "@/components/AppIcon.vue"
import MusicCard from "@/components/MusicCard.vue"; import MusicCard from "@/components/MusicCard.vue"
import { newSongs, rankLists, type Playlist } from "@/mocks/music"; import { newSongs, rankLists, type Playlist } from "@/mocks/music"
import { playSong } from "@/stores/player"; import { playSong } from "@/stores/player"
import { proxyCover } from "@/utils/cover"; import { proxyCover } from "@/utils/cover"
import { formatPlayCount } from "@/utils/format"; import { formatPlayCount } from "@/utils/format"
import { formatTime } from "@/utils/time"; import { formatTime } from "@/utils/time"
import PlayListApi from "@/api/playlist"; import PlayListApi from "@/api/playlist"
import Source from "@/types/global"; import Source from "@/types/global"
import { cachePlayListSummary, type PlayListCategory } from "@/api/playlist"; import { cachePlayListSummary, type PlayListCategory } from "@/api/playlist"
const message = useMessage(); const message = useMessage()
const router = useRouter(); const router = useRouter()
const categories = ref<PlayListCategory[]>([]); const categories = ref<PlayListCategory[]>([])
const activeCategory = ref<string>("") const activeCategory = ref<string>("")
const recommendedPlaylists = ref<Playlist[]>([]); const recommendedPlaylists = ref<Playlist[]>([])
const banners = [ const banners = [
{ {
@ -137,27 +131,27 @@ const banners = [
sub: "全站播放量最高的 100 首歌", sub: "全站播放量最高的 100 首歌",
gradient: "linear-gradient(120deg, #f59e0b 0%, #ef4444 55%, #f97316 100%)", gradient: "linear-gradient(120deg, #f59e0b 0%, #ef4444 55%, #f97316 100%)",
}, },
]; ]
function playNewSong(songIndex: number) { function playNewSong(songIndex: number) {
const song = newSongs[songIndex]; const song = newSongs[songIndex]
playSong(song, newSongs); playSong(song, newSongs)
message.success(`正在播放:${song.title}`); message.success(`正在播放:${song.title}`)
} }
const list = async () => { const list = async () => {
const res = await PlayListApi.getCategoryList([Source.KuGou]); const res = await PlayListApi.categoryList([Source.KuGou])
categories.value = res[0].categories; categories.value = res[0].categories
if (categories.value.length > 0) { if (categories.value.length > 0) {
activeCategory.value = categories.value[1].id; activeCategory.value = categories.value[1].id
await playList(activeCategory.value); await playList(activeCategory.value)
}
} }
};
const playList = async (categoryId: string) => { const playList = async (categoryId: string) => {
const res = await PlayListApi.getCategoryPlayList(categoryId, Source.KuGou, 1, 30); const res = await PlayListApi.categoryPlayList(categoryId, Source.KuGou, 1, 30)
recommendedPlaylists.value = res.playlists.map((p) => { recommendedPlaylists.value = res.playlists.map((p) => {
cachePlayListSummary(p); cachePlayListSummary(p)
return { return {
id: p.id, id: p.id,
name: p.name, name: p.name,
@ -166,13 +160,13 @@ const playList = async (categoryId: string) => {
tags: [], tags: [],
playCount: formatPlayCount(p.play_count), playCount: formatPlayCount(p.play_count),
songs: [], songs: [],
}; }
}); })
} }
onMounted(() => { onMounted(() => {
list(); list()
}); })
</script> </script>
<style scoped> <style scoped>
@ -247,6 +241,7 @@ onMounted(() => {
.card-grid { .card-grid {
display: grid; display: grid;
margin-top: 20px;
grid-template-columns: repeat(auto-fill, minmax(clamp(140px, 14vw, 180px), 1fr)); grid-template-columns: repeat(auto-fill, minmax(clamp(140px, 14vw, 180px), 1fr));
gap: 20px 18px; gap: 20px 18px;
} }

View File

@ -20,7 +20,11 @@
播放全部 播放全部
</n-button> </n-button>
</div> </div>
<SongTable v-if="tab.songs.length > 0" :songs="tab.songs" /> <SongTable
v-if="tab.songs.length > 0"
:songs="tab.songs"
@play="(song) => playSong(song, tab.songs)"
/>
<n-empty <n-empty
v-else v-else
class="empty" class="empty"

View File

@ -16,18 +16,11 @@
</p> </p>
<p v-if="headerDesc" class="desc">{{ headerDesc }}</p> <p v-if="headerDesc" class="desc">{{ headerDesc }}</p>
<div v-if="tags.length" class="tags"> <div v-if="tags.length" class="tags">
<n-tag <n-tag v-for="tag in tags" :key="tag" size="small" :bordered="false">
v-for="tag in tags"
:key="tag"
size="small"
:bordered="false"
>
{{ tag }} {{ tag }}
</n-tag> </n-tag>
</div> </div>
<p class="stats"> <p class="stats">{{ songs.length }} · {{ headerPlayCount }} 次播放</p>
{{ songs.length }} · {{ headerPlayCount }} 次播放
</p>
<div class="actions"> <div class="actions">
<n-button type="primary" round size="large" :disabled="songs.length === 0" @click="playAll"> <n-button type="primary" round size="large" :disabled="songs.length === 0" @click="playAll">
<AppIcon name="play" :size="16" class="btn-icon" /> <AppIcon name="play" :size="16" class="btn-icon" />
@ -43,62 +36,50 @@
<div class="song-section"> <div class="song-section">
<n-spin v-if="loading" class="song-loading" /> <n-spin v-if="loading" class="song-loading" />
<SongTable v-else :songs="songs" /> <SongTable v-else :songs="songs" @play="playWithUrl" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue"
import { NButton, NSpin, NTag, useMessage } from "naive-ui"; import { NButton, NSpin, NTag, useMessage } from "naive-ui"
import { useRouter } from "vue-router"; import { useRouter } from "vue-router"
import AppIcon from "@/components/AppIcon.vue"; import AppIcon from "@/components/AppIcon.vue"
import SongTable from "@/components/SongTable.vue"; import SongTable from "@/components/SongTable.vue"
import PlayListApi, { getCachedPlayListSummary } from "@/api/playlist"; import PlayListApi, { getCachedPlayListSummary } from "@/api/playlist"
import { playlists, type Song } from "@/mocks/music"; import { playlists, type Song } from "@/mocks/music"
import { playSong } from "@/stores/player"; import { playSong } from "@/stores/player"
import { displayCover } from "@/utils/cover"; import { displayCover } from "@/utils/cover"
import { formatPlayCount } from "@/utils/format"; import { formatPlayCount } from "@/utils/format"
import Source from "@/types/global"; import Source from "@/types/global"
import MusicApi from "@/api/music"
const props = defineProps<{ id: string }>(); const props = defineProps<{ id: string }>()
const message = useMessage(); const message = useMessage()
const router = useRouter(); const router = useRouter()
const songs = ref<Song[]>([]); const songs = ref<Song[]>([])
const loading = ref(true); const loading = ref(true)
const summary = computed(() => getCachedPlayListSummary(props.id)); const summary = computed(() => getCachedPlayListSummary(props.id))
const mockPlaylist = computed(() => const mockPlaylist = computed(() => playlists.find((item) => item.id === props.id))
playlists.find((item) => item.id === props.id),
);
const headerName = computed( const headerName = computed(() => summary.value?.name ?? mockPlaylist.value?.name ?? "歌单详情")
() => summary.value?.name ?? mockPlaylist.value?.name ?? "歌单详情", const headerCreator = computed(() => summary.value?.creator ?? "YwYMusic 官方")
); const headerDesc = computed(() => summary.value?.description ?? mockPlaylist.value?.desc ?? "")
const headerCreator = computed( const tags = computed(() => mockPlaylist.value?.tags ?? [])
() => summary.value?.creator ?? "YwYMusic 官方",
);
const headerDesc = computed(
() => summary.value?.description ?? mockPlaylist.value?.desc ?? "",
);
const tags = computed(() => mockPlaylist.value?.tags ?? []);
const headerPlayCount = computed(() => const headerPlayCount = computed(() =>
summary.value summary.value ? formatPlayCount(summary.value.play_count) : (mockPlaylist.value?.playCount ?? String(songs.value.length)),
? formatPlayCount(summary.value.play_count) )
: (mockPlaylist.value?.playCount ?? String(songs.value.length)),
);
const headerCover = computed(() => { const headerCover = computed(() => {
const cover = const cover = summary.value?.cover ?? mockPlaylist.value?.cover ?? songs.value[0]?.cover ?? ""
summary.value?.cover ?? mockPlaylist.value?.cover ?? songs.value[0]?.cover ?? ""; return cover ? displayCover(cover) : "linear-gradient(135deg, #4338ca 0%, #7c3aed 100%)"
return cover })
? displayCover(cover)
: "linear-gradient(135deg, #4338ca 0%, #7c3aed 100%)";
});
onMounted(async () => { onMounted(async () => {
try { try {
const res = await PlayListApi.getPlayListDetail(Source.KuGou, props.id); const res = await PlayListApi.detail(Source.KuGou, props.id)
songs.value = res.map((song) => ({ songs.value = res.map((song) => ({
id: song.id, id: song.id,
title: song.name, title: song.name,
@ -106,24 +87,35 @@ onMounted(async () => {
album: song.album, album: song.album,
duration: song.duration, duration: song.duration,
cover: displayCover(song.cover), cover: displayCover(song.cover),
})); }))
} catch { } catch {
// mock // mock
const mock = playlists.find((item) => item.id === props.id); const mock = playlists.find((item) => item.id === props.id)
if (mock) songs.value = mock.songs; if (mock) songs.value = mock.songs
} finally { } finally {
loading.value = false; loading.value = false
} }
}); })
function playAll() { function playAll() {
if (songs.value.length === 0) return; if (songs.value.length === 0) return
playSong(songs.value[0], songs.value); playWithUrl(songs.value[0], songs.value)
message.success(`正在播放歌单:${headerName.value}`); message.success(`正在播放歌单:${headerName.value}`)
}
async function playWithUrl(song: Song, queue?: Song[]) {
try {
const { url } = await MusicApi.url(String(song.id), Source.KuGou)
playSong(song, queue, url)
} catch {
// MusicApi.stream
playSong(song, queue)
}
message.success(`正在播放:${song.title}`)
} }
function favorite() { function favorite() {
message.success(`已收藏「${headerName.value}`); message.success(`已收藏「${headerName.value}`)
} }
</script> </script>

View File

@ -37,7 +37,11 @@
<n-tabs v-model:value="activeTab" type="line" animated> <n-tabs v-model:value="activeTab" type="line" animated>
<n-tab-pane name="song" :tab="`单曲 ${songResults.length}`"> <n-tab-pane name="song" :tab="`单曲 ${songResults.length}`">
<div class="result-body"> <div class="result-body">
<SongTable v-if="songResults.length > 0" :songs="songResults" /> <SongTable
v-if="songResults.length > 0"
:songs="songResults"
@play="(song) => playSong(song, songResults)"
/>
<n-empty v-else class="empty" description="没有找到相关单曲" /> <n-empty v-else class="empty" description="没有找到相关单曲" />
</div> </div>
</n-tab-pane> </n-tab-pane>
@ -65,6 +69,7 @@ import AppIcon from "@/components/AppIcon.vue";
import MusicCard from "@/components/MusicCard.vue"; import MusicCard from "@/components/MusicCard.vue";
import SongTable from "@/components/SongTable.vue"; import SongTable from "@/components/SongTable.vue";
import { allSongs, hotKeywords, playlists } from "@/mocks/music"; import { allSongs, hotKeywords, playlists } from "@/mocks/music";
import { playSong } from "@/stores/player";
const message = useMessage(); const message = useMessage();
const keyword = ref(""); const keyword = ref("");