feat: 搜索建议添加

This commit is contained in:
shano
2025-09-10 00:38:57 +08:00
parent d24d3d63b8
commit 2a8d0f2066
3 changed files with 209 additions and 21 deletions

View File

@@ -40,7 +40,14 @@ export default defineConfig({
],
publicDir: resolve('resources'),
server: {
host: '0.0.0.0'
host: '0.0.0.0',
proxy: {
'/kugou': {
target: 'http://msearchcdn.kugou.com', // 目标服务器地址
changeOrigin: true, // 必须设置为 true
rewrite: (path) => path.replace(/^\/kugou/, '') // 重写请求路径,去掉 /kugou 前缀
}
}
}
}
});

View File

@@ -1,4 +1,5 @@
import request from '@/utils/request';
import axios from 'axios';
interface IParams {
keywords: string;
@@ -12,3 +13,49 @@ export const getSearch = (params: IParams) => {
params
});
};
/**
* 搜索建议接口返回的数据结构
*/
interface Suggestion {
keyword: string;
}
interface KugouSuggestionResponse {
data: Suggestion[];
}
/**
* 从酷狗获取搜索建议
* @param keyword 搜索关键词
*/
export const getSearchSuggestions = async (keyword: string) => {
console.log('[API] getSearchSuggestions: 开始执行');
if (!keyword || !keyword.trim()) {
return Promise.resolve([]);
}
console.log(`[API] getSearchSuggestions: 准备请求,关键词: "${keyword}"`);
try {
const response = await axios.get<KugouSuggestionResponse>('/kugou/new/app/i/search.php', {
params: {
cmd: 302,
keyword: keyword,
},
});
if (response.data && Array.isArray(response.data.data)) {
const suggestions = response.data.data.map(item => item.keyword).slice(0, 8);
console.log('[API] getSearchSuggestions: 成功解析建议:', suggestions);
return suggestions;
}
console.warn('[API] getSearchSuggestions: 响应数据格式不正确,返回空数组。');
return [];
} catch (error) {
console.error('[API] getSearchSuggestions: 请求失败,错误信息:', error);
return [];
}
};

View File

@@ -3,29 +3,63 @@
<div v-if="showBackButton" class="back-button" @click="goBack">
<i class="ri-arrow-left-line"></i>
</div>
<div class="search-box-input flex-1">
<n-input
v-model:value="searchValue"
size="medium"
round
:placeholder="hotSearchKeyword"
class="border dark:border-gray-600 border-gray-200"
@keydown.enter="search"
<div class="search-box-input flex-1 relative">
<n-popover
trigger="manual"
placement="bottom-start"
:show="showSuggestions"
:show-arrow="false"
style="width: 100%; margin-top: 4px;"
content-style="padding: 0; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"
raw
>
<template #prefix>
<i class="iconfont icon-search"></i>
<template #trigger>
<n-input
v-model:value="searchValue"
size="medium"
round
:placeholder="hotSearchKeyword"
class="border dark:border-gray-600 border-gray-200"
@input="handleInput"
@keydown="handleKeydown"
@focus="handleFocus"
@blur="handleBlur"
>
<template #prefix>
<i class="iconfont icon-search"></i>
</template>
<template #suffix>
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
<div class="w-20 px-3 flex justify-between items-center">
<div>
{{ searchTypeOptions.find((item) => item.key === searchStore.searchType)?.label }}
</div>
<i class="iconfont icon-xiasanjiaoxing"></i>
</div>
</n-dropdown>
</template>
</n-input>
</template>
<template #suffix>
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
<div class="w-20 px-3 flex justify-between items-center">
<div>
{{ searchTypeOptions.find((item) => item.key === searchStore.searchType)?.label }}
</div>
<i class="iconfont icon-xiasanjiaoxing"></i>
<!-- ==================== 搜索建议列表 ==================== -->
<div class="search-suggestions-panel">
<n-scrollbar style="max-height: 300px;">
<div v-if="suggestionsLoading" class="suggestion-item loading">
<n-spin size="small" />
</div>
</n-dropdown>
</template>
</n-input>
<div
v-for="(suggestion, index) in suggestions"
:key="index"
class="suggestion-item"
:class="{ 'highlighted': index === highlightedIndex }"
@mousedown.prevent="selectSuggestion(suggestion)"
@mouseenter="highlightedIndex = index"
>
<i class="ri-search-line suggestion-icon"></i>
<span>{{ suggestion }}</span>
</div>
</n-scrollbar>
</div>
</n-popover>
</div>
<n-popover trigger="hover" placement="bottom" :show-arrow="false" raw>
<template #trigger>
@@ -131,9 +165,11 @@
import { computed, onMounted, ref, watch, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useDebounceFn } from '@vueuse/core';
import { getSearchKeyword } from '@/api/home';
import { getUserDetail } from '@/api/login';
import { getSearchSuggestions } from '@/api/search';
import alipay from '@/assets/alipay.png';
import wechat from '@/assets/wechat.png';
import Coffee from '@/components/Coffee.vue';
@@ -250,6 +286,9 @@ const search = () => {
type: searchStore.searchType
}
});
console.log(`[UI] 执行搜索,关键词: "${searchValue.value}"`); // <--- 日志 K
showSuggestions.value = false; // 搜索后强制隐藏
};
const selectSearchType = (key: number) => {
@@ -330,6 +369,83 @@ const toGithubRelease = () => {
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
}
};
// ==================== 搜索建议相关的状态和方法 ====================
const suggestions = ref<string[]>([]);
const showSuggestions = ref(false);
const suggestionsLoading = ref(false);
const highlightedIndex = ref(-1); // -1 表示没有高亮项
// 使用防抖函数来避免频繁请求API
const debouncedGetSuggestions = useDebounceFn(async (keyword: string) => {
if (!keyword.trim()) {
suggestions.value = [];
showSuggestions.value = false;
return;
}
suggestionsLoading.value = true;
suggestions.value = await getSearchSuggestions(keyword);
suggestionsLoading.value = false;
// 只有当有建议时才显示面板
showSuggestions.value = suggestions.value.length > 0;
highlightedIndex.value = -1;
}, 300); // 300ms延迟
const handleInput = (value: string) => {
debouncedGetSuggestions(value);
};
const handleFocus = () => {
if (searchValue.value && suggestions.value.length > 0) {
showSuggestions.value = true;
}
};
const handleBlur = () => {
setTimeout(() => {
showSuggestions.value = false;
}, 150);
};
const selectSuggestion = (suggestion: string) => {
searchValue.value = suggestion;
showSuggestions.value = false;
search();
};
const handleKeydown = (event: KeyboardEvent) => {
// 如果建议列表不显示,则不处理上下键
if (!showSuggestions.value || suggestions.value.length === 0) {
// 如果是回车键,则正常执行搜索
if (event.key === 'Enter') {
search();
}
return;
}
switch (event.key) {
case 'ArrowDown':
event.preventDefault(); // 阻止光标移动到末尾
highlightedIndex.value = (highlightedIndex.value + 1) % suggestions.value.length;
break;
case 'ArrowUp':
event.preventDefault(); // 阻止光标移动到开头
highlightedIndex.value = (highlightedIndex.value - 1 + suggestions.value.length) % suggestions.value.length;
break;
case 'Enter':
event.preventDefault(); // 阻止表单默认提交行为
if (highlightedIndex.value !== -1) {
// 如果有高亮项,就选择它
selectSuggestion(suggestions.value[highlightedIndex.value]);
} else {
// 否则,执行默认搜索
search();
}
break;
case 'Escape':
showSuggestions.value = false; // 按 Esc 隐藏建议
break;
}
};
// ================================================================
</script>
<style lang="scss" scoped>
@@ -437,4 +553,22 @@ const toGithubRelease = () => {
}
}
}
.search-suggestions-panel {
@apply bg-light dark:bg-dark-100 rounded-lg overflow-hidden;
.suggestion-item {
@apply flex items-center px-4 py-2 cursor-pointer;
@apply text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800;
&.highlighted {
@apply bg-gray-100 dark:bg-gray-800;
}
&.loading {
@apply justify-center;
}
.suggestion-icon {
@apply mr-2 text-gray-400;
}
}
}
</style>