feat:针对移动端优化

This commit is contained in:
alger
2025-12-19 00:23:24 +08:00
parent 70f1044dd9
commit 8e1259d2aa
18 changed files with 2299 additions and 189 deletions
+392
View File
@@ -0,0 +1,392 @@
<template>
<div class="mobile-search-page">
<!-- 搜索头部 -->
<div class="search-header" :class="{ 'safe-area-top': hasSafeArea }">
<div class="header-back" @click="goBack">
<i class="ri-arrow-left-s-line"></i>
</div>
<div class="search-input-wrapper">
<i class="ri-search-line search-icon"></i>
<input
ref="searchInputRef"
v-model="searchValue"
type="text"
class="search-input"
:placeholder="hotSearchKeyword"
@input="handleInput"
@keydown.enter="handleSearch"
/>
<i v-if="searchValue" class="ri-close-circle-fill clear-icon" @click="clearSearch"></i>
</div>
<div class="search-button" @click="handleSearch">
{{ t('common.search') }}
</div>
</div>
<!-- 搜索类型标签 -->
<div class="search-types">
<div
v-for="type in searchTypes"
:key="type.key"
class="type-tag"
:class="{ active: searchType === type.key }"
@click="selectType(type.key)"
>
{{ type.label }}
</div>
</div>
<!-- 搜索内容区域 -->
<div class="search-content">
<!-- 搜索建议 -->
<div v-if="suggestions.length > 0" class="search-section">
<div class="section-title">{{ t('search.suggestions') }}</div>
<div class="suggestion-list">
<div
v-for="(item, index) in suggestions"
:key="index"
class="suggestion-item"
@click="selectSuggestion(item)"
>
<i class="ri-search-line"></i>
<span>{{ item }}</span>
</div>
</div>
</div>
<!-- 搜索历史 -->
<div v-else-if="searchHistory.length > 0" class="search-section">
<div class="section-header">
<span class="section-title">{{ t('search.history') }}</span>
<span class="clear-history" @click="clearHistory">{{ t('common.clear') }}</span>
</div>
<div class="history-tags">
<div
v-for="(item, index) in searchHistory"
:key="index"
class="history-tag"
@click="selectSuggestion(item)"
>
{{ item }}
</div>
</div>
</div>
<!-- 热门搜索 -->
<div v-if="hotSearchList.length > 0 && !searchValue" class="search-section">
<div class="section-title">{{ t('search.hot') }}</div>
<div class="hot-list">
<div
v-for="(item, index) in hotSearchList"
:key="index"
class="hot-item"
@click="selectSuggestion(item.searchWord)"
>
<span class="hot-rank" :class="{ top: index < 3 }">{{ index + 1 }}</span>
<span class="hot-word">{{ item.searchWord }}</span>
<span v-if="item.iconUrl" class="hot-icon">
<img :src="item.iconUrl" alt="" />
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useDebounceFn } from '@vueuse/core';
import { computed, inject, nextTick, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { getHotSearch, getSearchKeyword } from '@/api/home';
import { getSearchSuggestions } from '@/api/search';
import { SEARCH_TYPES } from '@/const/bar-const';
import { useSearchStore } from '@/store/modules/search';
const { t, locale } = useI18n();
const router = useRouter();
const searchStore = useSearchStore();
// 注入是否有安全区域
const hasSafeArea = inject('hasSafeArea', false);
// 搜索值
const searchValue = ref('');
const searchInputRef = ref<HTMLInputElement | null>(null);
// 热门搜索关键词占位符
const hotSearchKeyword = ref('搜索音乐、歌手、歌单');
// 搜索类型
const searchType = ref(searchStore.searchType || 1);
const searchTypes = computed(() => {
locale.value;
return SEARCH_TYPES.map((type) => ({
label: t(type.label),
key: type.key
}));
});
// 搜索建议
const suggestions = ref<string[]>([]);
// 搜索历史
const HISTORY_KEY = 'mobile_search_history';
const searchHistory = ref<string[]>([]);
// 热门搜索
const hotSearchList = ref<any[]>([]);
// 加载热门搜索关键词
const loadHotSearchKeyword = async () => {
try {
const { data } = await getSearchKeyword();
hotSearchKeyword.value = data.data.showKeyword;
} catch (e) {
console.error('加载热门搜索关键词失败:', e);
}
};
// 加载热门搜索列表
const loadHotSearchList = async () => {
try {
const { data } = await getHotSearch();
hotSearchList.value = data.data || [];
} catch (e) {
console.error('加载热门搜索失败:', e);
}
};
// 加载搜索历史
const loadSearchHistory = () => {
try {
const history = localStorage.getItem(HISTORY_KEY);
searchHistory.value = history ? JSON.parse(history) : [];
} catch (e) {
console.error('加载搜索历史失败:', e);
searchHistory.value = [];
}
};
// 保存搜索历史
const saveSearchHistory = (keyword: string) => {
if (!keyword.trim()) return;
// 移除重复项并添加到开头
const history = searchHistory.value.filter((item) => item !== keyword);
history.unshift(keyword);
// 最多保存20条
searchHistory.value = history.slice(0, 20);
localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory.value));
};
// 清除搜索历史
const clearHistory = () => {
searchHistory.value = [];
localStorage.removeItem(HISTORY_KEY);
};
// 获取搜索建议(防抖)
const debouncedGetSuggestions = useDebounceFn(async (keyword: string) => {
if (!keyword.trim()) {
suggestions.value = [];
return;
}
suggestions.value = await getSearchSuggestions(keyword);
}, 300);
// 处理输入
const handleInput = () => {
debouncedGetSuggestions(searchValue.value);
};
// 清除搜索
const clearSearch = () => {
searchValue.value = '';
suggestions.value = [];
};
// 选择搜索类型
const selectType = (type: number) => {
searchType.value = type;
searchStore.searchType = type;
};
// 选择建议
const selectSuggestion = (keyword: string) => {
searchValue.value = keyword;
handleSearch();
};
// 执行搜索
const handleSearch = () => {
const keyword = searchValue.value.trim();
if (!keyword) return;
// 保存搜索历史
saveSearchHistory(keyword);
// 跳转到搜索结果页
router.push({
path: '/mobile-search-result',
query: {
keyword,
type: searchType.value
}
});
};
// 返回上一页
const goBack = () => {
router.back();
};
onMounted(() => {
loadHotSearchKeyword();
loadHotSearchList();
loadSearchHistory();
nextTick(() => {
searchInputRef.value?.focus();
});
});
</script>
<style lang="scss" scoped>
.mobile-search-page {
@apply fixed inset-0 z-50;
@apply bg-light dark:bg-black;
@apply flex flex-col;
}
.search-header {
@apply flex items-center gap-3 pl-1 pr-3 py-3;
@apply border-b border-gray-100 dark:border-gray-800;
&.safe-area-top {
padding-top: calc(var(--safe-area-inset-top, 0px) + 12px);
}
}
.header-back {
@apply flex items-center justify-center;
@apply w-8 h-8 rounded-full text-2xl;
@apply text-gray-600 dark:text-gray-300;
@apply active:bg-gray-100 dark:active:bg-gray-800;
}
.search-input-wrapper {
@apply flex-1 flex items-center gap-2;
@apply bg-gray-100 dark:bg-gray-800 rounded-full;
@apply px-4 py-1;
}
.search-icon {
@apply text-gray-400 text-lg;
}
.search-input {
@apply flex-1 bg-transparent border-none outline-none;
@apply text-gray-900 dark:text-white text-base;
&::placeholder {
@apply text-gray-400;
}
}
.clear-icon {
@apply text-gray-400 text-lg cursor-pointer;
}
.search-types {
@apply flex gap-2 px-4 py-3 overflow-x-auto;
@apply border-b border-gray-100 dark:border-gray-800;
&::-webkit-scrollbar {
display: none;
}
}
.type-tag {
@apply px-4 py-1.5 rounded-full text-sm whitespace-nowrap;
@apply bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300;
@apply transition-colors duration-200;
&.active {
@apply bg-green-500 text-white;
}
}
.search-content {
@apply flex-1 overflow-y-auto px-4 py-3;
}
.search-section {
@apply mb-6;
}
.section-header {
@apply flex items-center justify-between mb-3;
}
.section-title {
@apply text-sm font-medium text-gray-500 dark:text-gray-400 mb-3;
}
.clear-history {
@apply text-sm text-gray-400 dark:text-gray-500;
}
.suggestion-list {
@apply space-y-1;
}
.suggestion-item {
@apply flex items-center gap-3 py-3;
@apply text-gray-700 dark:text-gray-200;
@apply active:bg-gray-50 dark:active:bg-gray-800;
i {
@apply text-gray-400;
}
}
.history-tags {
@apply flex flex-wrap gap-2;
}
.history-tag {
@apply px-3 py-1.5 rounded-full text-sm;
@apply bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300;
@apply active:bg-gray-200 dark:active:bg-gray-700;
}
.hot-list {
@apply space-y-1;
}
.hot-item {
@apply flex items-center gap-3 py-2.5;
@apply active:bg-gray-50 dark:active:bg-gray-800;
}
.hot-rank {
@apply w-5 text-center text-sm font-medium text-gray-400;
&.top {
@apply text-red-500;
}
}
.hot-word {
@apply flex-1 text-gray-700 dark:text-gray-200;
}
.hot-icon {
img {
@apply h-4;
}
}
</style>