Files
jinshen-website/app/pages/search.vue
R2m1liA 1c98921e01
All checks were successful
deploy to server / build-and-deploy (push) Successful in 2m45s
feat: 为搜索页添加error监听
2025-09-28 16:41:36 +08:00

293 lines
6.7 KiB
Vue

<template>
<div class="search-page">
<div class="search-header">
<h1 class="page-title">{{ $t('search.title') }}</h1>
<div class="search-bar">
<el-input
v-model="keyword"
class="search-input"
:placeholder="$t('search-placeholder')"
:prefix-icon="Search"
clearable
@keyup.enter="navigateToQuery(keyword)"
@clear="handleClear"
/>
<el-button
type="primary"
class="search-button"
@click="navigateToQuery(keyword)"
>
{{ $t('search.search-button') }}
</el-button>
</div>
</div>
<div v-if="loading" class="search-state">
<el-skeleton :rows="4" animated />
</div>
<div v-else-if="hasResults" class="search-results">
<el-tabs v-model="activeTab">
<el-tab-pane :label="`全部(${resultCount['all']})`" name="all">
<search-results
v-model:current-page="currentPage"
:hit-items="hits"
/>
</el-tab-pane>
<el-tab-pane
:label="`产品(${resultCount['production'] || 0})`"
name="production"
>
<search-results
v-model:current-page="currentPage"
:hit-items="hits"
category="production"
/>
</el-tab-pane>
<el-tab-pane
:label="`解决方案(${resultCount['solution'] || 0})`"
name="solution"
>
<search-results
v-model:current-page="currentPage"
:hit-items="hits"
category="solution"
/>
</el-tab-pane>
<el-tab-pane
:label="`相关问题(${resultCount['question'] || 0})`"
name="question"
>
<search-results
v-model:current-page="currentPage"
:hit-items="hits"
category="question"
/>
</el-tab-pane>
<el-tab-pane
:label="`文档资料(${resultCount['document'] || 0})`"
name="document"
>
<search-results
v-model:current-page="currentPage"
:hit-items="hits"
category="document"
/>
</el-tab-pane>
</el-tabs>
</div>
<div v-else class="search-state">
<el-empty
:description="
route.query.query
? $t('search.no-results', { query: route.query?.query })
: $t('search.no-query')
"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue';
// i18n相关
const { t } = useI18n();
const { getStrapiLocale } = useLocalizations();
const strapiLocale = getStrapiLocale();
// 路由相关
const route = useRoute();
const router = useRouter();
const localePath = useLocalePath();
// 搜索相关
const { search } = useMeilisearch();
const keyword = ref('');
const activeRequestId = ref(0);
const {
data: sections,
pending: loading,
error,
} = await useAsyncData(
() => `search-${route.query.query ?? ''}`,
async () => {
const q = String(route.query.query ?? '').trim();
if (!q) return [];
return await search(q, { limit: 12 });
}
);
// 本地化+空Section过滤
const filteredSections = computed(() =>
sections.value
.map((section) => ({
...section,
hits: section.hits.filter(
(hit) =>
!hit.locale ||
String(hit.locale).toLowerCase() === strapiLocale.toLowerCase()
),
}))
.filter((section) => section.hits.length > 0)
);
// 展平hits
const hits = computed(() =>
filteredSections.value.flatMap((item) =>
item.hits.map((content) => ({ content, type: item.indexUid }))
)
);
// 分类控制
const activeTab = ref('all');
const resultCount = computed(() => {
const map: Record<string, number> = { all: hits.value.length };
for (const hit of hits.value) {
map[hit.type] = (map[hit.type] ?? 0) + 1;
}
return map;
});
// 分页控制
const currentPage = ref(1);
const hasResults = computed(() =>
filteredSections.value.some((section) => section.hits.length > 0)
);
const navigateToQuery = (value: string) => {
const trimmed = value.trim();
if (!trimmed) return;
navigateTo({
path: localePath('/search'),
query: { query: trimmed },
});
};
const performSearch = async (value: string) => {
activeRequestId.value += 1;
const requestId = activeRequestId.value;
const trimmed = value.trim();
if (!trimmed) {
if (requestId === activeRequestId.value) {
sections.value = [];
loading.value = false;
}
return;
}
try {
const results = await search(trimmed, { limit: 12 });
if (requestId === activeRequestId.value) {
sections.value = results;
}
console.log('hits:', hits.value);
console.log(resultCount.value);
} catch (error) {
console.error('Failed to perform search', error);
if (requestId === activeRequestId.value) {
sections.value = [];
}
}
};
const handleClear = () => {
keyword.value = '';
sections.value = [];
router.replace(localePath({ path: '/search' }));
};
watch(
() => route.query.query,
(newQuery) => {
if (typeof newQuery === 'string' && newQuery.trim()) {
keyword.value = newQuery;
performSearch(newQuery);
} else {
loading.value = false;
}
}
);
watch(error, (value) => {
if (value) {
console.error('数据获取失败: ', value);
}
});
onMounted(() => {
if (typeof route.query.query === 'string' && route.query.query.trim())
keyword.value = route.query.query;
});
useHead(() => ({
title: t('search.head-title'),
}));
</script>
<style scoped>
.search-page {
margin: 0 auto;
max-width: 960px;
padding: 2.5rem 1.5rem 3rem;
min-height: 70vh;
}
.search-header {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.page-title {
font-size: 2.25rem;
font-weight: 600;
color: var(--el-text-color-primary);
}
.search-bar {
display: flex;
gap: 1rem;
align-items: center;
font-size: 16px;
}
.search-input {
flex: 1;
height: 50px;
}
.search-button {
height: 50px;
width: 100px;
font-size: 16px;
}
.search-meta {
font-size: 0.9rem;
color: var(--el-text-color-secondary);
}
.search-state {
display: flex;
justify-content: center;
padding: 3rem 0;
}
@media (max-width: 640px) {
.search-page {
padding: 2rem 1rem;
}
.search-bar {
flex-direction: column;
align-items: stretch;
}
.search-input {
width: 100%;
}
}
</style>