feat: 搜索页图片预览 & 文档列表显示文档类型 #96

Manually merged
remilia merged 4 commits from feat/search-image into master 2025-12-05 17:23:10 +08:00
7 changed files with 141 additions and 40 deletions
Showing only changes of commit f1398a5545 - Show all commits

View File

@ -6,9 +6,12 @@
<p v-if="item.summary" class="result-summary"> <p v-if="item.summary" class="result-summary">
{{ item.summary }} {{ item.summary }}
</p> </p>
<p v-if="item.type" class="result-type"> <p v-if="item.sectionType" class="result-type">
<span>{{ $t('search.section') }}: </span> <span>{{ $t('search.section') }}: </span>
<span class="result-type-name">{{ typeLabel }}</span> <span class="result-type-name">{{ typeLabel }}</span>
<span v-if="item.type" class="result-type-name"
>({{ item.type }})</span
>
</p> </p>
</el-col> </el-col>
<el-col :span="12" class="image-col"> <el-col :span="12" class="image-col">

View File

@ -3,10 +3,13 @@
<div class="search-results"> <div class="search-results">
<NuxtLink <NuxtLink
v-for="hit in paginatedHits" v-for="hit in paginatedHits"
:key="`${hit.type}-${hit.id}`" :key="`${hit.sectionType}-${hit.id}`"
:to="localePath(resolveHitLink(hit))" :to="localePath(resolveHitLink(hit))"
> >
<search-result-card :item="hit" :type-label="getIndexLabel(hit.type)" /> <search-result-card
:item="hit"
:type-label="getIndexLabel(hit.sectionType)"
/>
</NuxtLink> </NuxtLink>
</div> </div>
@ -63,7 +66,7 @@
const items = props.searchItems; const items = props.searchItems;
const filteredHits = computed(() => { const filteredHits = computed(() => {
if (props.category) { if (props.category) {
return items.filter((item) => item.type === props.category); return items.filter((item) => item.sectionType === props.category);
} else { } else {
return items; return items;
} }
@ -107,19 +110,19 @@
const slug = String(slugCandidate); const slug = String(slugCandidate);
if (item.type === 'product') { if (item.sectionType === 'product') {
return localePath({ path: `/products/${slug}` }); return localePath({ path: `/products/${slug}` });
} }
if (item.type === 'solution') { if (item.sectionType === 'solution') {
return localePath({ path: `/solutions/${slug}` }); return localePath({ path: `/solutions/${slug}` });
} }
if (item.type === 'document') { if (item.sectionType === 'document') {
return localePath({ path: `/download/${slug}` }); return localePath({ path: `/download/${slug}` });
} }
if (item.type === 'question') { if (item.sectionType === 'question') {
return localePath({ path: `/support/faq`, query: { focus: slug } }); return localePath({ path: `/support/faq`, query: { focus: slug } });
} }

View File

@ -32,7 +32,7 @@
const resultCount = computed(() => { const resultCount = computed(() => {
const map: Record<string, number> = { all: props.searchItems.length }; const map: Record<string, number> = { all: props.searchItems.length };
for (const item of props.searchItems) { for (const item of props.searchItems) {
map[item.type] = (map[item.type] ?? 0) + 1; map[item.sectionType] = (map[item.sectionType] ?? 0) + 1;
} }
return map; return map;
}); });

View File

@ -17,9 +17,10 @@ describe('converters', () => {
const result = converters.products(item); const result = converters.products(item);
expect(result).toEqual({ expect(result).toEqual({
id: 1, id: 1,
type: 'product', sectionType: 'product',
title: 'Hydraulic Pump', title: 'Hydraulic Pump',
summary: 'High efficiency', summary: 'High efficiency',
type: 'pump',
thumbnail: '/api/assets/rand-om__-uuid-1234', thumbnail: '/api/assets/rand-om__-uuid-1234',
}); });
}); });
@ -36,9 +37,10 @@ describe('converters', () => {
const result = converters.solutions(item); const result = converters.solutions(item);
expect(result).toEqual({ expect(result).toEqual({
id: 1, id: 1,
type: 'solution', sectionType: 'solution',
title: 'Solution A', title: 'Solution A',
summary: 'Effective solution', summary: 'Effective solution',
type: 'Type A',
thumbnail: '/api/assets/rand-om__-uuid-5678', thumbnail: '/api/assets/rand-om__-uuid-5678',
}); });
}); });
@ -51,13 +53,15 @@ describe('converters', () => {
'This is a detailed explanation of how to use the product effectively.', 'This is a detailed explanation of how to use the product effectively.',
products: ['Product A'], products: ['Product A'],
product_types: ['Type A'], product_types: ['Type A'],
type: 'Question Type',
}; };
const result = converters.questions(item); const result = converters.questions(item);
expect(result).toEqual({ expect(result).toEqual({
id: 1, id: 1,
title: 'How to use product?', title: 'How to use product?',
summary: '', summary: undefined,
type: 'question', type: 'Question Type',
sectionType: 'question',
}); });
}); });
@ -68,13 +72,15 @@ describe('converters', () => {
products: ['Product A'], products: ['Product A'],
product_types: ['Type A'], product_types: ['Type A'],
fileUUID: 'TEST-UUID', fileUUID: 'TEST-UUID',
type: 'manual',
}; };
const result = converters.product_documents(item); const result = converters.product_documents(item);
expect(result).toEqual({ expect(result).toEqual({
id: 'TEST-UUID', id: 'TEST-UUID',
title: 'User Manual', title: 'User Manual',
summary: '', summary: undefined,
type: 'document', sectionType: 'document',
type: 'manual',
}); });
}); });
}); });

View File

@ -6,33 +6,35 @@ export const converters: {
} = { } = {
products: (item: MeiliIndexMap['products']): SearchItemView => ({ products: (item: MeiliIndexMap['products']): SearchItemView => ({
id: item.id, id: item.id,
type: 'product', sectionType: 'product',
title: item.name, title: item.name,
summary: item?.summary ?? '', summary: item?.summary,
type: item?.type,
thumbnail: item?.cover ? `/api/assets/${item.cover}` : undefined, thumbnail: item?.cover ? `/api/assets/${item.cover}` : undefined,
}), }),
solutions: (item: MeiliIndexMap['solutions']): SearchItemView => ({ solutions: (item: MeiliIndexMap['solutions']): SearchItemView => ({
id: item.id, id: item.id,
type: 'solution', sectionType: 'solution',
title: item.title, title: item.title,
summary: item?.summary ?? '', summary: item?.summary,
type: item?.type,
thumbnail: item?.cover ? `/api/assets/${item.cover}` : undefined, thumbnail: item?.cover ? `/api/assets/${item.cover}` : undefined,
}), }),
questions: (item: MeiliIndexMap['questions']): SearchItemView => ({ questions: (item: MeiliIndexMap['questions']): SearchItemView => ({
id: item.id, id: item.id,
type: 'question', sectionType: 'question',
title: item.title, title: item.title,
summary: '', type: item?.type,
}), }),
product_documents: ( product_documents: (
item: MeiliIndexMap['product_documents'] item: MeiliIndexMap['product_documents']
): SearchItemView => ({ ): SearchItemView => ({
id: item.fileUUID || item.id, id: item.fileUUID || item.id,
type: 'document', sectionType: 'document',
title: item.title, title: item.title,
summary: '', type: item?.type,
}), }),
}; };

View File

@ -57,6 +57,9 @@ export interface MeiliQuestionIndex {
/** 问题内容 **/ /** 问题内容 **/
content?: string; content?: string;
/** 问题类型 **/
type?: string;
/** 相关产品 **/ /** 相关产品 **/
products: string[]; products: string[];
@ -74,6 +77,9 @@ export interface MeiliProductDocumentIndex {
/** 文档标题 **/ /** 文档标题 **/
title: string; title: string;
/** 文档类型 **/
type?: string;
/** 相关产品 **/ /** 相关产品 **/
products: string[]; products: string[];

View File

@ -3,13 +3,16 @@ export interface SearchItemView {
id: number | string; id: number | string;
/** 条目类型 **/ /** 条目类型 **/
type: 'product' | 'solution' | 'question' | 'document'; sectionType: 'product' | 'solution' | 'question' | 'document';
/** 条目标题 **/ /** 条目标题 **/
title: string; title: string;
/** 条目摘要 **/ /** 条目摘要 **/
summary: string; summary?: string;
/** 条目分类 **/
type?: string;
/** 条目预览图 **/ /** 条目预览图 **/
thumbnail?: string; thumbnail?: string;