feat(search): 添加搜索来源切换和专辑搜索结果展示

1. 移除演示页面导航项
2. 新增搜索来源选择栏,支持切换搜索源
3. 新增专辑搜索结果标签页,展示专辑搜索结果
4. 调整搜索逻辑,按选中源单独发起搜索请求
5. 优化搜索结果处理逻辑,兼容专辑数据转换
This commit is contained in:
sparksfly 2026-08-13 23:19:52 +08:00
parent 22dac5328a
commit 381b29309a
2 changed files with 70 additions and 7 deletions

View File

@ -59,7 +59,7 @@ const navItems = [
{ path: "/", label: "歌单", icon: "home" }, { path: "/", label: "歌单", icon: "home" },
{ path: "/ranking", label: "排行榜", icon: "trophy" }, { path: "/ranking", label: "排行榜", icon: "trophy" },
{ path: "/library", label: "我的音乐", icon: "music" }, { path: "/library", label: "我的音乐", icon: "music" },
{ path: "/demo", label: "演示", icon: "play" }, // { path: "/demo", label: "", icon: "play" },
] as const ] as const
function isActive(path: string) { function isActive(path: string) {

View File

@ -17,6 +17,19 @@
</n-input> </n-input>
</div> </div>
<div class="source-bar">
<n-scrollbar x-scrollable class="source-scrollbar">
<n-radio-group v-model:value="selectedSource" name="search-source" @update:value="onSourceChange">
<n-radio-button
v-for="source in allSources"
:key="source"
:value="source"
:label="SOURCE_LABELS[source]"
/>
</n-radio-group>
</n-scrollbar>
</div>
<div v-if="!normalized" class="hot-area"> <div v-if="!normalized" class="hot-area">
<div v-if="searchHistoryStore.items.length > 0" class="history-block"> <div v-if="searchHistoryStore.items.length > 0" class="history-block">
<div class="section-line"> <div class="section-line">
@ -73,6 +86,17 @@
</template> </template>
</div> </div>
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="album" :tab="`专辑 ${albumResults.length}`">
<div class="result-body">
<n-spin v-if="loading" class="result-loading" />
<template v-else>
<div v-if="albumResults.length > 0" class="card-grid">
<MusicCard v-for="album in albumResults" :key="album.id" :playlist="album" />
</div>
<n-empty v-else class="empty" description="没有找到相关专辑" />
</template>
</div>
</n-tab-pane>
</n-tabs> </n-tabs>
</div> </div>
</div> </div>
@ -84,11 +108,15 @@ import { NEmpty, NInput, NSpin, NTabPane, NTabs, NTag, useDialog, useMessage } f
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 SongTable from "@/components/SongTable.vue" import SongTable from "@/components/SongTable.vue"
import MusicApi, { type MusicSearchPlaylist, type MusicSearchSong } from "@/api/music" import MusicApi, {
type MusicSearchAlbum,
type MusicSearchPlaylist,
type MusicSearchSong,
} from "@/api/music"
import type { PlayListSummary } from "@/api/playlist" import type { PlayListSummary } from "@/api/playlist"
import { hotKeywords, type Song } from "@/mocks/music" import { hotKeywords, type Song } from "@/mocks/music"
import { usePlayerStore } from "@/stores/player" import { usePlayerStore } from "@/stores/player"
import { useSettingsStore } from "@/stores/settings" import { ALL_SOURCES, SOURCE_LABELS, useSettingsStore } from "@/stores/settings"
import { displayCover } from "@/utils/cover" import { displayCover } from "@/utils/cover"
import Source from "@/types/global" import Source from "@/types/global"
import { useSearchHistoryStore } from "@/stores/searchHistory" import { useSearchHistoryStore } from "@/stores/searchHistory"
@ -100,12 +128,15 @@ const settingsStore = useSettingsStore()
const searchHistoryStore = useSearchHistoryStore() const searchHistoryStore = useSearchHistoryStore()
const keyword = ref("") const keyword = ref("")
const activeTab = ref("song") const activeTab = ref("song")
const allSources = ALL_SOURCES
const selectedSource = ref<Source>(settingsStore.defaultSource)
const normalized = computed(() => keyword.value.trim().toLowerCase()) const normalized = computed(() => keyword.value.trim().toLowerCase())
const loading = ref(false) const loading = ref(false)
const songResults = ref<Song[]>([]) const songResults = ref<Song[]>([])
const playlistResults = ref<PlayListSummary[]>([]) const playlistResults = ref<PlayListSummary[]>([])
const albumResults = ref<PlayListSummary[]>([])
let searchTimer: number | undefined let searchTimer: number | undefined
let requestSeq = 0 let requestSeq = 0
@ -119,6 +150,7 @@ watch(normalized, (q) => {
requestSeq += 1 requestSeq += 1
songResults.value = [] songResults.value = []
playlistResults.value = [] playlistResults.value = []
albumResults.value = []
loading.value = false loading.value = false
return return
} }
@ -130,9 +162,11 @@ watch(normalized, (q) => {
async function doSearch(q: string) { async function doSearch(q: string) {
const seq = ++requestSeq const seq = ++requestSeq
loading.value = true loading.value = true
const [songRes, playlistRes] = await Promise.allSettled([ const sources = [selectedSource.value]
MusicApi.search(q, "song", settingsStore.enabledSources), const [songRes, playlistRes, albumRes] = await Promise.allSettled([
MusicApi.search(q, "playlist", settingsStore.enabledSources), MusicApi.search(q, "song", sources),
MusicApi.search(q, "playlist", sources),
MusicApi.search(q, "album", sources),
]) ])
if (seq !== requestSeq) return if (seq !== requestSeq) return
if (songRes.status === "fulfilled") { if (songRes.status === "fulfilled") {
@ -145,6 +179,11 @@ async function doSearch(q: string) {
} else { } else {
playlistResults.value = [] playlistResults.value = []
} }
if (albumRes.status === "fulfilled") {
albumResults.value = (albumRes.value.data.albums ?? []).map(toPlaylist)
} else {
albumResults.value = []
}
loading.value = false loading.value = false
} }
@ -160,7 +199,7 @@ function toSong(item: MusicSearchSong): Song {
} }
} }
function toPlaylist(item: MusicSearchPlaylist): PlayListSummary { function toPlaylist(item: MusicSearchPlaylist | MusicSearchAlbum): PlayListSummary {
return { return {
id: item.id, id: item.id,
name: item.name, name: item.name,
@ -174,6 +213,18 @@ function toPlaylist(item: MusicSearchPlaylist): PlayListSummary {
} }
} }
function onSourceChange(source: Source) {
settingsStore.setDefaultSource(source)
const q = normalized.value
if (q) {
if (searchTimer !== undefined) {
window.clearTimeout(searchTimer)
searchTimer = undefined
}
void doSearch(q)
}
}
async function playSearchSong(song: Song) { async function playSearchSong(song: Song) {
const source = (song.source ?? settingsStore.defaultSource) as Source const source = (song.source ?? settingsStore.defaultSource) as Source
try { try {
@ -182,6 +233,10 @@ async function playSearchSong(song: Song) {
const res = await MusicApi.url(String(song.id), source) const res = await MusicApi.url(String(song.id), source)
const url = res.data.url const url = res.data.url
console.log("url: ", url) console.log("url: ", url)
if (!res) {
message.error(`获取直链失败:${song.title}`)
return
}
playerStore.playSong(song, songResults.value, url) playerStore.playSong(song, songResults.value, url)
} catch { } catch {
// MusicApi.stream // MusicApi.stream
@ -246,6 +301,14 @@ onBeforeUnmount(() => {
max-width: min(560px, 100%); max-width: min(560px, 100%);
} }
.source-bar {
margin-top: 18px;
}
.source-scrollbar {
padding-bottom: 8px;
}
.search-icon { .search-icon {
color: var(--app-text-muted); color: var(--app-text-muted);
} }