feat(search): 添加搜索来源切换和专辑搜索结果展示
1. 移除演示页面导航项 2. 新增搜索来源选择栏,支持切换搜索源 3. 新增专辑搜索结果标签页,展示专辑搜索结果 4. 调整搜索逻辑,按选中源单独发起搜索请求 5. 优化搜索结果处理逻辑,兼容专辑数据转换
This commit is contained in:
parent
22dac5328a
commit
381b29309a
|
|
@ -59,7 +59,7 @@ const navItems = [
|
|||
{ path: "/", label: "歌单", icon: "home" },
|
||||
{ path: "/ranking", label: "排行榜", icon: "trophy" },
|
||||
{ path: "/library", label: "我的音乐", icon: "music" },
|
||||
{ path: "/demo", label: "演示", icon: "play" },
|
||||
// { path: "/demo", label: "演示", icon: "play" },
|
||||
] as const
|
||||
|
||||
function isActive(path: string) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,19 @@
|
|||
</n-input>
|
||||
</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="searchHistoryStore.items.length > 0" class="history-block">
|
||||
<div class="section-line">
|
||||
|
|
@ -73,6 +86,17 @@
|
|||
</template>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -84,11 +108,15 @@ import { NEmpty, NInput, NSpin, NTabPane, NTabs, NTag, useDialog, useMessage } f
|
|||
import AppIcon from "@/components/AppIcon.vue"
|
||||
import MusicCard from "@/components/MusicCard.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 { hotKeywords, type Song } from "@/mocks/music"
|
||||
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 Source from "@/types/global"
|
||||
import { useSearchHistoryStore } from "@/stores/searchHistory"
|
||||
|
|
@ -100,12 +128,15 @@ const settingsStore = useSettingsStore()
|
|||
const searchHistoryStore = useSearchHistoryStore()
|
||||
const keyword = ref("")
|
||||
const activeTab = ref("song")
|
||||
const allSources = ALL_SOURCES
|
||||
const selectedSource = ref<Source>(settingsStore.defaultSource)
|
||||
|
||||
const normalized = computed(() => keyword.value.trim().toLowerCase())
|
||||
|
||||
const loading = ref(false)
|
||||
const songResults = ref<Song[]>([])
|
||||
const playlistResults = ref<PlayListSummary[]>([])
|
||||
const albumResults = ref<PlayListSummary[]>([])
|
||||
|
||||
let searchTimer: number | undefined
|
||||
let requestSeq = 0
|
||||
|
|
@ -119,6 +150,7 @@ watch(normalized, (q) => {
|
|||
requestSeq += 1
|
||||
songResults.value = []
|
||||
playlistResults.value = []
|
||||
albumResults.value = []
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
|
@ -130,9 +162,11 @@ watch(normalized, (q) => {
|
|||
async function doSearch(q: string) {
|
||||
const seq = ++requestSeq
|
||||
loading.value = true
|
||||
const [songRes, playlistRes] = await Promise.allSettled([
|
||||
MusicApi.search(q, "song", settingsStore.enabledSources),
|
||||
MusicApi.search(q, "playlist", settingsStore.enabledSources),
|
||||
const sources = [selectedSource.value]
|
||||
const [songRes, playlistRes, albumRes] = await Promise.allSettled([
|
||||
MusicApi.search(q, "song", sources),
|
||||
MusicApi.search(q, "playlist", sources),
|
||||
MusicApi.search(q, "album", sources),
|
||||
])
|
||||
if (seq !== requestSeq) return
|
||||
if (songRes.status === "fulfilled") {
|
||||
|
|
@ -145,6 +179,11 @@ async function doSearch(q: string) {
|
|||
} else {
|
||||
playlistResults.value = []
|
||||
}
|
||||
if (albumRes.status === "fulfilled") {
|
||||
albumResults.value = (albumRes.value.data.albums ?? []).map(toPlaylist)
|
||||
} else {
|
||||
albumResults.value = []
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +199,7 @@ function toSong(item: MusicSearchSong): Song {
|
|||
}
|
||||
}
|
||||
|
||||
function toPlaylist(item: MusicSearchPlaylist): PlayListSummary {
|
||||
function toPlaylist(item: MusicSearchPlaylist | MusicSearchAlbum): PlayListSummary {
|
||||
return {
|
||||
id: item.id,
|
||||
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) {
|
||||
const source = (song.source ?? settingsStore.defaultSource) as Source
|
||||
try {
|
||||
|
|
@ -182,6 +233,10 @@ async function playSearchSong(song: Song) {
|
|||
const res = await MusicApi.url(String(song.id), source)
|
||||
const url = res.data.url
|
||||
console.log("url: ", url)
|
||||
if (!res) {
|
||||
message.error(`获取直链失败:${song.title}`)
|
||||
return
|
||||
}
|
||||
playerStore.playSong(song, songResults.value, url)
|
||||
} catch {
|
||||
// 拿不到直链时仍切歌,后续可尝试 MusicApi.stream 代理播放
|
||||
|
|
@ -246,6 +301,14 @@ onBeforeUnmount(() => {
|
|||
max-width: min(560px, 100%);
|
||||
}
|
||||
|
||||
.source-bar {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.source-scrollbar {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: var(--app-text-muted);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue