301 lines
7.0 KiB
Vue
301 lines
7.0 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"
|
|
:search-items="searchItems"
|
|
/>
|
|
</el-tab-pane>
|
|
<el-tab-pane
|
|
:label="`产品(${resultCount['product'] || 0})`"
|
|
name="product"
|
|
>
|
|
<search-results
|
|
v-model:current-page="currentPage"
|
|
:search-items="searchItems"
|
|
category="product"
|
|
/>
|
|
</el-tab-pane>
|
|
<el-tab-pane
|
|
:label="`解决方案(${resultCount['solution'] || 0})`"
|
|
name="solution"
|
|
>
|
|
<search-results
|
|
v-model:current-page="currentPage"
|
|
:search-items="searchItems"
|
|
category="solution"
|
|
/>
|
|
</el-tab-pane>
|
|
<el-tab-pane
|
|
:label="`相关问题(${resultCount['question'] || 0})`"
|
|
name="question"
|
|
>
|
|
<search-results
|
|
v-model:current-page="currentPage"
|
|
:search-items="searchItems"
|
|
category="question"
|
|
/>
|
|
</el-tab-pane>
|
|
<el-tab-pane
|
|
:label="`文档资料(${resultCount['document'] || 0})`"
|
|
name="document"
|
|
>
|
|
<search-results
|
|
v-model:current-page="currentPage"
|
|
:search-items="searchItems"
|
|
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 { getDirectusLocale } = useLocalizations();
|
|
const directusLocale = getDirectusLocale();
|
|
|
|
// 路由相关
|
|
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-${directusLocale}-${route.query.query ?? ''}`,
|
|
async () => {
|
|
const q = String(route.query.query ?? '').trim();
|
|
if (!q) return [];
|
|
return await search(q, { limit: 12 }, directusLocale);
|
|
}
|
|
);
|
|
|
|
// 空Section过滤
|
|
const filteredSections = computed(() =>
|
|
sections.value.filter((section) => section.hits.length > 0)
|
|
);
|
|
|
|
const typeMap = {
|
|
products: 'products',
|
|
solutions: 'solutions',
|
|
questions: 'questions',
|
|
product_documents: 'product_documents',
|
|
} as const;
|
|
// 展平hits
|
|
const hits = computed(() =>
|
|
filteredSections.value.flatMap((section) => {
|
|
const type = typeMap[section.rawIndex as keyof typeof typeMap];
|
|
if (!type) return [];
|
|
return section.hits.map((hit) => ({ type, content: hit }));
|
|
})
|
|
);
|
|
|
|
const searchItems = computed(() =>
|
|
hits.value.map((hit) => {
|
|
return toSearchItemView(hit.content, hit.type);
|
|
})
|
|
);
|
|
|
|
console.log(searchItems.value);
|
|
|
|
// 分类控制
|
|
const activeTab = ref('all');
|
|
const resultCount = computed(() => {
|
|
const map: Record<string, number> = { all: searchItems.value.length };
|
|
for (const item of searchItems.value) {
|
|
map[item.type] = (map[item.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 }, directusLocale);
|
|
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,
|
|
async (newQuery) => {
|
|
if (typeof newQuery === 'string' && newQuery.trim()) {
|
|
keyword.value = newQuery;
|
|
await 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>
|