Files
AlgerMusicPlayer/src/renderer/layout/components/SearchBar.vue
T

570 lines
17 KiB
Vue
Raw Normal View History

<template>
2025-12-19 00:23:24 +08:00
<div class="search-box flex search-bar">
<div v-if="showBackButton" class="back-button" @click="goBack">
<i class="ri-arrow-left-line"></i>
</div>
2025-09-10 00:38:57 +08:00
<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
>
2025-09-10 00:38:57 +08:00
<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"
2025-09-10 00:38:57 +08:00
>
<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
}}
2025-09-10 00:38:57 +08:00
</div>
<i class="iconfont icon-xiasanjiaoxing"></i>
</div>
</n-dropdown>
</template>
</n-input>
</template>
2025-09-10 00:38:57 +08:00
<div class="search-suggestions-panel">
<n-scrollbar style="max-height: 300px">
2025-09-10 00:38:57 +08:00
<div v-if="suggestionsLoading" class="suggestion-item loading">
<n-spin size="small" />
2024-12-27 18:27:01 +08:00
</div>
2025-09-10 00:38:57 +08:00
<div
v-for="(suggestion, index) in suggestions"
:key="index"
class="suggestion-item"
:class="{ highlighted: index === highlightedIndex }"
@mousedown.prevent="selectSuggestion(suggestion)"
@mouseenter="highlightedIndex = index"
2025-09-10 00:38:57 +08:00
>
<i class="ri-search-line suggestion-icon"></i>
<span>{{ suggestion }}</span>
</div>
</n-scrollbar>
</div>
</n-popover>
</div>
2024-12-27 18:27:01 +08:00
<n-popover trigger="hover" placement="bottom" :show-arrow="false" raw>
<template #trigger>
<div class="user-box">
<n-avatar
2025-03-19 22:48:28 +08:00
v-if="userStore.user"
class="cursor-pointer"
2024-12-27 18:27:01 +08:00
circle
size="medium"
2025-03-19 22:48:28 +08:00
:src="getImgUrl(userStore.user.avatarUrl)"
2024-12-27 18:27:01 +08:00
@click="selectItem('user')"
/>
2025-02-19 01:01:43 +08:00
<div v-else class="mx-2 rounded-full cursor-pointer text-sm" @click="toLogin">
{{ t('comp.searchBar.login') }}
</div>
2024-12-27 18:27:01 +08:00
</div>
</template>
<div class="user-popover">
2025-03-19 22:48:28 +08:00
<div v-if="userStore.user" class="user-header" @click="selectItem('user')">
<n-avatar circle size="small" :src="getImgUrl(userStore.user?.avatarUrl)" />
<div>
<p class="username">{{ userStore.user?.nickname || 'Theodore' }}</p>
<p></p>
</div>
2024-12-27 18:27:01 +08:00
</div>
<div class="menu-items">
2025-03-19 22:48:28 +08:00
<div v-if="!userStore.user" class="menu-item" @click="toLogin">
2024-12-27 18:27:01 +08:00
<i class="iconfont ri-login-box-line"></i>
2025-02-19 01:01:43 +08:00
<span>{{ t('comp.searchBar.toLogin') }}</span>
2024-12-27 18:27:01 +08:00
</div>
2025-03-19 22:48:28 +08:00
<div v-if="userStore.user" class="menu-item" @click="selectItem('logout')">
2025-01-05 15:58:48 +08:00
<i class="iconfont ri-logout-box-r-line"></i>
2025-02-19 01:01:43 +08:00
<span>{{ t('comp.searchBar.logout') }}</span>
2025-01-05 15:58:48 +08:00
</div>
<!-- 切换主题 -->
2024-12-27 18:27:01 +08:00
<div class="menu-item" @click="selectItem('set')">
<i class="iconfont ri-settings-3-line"></i>
2025-02-19 01:01:43 +08:00
<span>{{ t('comp.searchBar.set') }}</span>
2024-12-27 18:27:01 +08:00
</div>
<div class="menu-item" v-if="isElectron">
<i class="iconfont ri-zoom-in-line"></i>
<span>{{ t('comp.searchBar.zoom') }}</span>
<div class="zoom-controls ml-auto">
<n-button quaternary circle size="tiny" @click="decreaseZoom">
<i class="ri-subtract-line"></i>
</n-button>
<n-tooltip trigger="hover">
<template #trigger>
<span class="zoom-value" :class="{ 'zoom-100': isZoom100() }" @click="resetZoom"
>{{ Math.round(zoomFactor * 100) }}%</span
>
</template>
{{ isZoom100() ? t('comp.searchBar.zoom100') : t('comp.searchBar.resetZoom') }}
</n-tooltip>
<n-button quaternary circle size="tiny" @click="increaseZoom">
<i class="ri-add-line"></i>
</n-button>
</div>
</div>
<div class="menu-item">
2025-03-19 22:48:28 +08:00
<i class="iconfont" :class="isDark ? 'ri-moon-line' : 'ri-sun-line'"></i>
2025-02-19 01:01:43 +08:00
<span>{{ t('comp.searchBar.theme') }}</span>
2025-03-19 22:48:28 +08:00
<n-switch v-model:value="isDark" class="ml-auto">
<template #checked>
<i class="ri-moon-line"></i>
</template>
<template #unchecked>
<i class="ri-sun-line"></i>
</template>
</n-switch>
</div>
<div class="menu-item" @click="restartApp">
<i class="iconfont ri-restart-line"></i>
2025-02-19 01:01:43 +08:00
<span>{{ t('comp.searchBar.restart') }}</span>
</div>
2025-01-13 22:55:46 +08:00
<div class="menu-item" @click="selectItem('refresh')">
2024-12-27 18:27:01 +08:00
<i class="iconfont ri-refresh-line"></i>
2025-02-19 01:01:43 +08:00
<span>{{ t('comp.searchBar.refresh') }}</span>
2025-01-13 22:55:46 +08:00
</div>
<div class="menu-item" @click="toGithubRelease">
<i class="iconfont ri-github-fill"></i>
2025-02-19 01:01:43 +08:00
<span>{{ t('comp.searchBar.currentVersion') }}</span>
<div class="version-info">
<span class="version-number">{{ updateInfo.currentVersion }}</span>
<n-tag v-if="updateInfo.hasUpdate" type="success" size="small" class="ml-1">
New {{ updateInfo.latestVersion }}
</n-tag>
</div>
2024-12-27 18:27:01 +08:00
</div>
</div>
</div>
</n-popover>
2024-12-15 14:13:13 +08:00
<coffee :alipay-q-r="alipay" :wechat-q-r="wechat">
2024-12-07 23:20:31 +08:00
<div class="github" @click="toGithub">
<i class="ri-github-fill"></i>
</div>
</coffee>
</div>
</template>
<script lang="ts" setup>
import { useDebounceFn } from '@vueuse/core';
import { computed, onMounted, ref, watch, watchEffect } from 'vue';
2025-02-19 01:01:43 +08:00
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { getSearchKeyword } from '@/api/home';
2025-01-25 23:53:20 +08:00
import { getUserDetail } from '@/api/login';
2025-09-10 00:38:57 +08:00
import { getSearchSuggestions } from '@/api/search';
2024-12-15 14:13:13 +08:00
import alipay from '@/assets/alipay.png';
import wechat from '@/assets/wechat.png';
2024-12-07 23:20:31 +08:00
import Coffee from '@/components/Coffee.vue';
import { SEARCH_TYPE, SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';
import { useZoom } from '@/hooks/useZoom';
2025-03-19 22:48:28 +08:00
import { useSearchStore } from '@/store/modules/search';
import { useSettingsStore } from '@/store/modules/settings';
import { useUserStore } from '@/store/modules/user';
import { getImgUrl, isElectron } from '@/utils';
2025-01-01 13:12:46 +08:00
import { checkUpdate, UpdateResult } from '@/utils/update';
import config from '../../../../package.json';
const router = useRouter();
2025-03-19 22:48:28 +08:00
const searchStore = useSearchStore();
const settingsStore = useSettingsStore();
const userStore = useUserStore();
const userSetOptions = ref(USER_SET_OPTIONS);
2025-06-08 14:49:24 +02:00
const { t, locale } = useI18n();
// 使用缩放hook
const { zoomFactor, initZoomFactor, increaseZoom, decreaseZoom, resetZoom, isZoom100 } = useZoom();
// 显示返回按钮
const showBackButton = computed(() => {
return router.currentRoute.value.meta.back === true;
});
// 返回上一页
const goBack = () => {
router.back();
};
// 推荐热搜词
2025-02-19 01:01:43 +08:00
const hotSearchKeyword = ref(t('comp.searchBar.searchPlaceholder'));
const hotSearchValue = ref('');
const loadHotSearchKeyword = async () => {
const { data } = await getSearchKeyword();
hotSearchKeyword.value = data.data.showKeyword;
hotSearchValue.value = data.data.realkeyword;
};
const loadPage = async () => {
const token = localStorage.getItem('token');
if (!token) return;
const { data } = await getUserDetail();
2025-03-19 22:48:28 +08:00
userStore.user =
data.profile || userStore.user || JSON.parse(localStorage.getItem('user') || '{}');
localStorage.setItem('user', JSON.stringify(userStore.user));
};
loadPage();
watchEffect(() => {
2025-03-19 22:48:28 +08:00
if (userStore.user) {
userSetOptions.value = USER_SET_OPTIONS;
} else {
userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');
}
});
const restartApp = () => {
window.electron.ipcRenderer.send('restart');
};
const toLogin = () => {
router.push('/user');
};
// 页面初始化
onMounted(() => {
loadHotSearchKeyword();
loadPage();
checkForUpdates();
isElectron && initZoomFactor();
});
2025-03-19 22:48:28 +08:00
const isDark = computed({
get: () => settingsStore.theme === 'dark',
set: () => settingsStore.toggleTheme()
});
// 搜索词
const searchValue = ref('');
// 使用 watch 代替 watchEffect 监听搜索值变化,确保深度监听
watch(
() => searchStore.searchValue,
(newValue) => {
if (newValue) {
searchValue.value = newValue;
}
},
{ immediate: true }
);
const search = () => {
const { value } = searchValue;
if (value === '') {
searchValue.value = hotSearchValue.value;
2024-05-22 19:20:57 +08:00
return;
}
2024-05-22 19:20:57 +08:00
2024-09-12 17:28:51 +08:00
if (router.currentRoute.value.path === '/search') {
2025-03-19 22:48:28 +08:00
searchStore.searchValue = value;
2024-09-12 17:28:51 +08:00
return;
}
2024-05-22 19:20:57 +08:00
router.push({
path: '/search',
query: {
keyword: value,
2025-03-19 22:48:28 +08:00
type: searchStore.searchType
}
2024-05-22 19:20:57 +08:00
});
2025-09-10 00:38:57 +08:00
console.log(`[UI] 执行搜索,关键词: "${searchValue.value}"`); // <--- 日志 K
showSuggestions.value = false; // 搜索后强制隐藏
};
2024-05-21 10:16:30 +08:00
const selectSearchType = (key: number) => {
2025-03-19 22:48:28 +08:00
searchStore.searchType = key;
if (searchValue.value) {
if (router.currentRoute.value.path === '/search') {
search();
} else {
router.push({
path: '/search',
query: {
keyword: searchValue.value,
type: key
}
});
}
}
};
const rawSearchTypes = ref(SEARCH_TYPES);
2025-06-08 14:49:24 +02:00
const searchTypeOptions = computed(() => {
locale.value;
return rawSearchTypes.value
.filter((type) => isElectron || type.key !== SEARCH_TYPE.BILIBILI)
.map((type) => ({
label: t(type.label),
key: type.key
}));
2025-06-08 14:49:24 +02:00
});
2024-05-21 10:16:30 +08:00
const selectItem = async (key: string) => {
// switch 判断
switch (key) {
case 'logout':
2025-03-19 22:48:28 +08:00
userStore.handleLogout();
break;
case 'login':
router.push('/login');
break;
case 'set':
router.push('/set');
break;
2024-12-27 18:27:01 +08:00
case 'user':
router.push('/user');
break;
2025-01-13 22:55:46 +08:00
case 'refresh':
window.location.reload();
break;
default:
}
};
2024-12-05 21:57:14 +08:00
const toGithub = () => {
window.open('http://donate.alger.fun/download', '_blank');
2024-12-05 21:57:14 +08:00
};
2024-12-27 18:27:01 +08:00
2025-01-01 13:12:46 +08:00
const updateInfo = ref<UpdateResult>({
hasUpdate: false,
latestVersion: '',
currentVersion: config.version,
releaseInfo: null
});
const checkForUpdates = async () => {
try {
2025-01-01 13:12:46 +08:00
const result = await checkUpdate(config.version);
if (result) {
updateInfo.value = result;
}
} catch (error) {
console.error('检查更新失败:', error);
}
};
2024-12-27 18:27:01 +08:00
const toGithubRelease = () => {
2025-12-19 00:23:24 +08:00
window.location.href = 'https://donate.alger.fun/download';
2024-12-27 18:27:01 +08:00
};
2025-09-10 00:38:57 +08:00
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;
2025-09-10 00:38:57 +08:00
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>
.back-button {
@apply mr-2 flex items-center justify-center text-xl cursor-pointer;
@apply w-9 h-9 rounded-full;
@apply bg-light-100 dark:bg-dark-100 text-gray-900 dark:text-white;
@apply border dark:border-gray-600 border-gray-200;
@apply hover:bg-light-200 dark:hover:bg-dark-200;
@apply transition-all duration-200;
}
.user-box {
@apply ml-4 flex text-lg justify-center items-center rounded-full transition-colors duration-200;
@apply border dark:border-gray-600 border-gray-200 hover:border-gray-400 dark:hover:border-gray-400;
@apply bg-light dark:bg-gray-800;
}
.search-box {
@apply pb-4 pr-4;
}
.search-box-input {
@apply relative;
:deep(.n-input) {
@apply bg-gray-50 dark:bg-black;
.n-input__input-el {
@apply text-gray-900 dark:text-white;
}
.n-input__prefix {
@apply text-gray-500 dark:text-gray-400;
}
}
}
2024-05-23 17:12:35 +08:00
.mobile {
.search-box {
@apply pl-4;
}
}
2024-12-05 21:57:14 +08:00
.github {
@apply cursor-pointer text-gray-900 dark:text-gray-100 hover:text-gray-600 dark:hover:text-gray-400 text-xl ml-4 rounded-full flex justify-center items-center px-2 h-full;
@apply border dark:border-gray-600 border-gray-200 bg-light dark:bg-black;
2024-12-05 21:57:14 +08:00
}
2024-12-27 18:27:01 +08:00
.user-popover {
2025-01-13 22:55:46 +08:00
@apply min-w-[220px] p-0 rounded-xl overflow-hidden;
@apply bg-light dark:bg-black;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
2024-12-27 18:27:01 +08:00
.user-header {
@apply flex items-center gap-2 p-3 cursor-pointer;
@apply border-b dark:border-gray-700 border-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700;
2024-12-27 18:27:01 +08:00
.username {
@apply text-sm font-medium text-gray-900 dark:text-gray-200;
2024-12-27 18:27:01 +08:00
}
}
.menu-items {
@apply py-1;
.menu-item {
2025-01-13 22:55:46 +08:00
@apply flex items-center px-3 py-1 text-sm cursor-pointer;
@apply text-gray-700 dark:text-gray-300;
2024-12-27 18:27:01 +08:00
transition: background-color 0.2s;
&:hover {
@apply bg-gray-100 dark:bg-gray-700;
2024-12-27 18:27:01 +08:00
}
i {
@apply mr-1 text-lg text-gray-500 dark:text-gray-400;
2024-12-27 18:27:01 +08:00
}
.version-info {
@apply ml-auto flex items-center;
2025-01-10 22:49:55 +08:00
.version-number {
@apply text-xs px-2 py-0.5 rounded;
@apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;
}
2024-12-27 18:27:01 +08:00
}
// 缩放控制样式
.zoom-controls {
@apply flex items-center gap-1;
.zoom-value {
@apply text-xs px-2 py-0.5 rounded cursor-pointer;
@apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;
@apply hover:bg-gray-200 dark:hover:bg-gray-600;
transition: all 0.2s ease;
&.zoom-100 {
@apply bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 font-bold;
@apply hover:bg-green-200 dark:hover:bg-green-800;
}
}
}
2024-12-27 18:27:01 +08:00
}
}
}
2025-09-10 00:38:57 +08:00
.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>