Compare commits

..

5 Commits

Author SHA1 Message Date
5011448902 chore: 补全i18n文本
All checks were successful
deploy to server / build-and-deploy (push) Successful in 2m55s
- 补全俄语与西班牙语的i18n翻译
2025-12-03 17:53:18 +08:00
53f3e99d90 feat: 文档库页面添加文档类型筛选功能
- 功能添加:添加按照文档类型筛选文档的功能
- 组件更改:将筛选器由product-filter改为document-filter
2025-12-03 17:47:57 +08:00
815df40745 style: 删除无用class 2025-12-03 17:33:28 +08:00
c27337a145 feat: 文档库查询添加文档类型字段
- graphQL查询修改:添加type字段查询
- 视图模型字段更新:添加DocumentTypeView视图模型并为DocumentList类型添加type字段
- mapper更新: 添加DocumentType的转换方法
2025-12-03 17:30:16 +08:00
3fb721f278 test: 修改测试用例 2025-12-03 17:17:34 +08:00
10 changed files with 270 additions and 22 deletions

View File

@ -0,0 +1,113 @@
<template>
<div class="document-category">
<el-row :gutter="12">
<el-col :span="12" :xs="12">
<span class="select-label">{{
$t('product-filter.product-type')
}}</span>
<el-select
v-model="model.selectedProductType"
:placeholder="$t('product-filter.select-product-type')"
clearable
>
<el-option
v-for="type in productTypeOptions"
:key="type.id"
:label="type.name"
:value="type.id"
/>
</el-select>
</el-col>
<el-col :span="12" :xs="12">
<span class="select-label">{{
$t('product-filter.product-model')
}}</span>
<el-select
v-model="model.selectedProduct"
:placeholder="$t('product-filter.select-product-model')"
clearable
>
<el-option
v-for="product in productOptions"
:key="product.id"
:label="product.name"
:value="product.id"
/>
</el-select>
</el-col>
<el-col :span="12" :xs="24">
<span class="select-label">
{{ $t('product-filter.document-type') }}
</span>
<el-select
v-model="model.selectedDocumentType"
:placeholder="$t('product-filter.select-document-type')"
clearable
>
<el-option
v-for="questionType in documentTypeOptions"
:key="questionType.id"
:label="questionType.name"
:value="questionType.id"
/>
</el-select>
</el-col>
<el-col :span="12" :xs="24">
<span class="select-label">{{ $t('product-filter.keyword') }}</span>
<el-input
v-model="model.keyword"
:placeholder="$t('product-filter.enter-keyword')"
clearable
:prefix-icon="Search"
/>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { Search } from '@element-plus/icons-vue';
defineProps({
productTypeOptions: {
type: Array as () => Array<DocumentListProductType>,
default: () => [],
},
productOptions: {
type: Array as () => Array<DocumentListProduct>,
default: () => [],
},
documentTypeOptions: {
type: Array as () => Array<DocumentTypeView>,
default: () => [],
},
});
const model = defineModel<{
selectedProduct: string | null;
selectedProductType: string | null;
selectedDocumentType: string | null;
keyword: string;
}>();
</script>
<style scoped>
.document-category {
margin-bottom: 1rem;
padding: 0 0;
}
.select-label {
color: var(--el-text-color-secondary);
font-size: 0.9rem;
}
:deep(.el-select__wrapper),
:deep(.el-input__wrapper) {
height: 40px;
font-size: 0.9rem;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="question-category">
<el-row class="hide-on-mobile" :gutter="12">
<el-row :gutter="12">
<el-col :span="12" :xs="12">
<span class="select-label">{{
$t('product-filter.product-type')

View File

@ -10,13 +10,15 @@
<app-breadcrumb class="breadcrumb" :items="breadcrumbItems" />
</div>
<div class="page-content">
<product-filter
<document-filter
v-model="filters"
:product-type-options="productTypeOptions"
:product-options="productOptions"
:document-type-options="documentTypeOptions"
/>
<!-- <document-list :documents="filteredDocuments" /> -->
<document-list :documents="paginatedDocuments" />
<el-pagination
v-model:current-page="page"
class="justify-center pagination-container"
@ -39,7 +41,8 @@
];
const filters = reactive({
selectedType: null as string | null,
selectedDocumentType: null as string | null,
selectedProductType: null as string | null,
selectedProduct: null as string | null,
keyword: '',
});
@ -49,6 +52,24 @@
const { data: documents, pending, error } = await useDocumentList();
const documentTypeOptions = computed(() => {
const types: DocumentTypeView[] = [];
documents.value.forEach((doc: DocumentListView) => {
if (!types.some((item) => item.id === doc.type.id)) {
if (doc.type.id === '-1') {
types.push({
id: '-1',
name: $t('product-filter.misc'),
});
} else {
types.push(doc.type);
}
}
});
return types;
});
const productTypeOptions = computed(() => {
const types: DocumentListProductType[] = [];
documents.value.forEach((doc: DocumentListView) => {
@ -64,13 +85,13 @@
});
const productOptions = computed(() => {
if (!filters.selectedType) return [];
if (!filters.selectedProductType) return [];
const products: DocumentListProduct[] = [];
documents.value.forEach((doc: DocumentListView) => {
doc.products?.forEach((product: DocumentListProduct) => {
if (
product.type.id === filters.selectedType &&
product.type.id === filters.selectedProductType &&
!products.some((item) => item.id === product.id)
) {
products.push(product);
@ -93,14 +114,18 @@
(product: DocumentListProduct) =>
product.id === filters.selectedProduct
)
: filters.selectedType
: filters.selectedProductType
? doc.products?.some(
(product: DocumentListProduct) =>
product.type?.id === filters.selectedType
product.type?.id === filters.selectedProductType
)
: true;
return matchProduct;
const matchDocumentType = filters.selectedDocumentType
? doc.type.id === filters.selectedDocumentType
: true;
return matchProduct && matchDocumentType;
});
});
@ -112,7 +137,7 @@
});
watch(
() => filters.selectedType,
() => filters.selectedProductType,
() => {
filters.selectedProduct = null;
}
@ -152,12 +177,6 @@
margin-left: auto;
}
.document-category {
padding: 0rem 2rem;
gap: 4px;
margin-bottom: 0.5rem;
}
.page-content {
padding: 1rem 2rem 2rem;
}

View File

@ -72,14 +72,18 @@
"documents": "Proporcionamos manuales de productos, especificaciones técnicas y otros documentos para la comodidad del usuario.",
"contact-info": "Contáctenos por teléfono o correo electrónico, y le brindaremos servicio presencial."
},
"product-filter": {
"product-type": "Tipo de producto",
"product-model": "Modelo del producto",
"keyword": "Palabra clave",
"select-product-type": "Seleccione el tipo de producto",
"select-product-model": "Seleccione modelo de producto",
"enter-keyword": "Ingrese palabra clave"
"enter-keyword": "Ingrese palabra clave",
"misc": "Varios",
"document-type": "Tipo de documento",
"select-document-type": "Seleccionar tipo de documento",
"question-type": "Tipo de pregunta",
"select-question-type": "Seleccionar tipo de pregunta"
},
"document-meta": {
"size": "Tamaño",

View File

@ -72,14 +72,18 @@
"documents": "Предоставляем документацию, такую как руководства по продуктам, технические спецификации, для удобства пользователей.",
"contact-info": "Свяжитесь с нами по телефону или электронной почте, и мы оперативно вам поможем."
},
"product-filter": {
"product-type": "Тип продукта",
"product-model": "Модель продукта",
"keyword": "Ключевое слово",
"select-product-type": "Выберите тип продукта",
"select-product-model": "Выберите модель продукта",
"enter-keyword": "Введите ключевое слово"
"enter-keyword": "Введите ключевое слово",
"misc": "разное",
"document-type": "Тип документа",
"question-type": "Тип вопроса",
"select-document-type": "Выберите тип документа",
"select-question-type": "Выберите тип вопроса"
},
"document-meta": {
"size": "Размер",

View File

@ -10,6 +10,13 @@ query GetDocumentList($locale: String!) {
id
title
}
type {
id
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
name
}
}
products {
id
products_id {

View File

@ -1,6 +1,51 @@
import { describe, test, expect } from 'vitest';
import { toProductDocumentView, toDocumentListView } from './documentMapper';
import {
toProductDocumentView,
toDocumentListView,
toDocumentTypeView,
} from './documentMapper';
/**
* 单元测试: toDocumentTypeView
*/
describe('toDocumentTypeView', () => {
const baseData: DocumentType = {
id: 1,
translations: [{ id: 1, name: 'Type Name' }],
};
test('convert raw data to DocumentTypeView correctly', () => {
const rawData: DocumentType = {
...baseData,
};
expect(toDocumentTypeView(rawData)).toEqual({
id: '1',
name: 'Type Name',
});
});
test('convert raw data with missing translations', () => {
const rawData: DocumentType = {
...baseData,
translations: [],
};
expect(toDocumentTypeView(rawData)).toEqual({
id: '1',
name: '',
});
});
test('convert null input to default DocumentTypeView', () => {
const rawData: DocumentType | null = null;
expect(toDocumentTypeView(rawData)).toEqual({
id: '-1',
name: '',
});
});
});
/**
* 单元测试: toProductDocumentView
*/
@ -65,6 +110,15 @@ describe('toProductDocumentView', () => {
describe('toDocumentListView', () => {
const baseData: ProductDocument = {
id: 1,
type: {
id: 1,
translations: [
{
id: 1,
name: 'Type A',
},
],
},
file: {
id: 'rand-om__-uuid-1234',
filename_download: 'document.pdf',
@ -94,6 +148,10 @@ describe('toDocumentListView', () => {
title: 'Document Title',
url: '/api/assets/rand-om__-uuid-1234',
size: 2048,
type: {
id: '1',
name: 'Type A',
},
products: [
{
id: '1',

View File

@ -1,5 +1,31 @@
import { isObject } from '../../server/utils/object';
/**
* 将 Directus 返回的 DocumentType 数据转换为 DocumentTypeView 视图模型
*
* @param raw: 原始的 DocumentType 数据
* @returns 转换后的 DocumentTypeView 对象
*
* @example
* const view = toDocumentTypeView(rawDocumentType);
*/
export function toDocumentTypeView(
raw: DocumentType | string | null
): DocumentTypeView {
if (typeof raw === 'string' || raw === null) {
return {
id: '-1',
name: '',
} satisfies DocumentTypeView;
}
const trans = raw.translations?.[0];
return {
id: raw.id.toString(),
name: trans?.name ?? '',
};
}
/**
* 将 Directus 返回的 Document 数据转换为 ProductDocumentView 视图模型
*
@ -40,6 +66,8 @@ export function toProductDocumentView(
export function toDocumentListView(raw: ProductDocument): DocumentListView {
const trans = raw.translations?.[0];
const type = toDocumentTypeView(raw.type ?? null);
const file = isObject<DirectusFile>(raw.file) ? raw.file : undefined;
const fileId = file?.id ?? '';
@ -73,6 +101,7 @@ export function toDocumentListView(raw: ProductDocument): DocumentListView {
title: trans?.title ?? '',
url: url,
size: file?.filesize ?? 0,
type: type,
products: related_products,
};
}

View File

@ -42,7 +42,7 @@ describe('toQuestionTypeView', () => {
expect(toQuestionTypeView(rawData)).toEqual({
id: '-1',
type: '',
name: '',
});
});
});

View File

@ -1,3 +1,14 @@
/**
* 文档类型视图模型
* 用于在文档库中提供类型筛选功能
*/
export interface DocumentTypeView {
/** 唯一标识符 **/
id: string;
/** 类型名 **/
name: string;
}
/**
* 文档关联产品类型模型
* 用于在文档库中提供产品筛选功能
@ -40,6 +51,9 @@ export interface DocumentListView {
/** 文档链接 **/
url: string;
/** 文档类型 **/
type: DocumentTypeView;
/** 相关产品 **/
products: DocumentListProduct[];
}