Files
jinshen-website/app/components/SearchResults.vue
R2m1liA dd05dc5175 feat: 修改搜索页的鼠标指针行为 & 调整搜索页组件样式
- 当鼠标指针进入搜索条目卡片时,鼠标指针修改为pointer样式
- 调整分页组件大小
2025-09-23 16:25:41 +08:00

255 lines
6.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div v-if="hasResults">
<div class="search-results">
<NuxtLink
v-for="(hit, hitIndex) in paginatedHits"
:key="`${getHitIdentifier(hit.content, hitIndex)}`"
:to="localePath(resolveHitLink(hit.content))"
>
<el-card class="result-card">
<h3 class="result-title">{{ getHitTitle(hit.content) }}</h3>
<p v-if="getHitSummary(hit.content)" class="result-summary">
{{ getHitSummary(hit.content) }}
</p>
<p v-if="hit.type" class="result-type">
<span>内容类型: </span>
<span class="result-type-name">{{ getIndexLabel(hit.type) }}</span>
</p>
</el-card>
</NuxtLink>
</div>
<!-- 分页组件 -->
<div class="pagination-container text-align-center mt-12">
<el-pagination
v-model:current-page="page"
class="justify-center"
layout="prev, pager, next"
hide-on-single-page
:page-size="pageSize"
:total="filteredHits.length"
@current-change="handleCurrentChange"
/>
</div>
</div>
<div v-else>
<el-empty
:description="
route.query.query
? $t('search.no-results', { query: route.query?.query })
: $t('search.no-query')
"
/>
</div>
</template>
<script setup lang="ts">
interface HitItem {
content: SearchHit;
type: string;
}
const props = defineProps<{
hitItems: HitItem[];
currentPage: number;
category?: string;
}>();
const emit = defineEmits(['update:current-page']);
// i18n相关
const { t } = useI18n();
// 路由相关
const localePath = useLocalePath();
const route = useRoute();
// 分页相关
const page = computed({
get: () => props.currentPage,
set: (val) => {
emit('update:current-page', val);
},
});
const pageSize = ref(5);
// 搜索相关
const hits = props.hitItems;
const filteredHits = computed(() => {
if (props.category) {
return hits.filter((hit) => hit.type === props.category);
} else {
return hits;
}
});
const paginatedHits = computed(() => {
const start = (page.value - 1) * pageSize.value;
const end = page.value * pageSize.value;
return filteredHits.value.slice(start, end);
});
const indexLabels = computed<Record<string, string>>(() => ({
production: t('search.sections.production'),
solution: t('search.sections.solution'),
support: t('search.sections.support'),
default: t('search.sections.default'),
}));
/**
* 根据indexUid获取标签名
* @param indexUid 搜索条目的IndexUid
*/
const getIndexLabel = (indexUid: string) =>
indexLabels.value[indexUid] || indexLabels.value.default;
const hasResults = computed(() => {
return filteredHits.value.length > 0;
});
/**
* 获取搜索条目的唯一标识符
* 尝试根据搜索条目的相关词条获取唯一标识符
* 若未找到则fallback至给定的index
* @param hit 搜索条目
* @param index 条目索引
*/
const getHitIdentifier = (hit: SearchHit, index: number) => {
const candidate = [hit.objectID, hit.documentId, hit.id, hit.slug].find(
(value) =>
['string', 'number'].includes(typeof value) && String(value).length > 0
);
return candidate != null ? String(candidate) : String(index);
};
/**
* 获取搜索条目的标题
* @param hit 搜索条目
*/
const getHitTitle = (hit: SearchHit) => {
const candidate = [
hit.title,
hit.name,
hit.heading,
hit.documentTitle,
].find((value) => typeof value === 'string' && value.trim().length > 0);
return candidate ? String(candidate) : t('search.untitled');
};
/**
* 获取搜索条目的摘要
* @param hit 搜索条目
*/
const getHitSummary = (hit: SearchHit) => {
const candidate = [
hit.summary,
hit.description,
hit.snippet,
hit.content,
hit.text,
].find((value) => typeof value === 'string' && value.trim().length > 0);
return candidate ? String(candidate) : '';
};
/**
* 解析条目链接
* 根据条目类型返回正确的跳转链接
* @param hit 搜索条目
*/
const resolveHitLink = (hit: SearchHit) => {
if (typeof hit.route === 'string' && hit.route.trim().length > 0) {
return localePath(hit.route);
}
const slugCandidate = [hit.slug, hit.documentId, hit.id, hit.objectID].find(
(value) =>
['string', 'number'].includes(typeof value) && String(value).length > 0
);
if (!slugCandidate) {
return null;
}
const slug = String(slugCandidate);
if (hit.indexUid === 'production') {
return localePath({ path: `/productions/${slug}` });
}
if (hit.indexUid === 'solution') {
return localePath({ path: `/solutions/${slug}` });
}
return null;
};
const handleCurrentChange = () => {};
</script>
<style scoped>
.search-results {
display: grid;
gap: 1.5rem;
padding: 0.5rem;
}
.result-card {
border-radius: 12px;
transition: box-shadow 0.2s ease;
}
.result-card:hover {
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
}
.result-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--el-color-primary);
display: inline-block;
}
.result-summary {
font-size: 0.95rem;
color: var(--el-text-color-regular);
margin-bottom: 0.5rem;
line-height: 1.6;
}
.result-type {
font-size: 0.8rem;
color: var(--el-text-color-secondary);
}
.result-type-name {
margin-left: 4px;
color: var(--el-color-primary);
}
:deep(.el-pagination) {
.btn-prev,
.btn-next {
.el-icon {
font-size: 24px;
}
}
.el-pager {
gap: 0.5rem;
}
.el-pager li {
font-size: 1rem;
/* border: 1px solid #409eff; */
border-radius: 50%;
&:hover {
background-color: #ecf5ff;
}
&.is-active {
background-color: var(--el-color-primary);
color: #fff;
}
}
}
</style>