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: "/", 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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue