Skip to content

Commit e9ef712

Browse files
authored
Merge pull request #470 from souvenp/feat/search-suggestions
feat: search suggestions
2 parents 9003de8 + 5fbe4c3 commit e9ef712

File tree

6 files changed

+263
-20
lines changed

6 files changed

+263
-20
lines changed

src/main/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { setupUpdateHandlers } from './modules/update';
1616
import { createMainWindow, initializeWindowManager, setAppQuitting } from './modules/window';
1717
import { initWindowSizeManager } from './modules/window-size';
1818
import { startMusicApi } from './server';
19+
import { initializeOtherApi } from './modules/otherApi';
1920

2021
// 导入所有图标
2122
const iconPath = join(__dirname, '../../resources');
@@ -38,6 +39,8 @@ function initialize() {
3839

3940
// 初始化文件管理
4041
initializeFileManager();
42+
// 初始化其他 API (搜索建议等)
43+
initializeOtherApi();
4144
// 初始化窗口管理
4245
initializeWindowManager();
4346
// 初始化字体管理

src/main/modules/otherApi.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import axios from 'axios';
2+
import { ipcMain } from 'electron';
3+
4+
/**
5+
* 初始化其他杂项 API(如搜索建议等)
6+
*/
7+
export function initializeOtherApi() {
8+
// 搜索建议(从酷狗获取)
9+
ipcMain.handle('get-search-suggestions', async (_, keyword: string) => {
10+
if (!keyword || !keyword.trim()) {
11+
return [];
12+
}
13+
try {
14+
console.log(`[Main Process Proxy] Forwarding suggestion request for: ${keyword}`);
15+
const response = await axios.get('http://msearchcdn.kugou.com/new/app/i/search.php', {
16+
params: {
17+
cmd: 302,
18+
keyword: keyword
19+
},
20+
timeout: 5000
21+
});
22+
return response.data;
23+
} catch (error: any) {
24+
console.error('[Main Process Proxy] Failed to fetch search suggestions:', error.message);
25+
return [];
26+
}
27+
});
28+
}

src/preload/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface API {
2323
removeDownloadListeners: () => void;
2424
importCustomApiPlugin: () => Promise<{ name: string; content: string } | null>;
2525
invoke: (channel: string, ...args: any[]) => Promise<any>;
26+
getSearchSuggestions: (keyword: string) => Promise<any>;
2627
}
2728

2829
// 自定义IPC渲染进程通信接口

src/preload/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const api = {
5656
}
5757
return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`));
5858
},
59+
// 搜索建议
60+
getSearchSuggestions: (keyword: string) => ipcRenderer.invoke('get-search-suggestions', keyword),
5961
};
6062

6163
// 创建带类型的ipcRenderer对象,暴露给渲染进程

src/renderer/api/search.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isElectron } from '@/utils';
12
import request from '@/utils/request';
23

34
interface IParams {
@@ -12,3 +13,74 @@ export const getSearch = (params: IParams) => {
1213
params
1314
});
1415
};
16+
17+
/**
18+
* 搜索建议接口返回的数据结构
19+
*/
20+
interface Suggestion {
21+
keyword: string;
22+
}
23+
24+
interface KugouSuggestionResponse {
25+
data: Suggestion[];
26+
}
27+
28+
// 网易云搜索建议返回的数据结构(部分字段)
29+
interface NeteaseSuggestResult {
30+
result?: {
31+
songs?: Array<{ name: string }>;
32+
artists?: Array<{ name: string }>;
33+
albums?: Array<{ name: string }>;
34+
};
35+
code?: number;
36+
}
37+
38+
/**
39+
* 从酷狗获取搜索建议
40+
* @param keyword 搜索关键词
41+
*/
42+
export const getSearchSuggestions = async (keyword: string) => {
43+
console.log('[API] getSearchSuggestions: 开始执行');
44+
45+
if (!keyword || !keyword.trim()) {
46+
return Promise.resolve([]);
47+
}
48+
49+
console.log(`[API] getSearchSuggestions: 准备请求,关键词: "${keyword}"`);
50+
51+
try {
52+
let responseData: KugouSuggestionResponse;
53+
if (isElectron) {
54+
console.log('[API] Running in Electron, using IPC proxy.');
55+
responseData = await window.api.getSearchSuggestions(keyword);
56+
} else {
57+
// 非 Electron 环境下,使用网易云接口
58+
const res = await request.get<NeteaseSuggestResult>('/search/suggest', {
59+
params: { keywords: keyword }
60+
});
61+
62+
const result = res?.data?.result || {};
63+
const names: string[] = [];
64+
if (Array.isArray(result.songs)) names.push(...result.songs.map((s) => s.name));
65+
if (Array.isArray(result.artists)) names.push(...result.artists.map((a) => a.name));
66+
if (Array.isArray(result.albums)) names.push(...result.albums.map((al) => al.name));
67+
68+
// 去重并截取前10个
69+
const unique = Array.from(new Set(names)).slice(0, 10);
70+
console.log('[API] getSearchSuggestions: 网易云建议解析成功:', unique);
71+
return unique;
72+
}
73+
74+
if (responseData && Array.isArray(responseData.data)) {
75+
const suggestions = responseData.data.map((item) => item.keyword).slice(0, 10);
76+
console.log('[API] getSearchSuggestions: 成功解析建议:', suggestions);
77+
return suggestions;
78+
}
79+
80+
console.warn('[API] getSearchSuggestions: 响应数据格式不正确,返回空数组。');
81+
return [];
82+
} catch (error) {
83+
console.error('[API] getSearchSuggestions: 请求失败,错误信息:', error);
84+
return [];
85+
}
86+
};

src/renderer/layout/components/SearchBar.vue

Lines changed: 157 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,65 @@
33
<div v-if="showBackButton" class="back-button" @click="goBack">
44
<i class="ri-arrow-left-line"></i>
55
</div>
6-
<div class="search-box-input flex-1">
7-
<n-input
8-
v-model:value="searchValue"
9-
size="medium"
10-
round
11-
:placeholder="hotSearchKeyword"
12-
class="border dark:border-gray-600 border-gray-200"
13-
@keydown.enter="search"
6+
<div class="search-box-input flex-1 relative">
7+
<n-popover
8+
trigger="manual"
9+
placement="bottom-start"
10+
:show="showSuggestions"
11+
:show-arrow="false"
12+
style="width: 100%; margin-top: 4px"
13+
content-style="padding: 0; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"
14+
raw
1415
>
15-
<template #prefix>
16-
<i class="iconfont icon-search"></i>
16+
<template #trigger>
17+
<n-input
18+
v-model:value="searchValue"
19+
size="medium"
20+
round
21+
:placeholder="hotSearchKeyword"
22+
class="border dark:border-gray-600 border-gray-200"
23+
@input="handleInput"
24+
@keydown="handleKeydown"
25+
@focus="handleFocus"
26+
@blur="handleBlur"
27+
>
28+
<template #prefix>
29+
<i class="iconfont icon-search"></i>
30+
</template>
31+
<template #suffix>
32+
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
33+
<div class="w-20 px-3 flex justify-between items-center">
34+
<div>
35+
{{
36+
searchTypeOptions.find((item) => item.key === searchStore.searchType)?.label
37+
}}
38+
</div>
39+
<i class="iconfont icon-xiasanjiaoxing"></i>
40+
</div>
41+
</n-dropdown>
42+
</template>
43+
</n-input>
1744
</template>
18-
<template #suffix>
19-
<n-dropdown trigger="hover" :options="searchTypeOptions" @select="selectSearchType">
20-
<div class="w-20 px-3 flex justify-between items-center">
21-
<div>
22-
{{ searchTypeOptions.find((item) => item.key === searchStore.searchType)?.label }}
23-
</div>
24-
<i class="iconfont icon-xiasanjiaoxing"></i>
45+
<!-- ==================== 搜索建议列表 ==================== -->
46+
<div class="search-suggestions-panel">
47+
<n-scrollbar style="max-height: 300px">
48+
<div v-if="suggestionsLoading" class="suggestion-item loading">
49+
<n-spin size="small" />
2550
</div>
26-
</n-dropdown>
27-
</template>
28-
</n-input>
51+
<div
52+
v-for="(suggestion, index) in suggestions"
53+
:key="index"
54+
class="suggestion-item"
55+
:class="{ highlighted: index === highlightedIndex }"
56+
@mousedown.prevent="selectSuggestion(suggestion)"
57+
@mouseenter="highlightedIndex = index"
58+
>
59+
<i class="ri-search-line suggestion-icon"></i>
60+
<span>{{ suggestion }}</span>
61+
</div>
62+
</n-scrollbar>
63+
</div>
64+
</n-popover>
2965
</div>
3066
<n-popover trigger="hover" placement="bottom" :show-arrow="false" raw>
3167
<template #trigger>
@@ -128,12 +164,14 @@
128164
</template>
129165

130166
<script lang="ts" setup>
167+
import { useDebounceFn } from '@vueuse/core';
131168
import { computed, onMounted, ref, watch, watchEffect } from 'vue';
132169
import { useI18n } from 'vue-i18n';
133170
import { useRouter } from 'vue-router';
134171
135172
import { getSearchKeyword } from '@/api/home';
136173
import { getUserDetail } from '@/api/login';
174+
import { getSearchSuggestions } from '@/api/search';
137175
import alipay from '@/assets/alipay.png';
138176
import wechat from '@/assets/wechat.png';
139177
import Coffee from '@/components/Coffee.vue';
@@ -250,6 +288,9 @@ const search = () => {
250288
type: searchStore.searchType
251289
}
252290
});
291+
292+
console.log(`[UI] 执行搜索,关键词: "${searchValue.value}"`); // <--- 日志 K
293+
showSuggestions.value = false; // 搜索后强制隐藏
253294
};
254295
255296
const selectSearchType = (key: number) => {
@@ -330,6 +371,84 @@ const toGithubRelease = () => {
330371
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases', '_blank');
331372
}
332373
};
374+
375+
// ==================== 搜索建议相关的状态和方法 ====================
376+
const suggestions = ref<string[]>([]);
377+
const showSuggestions = ref(false);
378+
const suggestionsLoading = ref(false);
379+
const highlightedIndex = ref(-1); // -1 表示没有高亮项
380+
// 使用防抖函数来避免频繁请求API
381+
const debouncedGetSuggestions = useDebounceFn(async (keyword: string) => {
382+
if (!keyword.trim()) {
383+
suggestions.value = [];
384+
showSuggestions.value = false;
385+
return;
386+
}
387+
suggestionsLoading.value = true;
388+
suggestions.value = await getSearchSuggestions(keyword);
389+
suggestionsLoading.value = false;
390+
// 只有当有建议时才显示面板
391+
showSuggestions.value = suggestions.value.length > 0;
392+
highlightedIndex.value = -1;
393+
}, 300); // 300ms延迟
394+
395+
const handleInput = (value: string) => {
396+
debouncedGetSuggestions(value);
397+
};
398+
const handleFocus = () => {
399+
if (searchValue.value && suggestions.value.length > 0) {
400+
showSuggestions.value = true;
401+
}
402+
};
403+
404+
const handleBlur = () => {
405+
setTimeout(() => {
406+
showSuggestions.value = false;
407+
}, 150);
408+
};
409+
410+
const selectSuggestion = (suggestion: string) => {
411+
searchValue.value = suggestion;
412+
showSuggestions.value = false;
413+
search();
414+
};
415+
const handleKeydown = (event: KeyboardEvent) => {
416+
// 如果建议列表不显示,则不处理上下键
417+
if (!showSuggestions.value || suggestions.value.length === 0) {
418+
// 如果是回车键,则正常执行搜索
419+
if (event.key === 'Enter') {
420+
search();
421+
}
422+
return;
423+
}
424+
425+
switch (event.key) {
426+
case 'ArrowDown':
427+
event.preventDefault(); // 阻止光标移动到末尾
428+
highlightedIndex.value = (highlightedIndex.value + 1) % suggestions.value.length;
429+
break;
430+
case 'ArrowUp':
431+
event.preventDefault(); // 阻止光标移动到开头
432+
highlightedIndex.value =
433+
(highlightedIndex.value - 1 + suggestions.value.length) % suggestions.value.length;
434+
break;
435+
case 'Enter':
436+
event.preventDefault(); // 阻止表单默认提交行为
437+
if (highlightedIndex.value !== -1) {
438+
// 如果有高亮项,就选择它
439+
selectSuggestion(suggestions.value[highlightedIndex.value]);
440+
} else {
441+
// 否则,执行默认搜索
442+
search();
443+
}
444+
break;
445+
case 'Escape':
446+
showSuggestions.value = false; // 按 Esc 隐藏建议
447+
break;
448+
}
449+
};
450+
451+
// ================================================================
333452
</script>
334453

335454
<style lang="scss" scoped>
@@ -437,4 +556,22 @@ const toGithubRelease = () => {
437556
}
438557
}
439558
}
559+
560+
.search-suggestions-panel {
561+
@apply bg-light dark:bg-dark-100 rounded-lg overflow-hidden;
562+
.suggestion-item {
563+
@apply flex items-center px-4 py-2 cursor-pointer;
564+
@apply text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800;
565+
&.highlighted {
566+
@apply bg-gray-100 dark:bg-gray-800;
567+
}
568+
&.loading {
569+
@apply justify-center;
570+
}
571+
572+
.suggestion-icon {
573+
@apply mr-2 text-gray-400;
574+
}
575+
}
576+
}
440577
</style>

0 commit comments

Comments
 (0)