refactor: 重构产品相关Mapper

- 空值处理与类型控制:为相关关系型字段的数据处理添加空值处理与类型控制
- 调整目录结构:将文件目录按照实际查询划分为Product和ProductList两个文件
This commit is contained in:
2025-12-04 17:38:43 +08:00
parent f081a3b33a
commit 1245df497b
14 changed files with 801 additions and 414 deletions

View File

@ -1,9 +1,5 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { import { toDocumentListView, toDocumentTypeView } from './documentMapper';
toProductDocumentView,
toDocumentListView,
toDocumentTypeView,
} from './documentMapper';
/** /**
* 单元测试: toDocumentTypeView * 单元测试: toDocumentTypeView
@ -46,63 +42,6 @@ describe('toDocumentTypeView', () => {
}); });
}); });
}); });
/**
* 单元测试: toProductDocumentView
*/
describe('toProductDocumentView', () => {
const baseData: ProductDocument = {
id: 1,
file: {
id: 'rand-om__-uuid-1234',
filename_download: 'document.pdf',
filesize: 2048,
},
translations: [{ id: 10, title: 'Document Title' }],
};
test('convert raw data with fileMeta to ProductDocumentView correctly', () => {
const rawData: ProductDocument = { ...baseData };
expect(toProductDocumentView(rawData)).toEqual({
id: '1',
fileId: 'rand-om__-uuid-1234',
filename: 'document.pdf',
title: 'Document Title',
url: '/api/assets/rand-om__-uuid-1234',
size: 2048,
});
});
test('convert raw data with fileId', () => {
const rawData: ProductDocument = {
...baseData,
file: 'rand-om__-uuid-1234',
};
expect(toProductDocumentView(rawData)).toEqual({
id: '1',
fileId: '',
filename: '',
title: 'Document Title',
url: '/api/assets/',
size: 0,
});
});
test('convert raw data with missing translations', () => {
const rawData: ProductDocument = {
...baseData,
translations: [],
};
expect(toProductDocumentView(rawData)).toEqual({
id: '1',
fileId: 'rand-om__-uuid-1234',
filename: 'document.pdf',
title: '',
url: '/api/assets/rand-om__-uuid-1234',
size: 2048,
});
});
});
/** /**
* 单元测试: toDocumentListView * 单元测试: toDocumentListView

View File

@ -26,34 +26,6 @@ export function toDocumentTypeView(
}; };
} }
/**
* 将 Directus 返回的 Document 数据转换为 ProductDocumentView 视图模型
*
* @param raw: 原始的 Document 数据
* @returns 转换后的 ProductDocumentView 对象
*
* @example
* const view = toProductDocumentView(rawDocument);
*/
export function toProductDocumentView(
raw: ProductDocument
): ProductDocumentView {
const trans = raw.translations?.[0];
const file = isObject<DirectusFile>(raw.file) ? raw.file : undefined;
const fileId = file?.id ?? '';
const url = `/api/assets/${fileId}`;
return {
id: raw.id.toString(),
fileId: fileId,
filename: file?.filename_download ?? '',
title: trans?.title ?? '',
url: url,
size: file?.filesize ?? 0,
};
}
/** /**
* 将 Directus 返回的 Document 数据转换为 DocumentListView 视图模型 * 将 Directus 返回的 Document 数据转换为 DocumentListView 视图模型
* *

View File

@ -0,0 +1,125 @@
import { describe, expect, test } from 'vitest';
import { toProductTypeView, toProductListView } from './productListMapper';
/**
* 单元测试: toProductTypeView
*/
describe('toProductTypeView', () => {
const baseData: ProductType = {
id: 1,
translations: [
{
id: 1,
name: 'Type Name',
},
],
sort: 5,
};
test('convert raw data to ProductTypeView correctly', () => {
const rawData: ProductType = { ...baseData };
expect(toProductTypeView(rawData)).toEqual({
id: '1',
name: 'Type Name',
sort: 5,
});
});
test('convert raw data with missing translations', () => {
const rawData: ProductType = {
...baseData,
translations: [],
};
expect(toProductTypeView(rawData)).toEqual({
id: '1',
name: '',
sort: 5,
});
});
test('convert raw data with missing sort', () => {
const rawData: ProductType = {
...baseData,
sort: undefined,
};
expect(toProductTypeView(rawData)).toEqual({
id: '1',
name: 'Type Name',
sort: 999,
});
});
test('convert null input to default value', () => {
const rawData: ProductType | string | null = null;
expect(toProductTypeView(rawData)).toEqual({
id: '-1',
name: '',
sort: 999,
});
});
});
/**
* 单元测试: toProductListView
*/
describe('toProductListView', () => {
test('convert raw data to ProductListView correctly', () => {
const rawData: Product = {
id: 1,
translations: [
{ id: 10, name: 'Product Name', summary: 'Product Summary' },
],
cover: {
id: 'cover-file-uuid-1234',
},
product_type: {
id: 1,
translations: [{ id: 20, name: 'Type Name' }],
sort: 1,
},
};
expect(toProductListView(rawData)).toEqual({
id: '1',
name: 'Product Name',
summary: 'Product Summary',
cover: 'cover-file-uuid-1234',
product_type: {
id: '1',
name: 'Type Name',
sort: 1,
},
});
});
test('convert raw data with missing translations', () => {
const rawData: Product = {
id: 1,
translations: [],
cover: {
id: 'cover-file-uuid-1234',
},
product_type: {
id: 20,
translations: [],
sort: 1,
},
};
expect(toProductListView(rawData)).toEqual({
id: '1',
name: '',
summary: '',
cover: 'cover-file-uuid-1234',
product_type: {
id: '20',
name: '',
sort: 1,
},
});
});
});

View File

@ -0,0 +1,54 @@
import { isObject } from '../../server/utils/object';
/**
* 将Directus返回的ProductType数据转换为ProductTypeView视图模型
*
* @param raw: 原始的ProductType数据
* @returns 转换后的ProductTypeView对象
*
* @example
* const view = toProductTypeView(rawProductType);
*/
export function toProductTypeView(
raw: ProductType | string | null
): ProductTypeView {
if (typeof raw === 'string' || raw === null) {
return {
id: '-1',
name: '',
sort: 999,
} satisfies ProductTypeView;
}
const trans = raw.translations?.[0] ?? { name: '' };
return {
id: raw.id.toString(),
name: trans.name,
sort: raw?.sort ?? 999,
} satisfies ProductTypeView;
}
/**
* 将 Directus返回的 Product 数据转换为 ProductListView 视图模型
*
* @param raw: 原始的 Product 数据
* @returns 转换后的 ProductListView 对象
*
* @example
* const view = toProductListView(rawProduct);
*/
export function toProductListView(raw: Product): ProductListView {
const trans = raw.translations?.[0];
const type = toProductTypeView(raw.product_type ?? null);
const cover = isObject<DirectusFile>(raw.cover) ? raw.cover.id : '';
return {
id: raw.id.toString(),
product_type: type,
name: trans?.name ?? '',
summary: trans?.summary ?? '',
cover: cover,
} satisfies ProductListView;
}

View File

@ -1,69 +1,111 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { import {
toProductListView, toProductImageView,
toProductSpecView, toProductSpecView,
toProductSpecGroupView, toProductSpecGroupView,
toProductQuestionView,
toProductDocumentView,
toProductView, toProductView,
} from './productMapper'; } from './productMapper';
/** /**
* 单元测试: toProductListView * 单元测试: toProductImageView
*/ */
describe('toProductListView', () => { describe('toProductImageView', () => {
test('convert raw data to ProductListView correctly', () => { const baseData: Array<ProductsProductImage> = [
const rawData: Product = { {
id: 1, id: 1,
product_images_id: {
id: 1,
image: {
id: 'rand-om__-uuid-1234',
},
translations: [ translations: [
{ id: 10, name: 'Product Name', summary: 'Product Summary' }, {
],
cover: {
id: 'cover-file-uuid-1234',
},
product_type: {
id: 1, id: 1,
translations: [{ id: 20, name: 'Type Name' }], caption: 'Image Caption',
sort: 1,
}, },
}; ],
},
},
{
id: 2,
product_images_id: {
id: 2,
image: {
id: 'rand-om__-uuid-5678',
},
translations: [
{
id: 2,
caption: 'Image Caption 2',
},
],
},
},
];
expect(toProductListView(rawData)).toEqual({ test('convert raw data to ProductImageView correctly', () => {
const rawData: Array<ProductsProductImage> = [...baseData];
expect(toProductImageView(rawData)).toEqual([
{
id: '1', id: '1',
name: 'Product Name', image: 'rand-om__-uuid-1234',
summary: 'Product Summary', caption: 'Image Caption',
cover: 'cover-file-uuid-1234',
product_type: {
id: '1',
name: 'Type Name',
sort: 1,
}, },
}); {
id: '2',
image: 'rand-om__-uuid-5678',
caption: 'Image Caption 2',
},
]);
}); });
test('convert raw data with missing translations', () => { test('convert raw data with missing translations', () => {
const rawData: Product = { const rawData: Array<ProductsProductImage> = [
{
id: 1, id: 1,
translations: [], product_images_id: {
cover: { id: 1,
id: 'cover-file-uuid-1234', image: {
id: 'rand-om__-uuid-1234',
}, },
product_type: {
id: 20,
translations: [], translations: [],
sort: 1,
}, },
}; },
];
expect(toProductListView(rawData)).toEqual({ expect(toProductImageView(rawData)).toEqual([
{
id: '1', id: '1',
name: '', image: 'rand-om__-uuid-1234',
summary: '', caption: '',
cover: 'cover-file-uuid-1234',
product_type: {
id: '20',
name: '',
sort: 1,
}, },
]);
}); });
test('convert empty array to empty ProductImageView array', () => {
const rawData: Array<ProductsProductImage> = [];
expect(toProductImageView(rawData)).toEqual([]);
});
test('convert input with wrong type to default value', () => {
const rawData: string[] = ['1', '2'];
expect(toProductImageView(rawData)).toEqual([
{
id: '1',
image: '',
caption: '',
},
{
id: '2',
image: '',
caption: '',
},
]);
}); });
}); });
@ -71,30 +113,78 @@ describe('toProductListView', () => {
* 单元测试: toProductSpecView * 单元测试: toProductSpecView
*/ */
describe('toProductSpecView', () => { describe('toProductSpecView', () => {
test('convert raw data to ProductSpecView correctly', () => { const baseData: ProductSpec[] = [
const rawData: ProductSpec = { {
id: 1, id: 1,
translations: [{ id: 1, key: 'Key', value: 'Value' }], translations: [
}; {
id: 1,
key: 'key 1',
value: 'value 1',
},
],
},
{
id: 2,
translations: [
{
id: 2,
key: 'key 2',
value: 'value 2',
},
],
},
];
test('convert raw data to ProductSpecView correctly', () => {
const rawData: ProductSpec[] = [...baseData];
expect(toProductSpecView(rawData)).toEqual({ expect(toProductSpecView(rawData)).toEqual([
{
id: '1', id: '1',
key: 'Key', key: 'key 1',
value: 'Value', value: 'value 1',
}); },
{
id: '2',
key: 'key 2',
value: 'value 2',
},
]);
}); });
test('convert raw data with missing translations', () => { test('convert raw data with missing translations', () => {
const rawData: ProductSpec = { const rawData: ProductSpec[] = [{ id: 1, translations: [] }];
id: 1,
translations: [],
};
expect(toProductSpecView(rawData)).toEqual({ expect(toProductSpecView(rawData)).toEqual([
{
id: '1', id: '1',
key: '', key: '',
value: '', value: '',
},
]);
}); });
test('convert empty input to empty array', () => {
const rawData: ProductSpec[] = [];
expect(toProductSpecView(rawData)).toEqual([]);
});
test('convert input with wrong type to default value', () => {
const rawData: string[] = ['1', '2'];
expect(toProductSpecView(rawData)).toEqual([
{
id: '1',
key: '',
value: '',
},
{
id: '2',
key: '',
value: '',
},
]);
}); });
}); });
@ -103,37 +193,237 @@ describe('toProductSpecView', () => {
*/ */
describe('toProductSpecGroupView', () => { describe('toProductSpecGroupView', () => {
test('convert raw data to ProductSpecGroupView correctly', () => { test('convert raw data to ProductSpecGroupView correctly', () => {
const rawData: ProductSpecGroup = { const rawData: ProductSpecGroup[] = [
{
id: 1, id: 1,
translations: [{ id: 1, name: 'Group Name' }], translations: [{ id: 1, name: 'Group Name' }],
specs: [ specs: [
{ id: 1, translations: [{ id: 1, key: 'Key1', value: 'Value1' }] }, { id: 1, translations: [{ id: 1, key: 'Key1', value: 'Value1' }] },
{ id: 2, translations: [{ id: 2, key: 'Key2', value: 'Value2' }] }, { id: 2, translations: [{ id: 2, key: 'Key2', value: 'Value2' }] },
], ],
}; },
];
expect(toProductSpecGroupView(rawData)).toEqual({ expect(toProductSpecGroupView(rawData)).toEqual([
{
id: '1', id: '1',
name: 'Group Name', name: 'Group Name',
specs: [ specs: [
{ id: '1', key: 'Key1', value: 'Value1' }, { id: '1', key: 'Key1', value: 'Value1' },
{ id: '2', key: 'Key2', value: 'Value2' }, { id: '2', key: 'Key2', value: 'Value2' },
], ],
}); },
]);
}); });
test('convert raw data with missing translations', () => { test('convert raw data with missing translations', () => {
const rawData: ProductSpecGroup = { const rawData: ProductSpecGroup[] = [
{
id: 1, id: 1,
translations: [], translations: [],
specs: [], specs: [],
}; },
];
expect(toProductSpecGroupView(rawData)).toEqual({ expect(toProductSpecGroupView(rawData)).toEqual([
{
id: '1', id: '1',
name: '', name: '',
specs: [], specs: [],
},
]);
}); });
test('convert input with wrong type to default value', () => {
const rawData: string[] = ['1', '2'];
expect(toProductSpecGroupView(rawData)).toEqual([
{
id: '1',
name: '',
specs: [],
},
{
id: '2',
name: '',
specs: [],
},
]);
});
test('convert empty ar ray to empty ProductSpecGroupView array', () => {
const rawData: ProductSpec[] = [];
expect(toProductSpecGroupView(rawData)).toEqual([]);
});
});
/**
* 单元测试: toProductQuestionView
*/
describe('toProductQuestionView', () => {
test('convert raw data to ProductQuestionView correctly', () => {
const rawData: ProductsQuestion[] = [
{
id: 1,
questions_id: {
id: 1,
translations: [
{
id: 1,
title: 'Question Title',
content: 'Question Content',
},
],
},
},
];
expect(toProductQuestionView(rawData)).toEqual([
{
id: '1',
title: 'Question Title',
content: 'Question Content',
},
]);
});
test('convert raw data with missing translations', () => {
const rawData: ProductsQuestion[] = [
{
id: 1,
questions_id: {
id: 1,
translations: [],
},
},
];
expect(toProductQuestionView(rawData)).toEqual([
{
id: '1',
title: '',
content: '',
},
]);
});
test('convert empty array to empty ProductQuestionView array', () => {
const rawData: ProductsQuestion[] = [];
expect(toProductQuestionView(rawData)).toEqual([]);
});
test('convert input with wrong type to default value', () => {
const rawData: string[] = ['1', '2'];
expect(toProductQuestionView(rawData)).toEqual([
{
id: '1',
title: '',
content: '',
},
{
id: '2',
title: '',
content: '',
},
]);
});
});
/**
* 单元测试: toProductDocumentView
*/
describe('toProductDocumentView', () => {
test('convert raw data to ProductDocumentView correctly', () => {
const rawData: ProductsProductDocument[] = [
{
id: 1,
product_documents_id: {
id: 1,
file: {
id: 'rand-om__-uuid-1234',
filesize: 1000,
filename_download: 'doc1.pdf',
},
translations: [
{
id: 1,
title: 'Document Title 1',
},
],
},
},
];
expect(toProductDocumentView(rawData)).toEqual([
{
id: '1',
fileId: 'rand-om__-uuid-1234',
filename: 'doc1.pdf',
title: 'Document Title 1',
size: 1000,
url: '/api/assets/rand-om__-uuid-1234',
},
]);
});
test('convert raw data with missing translations', () => {
const rawData: ProductsProductDocument[] = [
{
id: 1,
product_documents_id: {
id: 1,
file: {
id: 'rand-om__-uuid-1234',
filesize: 1000,
filename_download: 'doc1.pdf',
},
translations: [],
},
},
];
expect(toProductDocumentView(rawData)).toEqual([
{
id: '1',
fileId: 'rand-om__-uuid-1234',
filename: 'doc1.pdf',
title: '',
size: 1000,
url: '/api/assets/rand-om__-uuid-1234',
},
]);
});
test('convert empty array to empty ProductDocumentView array', () => {
const rawData: ProductsProductDocument[] = [];
expect(toProductDocumentView(rawData)).toEqual([]);
});
test('convert input with wrong type to default value', () => {
const rawData: string[] = ['1', '2'];
expect(toProductDocumentView(rawData)).toEqual([
{
id: '1',
fileId: '',
filename: '',
title: '',
size: 0,
url: '',
},
{
id: '2',
fileId: '',
filename: '',
title: '',
size: 0,
url: '',
},
]);
}); });
}); });
@ -159,7 +449,6 @@ describe('toProductView', () => {
name: 'Product Name', name: 'Product Name',
summary: 'Product Summary', summary: 'Product Summary',
description: 'Product Description', description: 'Product Description',
product_type: '',
images: [], images: [],
documents: [], documents: [],
faqs: [], faqs: [],
@ -178,7 +467,6 @@ describe('toProductView', () => {
name: '', name: '',
summary: '', summary: '',
description: '', description: '',
product_type: '',
images: [], images: [],
documents: [], documents: [],
faqs: [], faqs: [],

View File

@ -1,55 +1,41 @@
import { toProductQuestionView } from './questionMapper';
import { toProductDocumentView } from './documentMapper';
import { isObject } from '../../server/utils/object'; import { isObject } from '../../server/utils/object';
/** /**
* 将Directus返回的ProductType数据转换为ProductTypeView视图模型 * 将 Directus 返回的 ProductImage 数据转换为 ProductImageView 视图模型
* *
* @param raw: 原始的ProductType数据 * @param raw: 原始的 ProductsProductImage 数据
* @returns 转换后的ProductTypeView对象 * @returns 转换后的 ProductImageView 对象
* *
* @example * @example
* const view = toProductTypeView(rawProductType); * const view = toProductImageView(rawProductImage);
*/ */
export function toProductTypeView(raw: ProductType): ProductTypeView { export function toProductImageView(
const trans = raw.translations?.[0] ?? { name: '' }; raw: (ProductsProductImage | string)[]
): ProductImageView[] {
return (raw ?? []).map((item) => {
if (!isObject<ProductsProductImage>(item))
return {
id: item,
image: '',
caption: '',
} satisfies ProductImageView;
const image = item.product_images_id;
if (!isObject<ProductImage>(image))
return {
id: item.id.toString(),
image: '',
caption: '',
} satisfies ProductImageView;
const trans = image.translations?.[0];
return { return {
id: raw.id.toString(), id: item.id.toString(),
name: trans.name, image: isObject<DirectusFile>(image.image) ? image.image.id : '',
sort: raw?.sort ?? 999, caption: trans?.caption || '',
}; } satisfies ProductImageView;
} });
/**
* 将 Directus返回的 Product 数据转换为 ProductListView 视图模型
*
* @param raw: 原始的 Product 数据
* @returns 转换后的 ProductListView 对象
*
* @example
* const view = toProductListView(rawProduct);
*/
export function toProductListView(raw: Product): ProductListView {
const trans = raw.translations?.[0];
const type = isObject<ProductType>(raw.product_type)
? toProductTypeView(raw.product_type)
: ({
id: '',
name: '',
sort: 999,
} satisfies ProductTypeView);
const cover = isObject<DirectusFile>(raw.cover) ? raw.cover.id : '';
return {
id: raw.id.toString(),
product_type: type,
name: trans?.name ?? '',
summary: trans?.summary ?? '',
cover: cover,
};
} }
/** /**
@ -62,19 +48,26 @@ export function toProductListView(raw: Product): ProductListView {
* const view = toProductSpecGroupView(rawSpecGroup); * const view = toProductSpecGroupView(rawSpecGroup);
*/ */
export function toProductSpecGroupView( export function toProductSpecGroupView(
raw: ProductSpecGroup raw: (ProductSpecGroup | string)[]
): ProductSpecGroupView { ): ProductSpecGroupView[] {
const trans = raw.translations?.[0]; return (raw ?? []).map((item) => {
if (!isObject<ProductSpecGroup>(item)) {
return {
id: item,
name: '',
specs: [],
} satisfies ProductSpecGroupView;
}
const trans = item.translations?.[0];
const specs = raw.specs ?? []; const specs = toProductSpecView(item?.specs ?? []);
return { return {
id: raw.id.toString(), id: item.id.toString(),
name: trans?.name ?? '', name: trans?.name ?? '',
specs: specs specs: specs,
.filter(isObject<ProductSpec>) } satisfies ProductSpecGroupView;
.map((item) => toProductSpecView(item)), });
};
} }
/** /**
@ -86,14 +79,120 @@ export function toProductSpecGroupView(
* @example * @example
* const view = toProductSpecView(rawSpecGroup); * const view = toProductSpecView(rawSpecGroup);
*/ */
export function toProductSpecView(raw: ProductSpec): ProductSpecView { export function toProductSpecView(
const trans = raw.translations?.[0]; raw: (ProductSpec | string)[]
): ProductSpecView[] {
return (raw ?? []).map((item) => {
if (!isObject<ProductSpec>(item)) {
return {
id: item,
key: '',
value: '',
} satisfies ProductSpecView;
}
const trans = item.translations?.[0];
return { return {
id: raw.id.toString(), id: item.id.toString(),
key: trans?.key ?? '', key: trans?.key ?? '',
value: trans?.value ?? '', value: trans?.value ?? '',
}; } satisfies ProductSpecView;
});
}
/**
* 将 Directus 返回的 ProductQuestion 数据转换为 ProductQuestionView 视图模型
*
* @param raw: 原始的ProductQuestion 数据
* @returns 转换后的 ProductQuestionView 对象
*
* @example
* const view = toProductQuestionView(rawQuestion);
*/
export function toProductQuestionView(
raw: (ProductsQuestion | string)[]
): ProductQuestionView[] {
return (raw ?? []).map((item) => {
if (!isObject<ProductsQuestion>(item)) {
return {
id: item,
title: '',
content: '',
} satisfies ProductQuestionView;
}
const question = item.questions_id;
if (!isObject<Question>(question)) {
return {
id: item.id.toString(),
title: '',
content: '',
} satisfies ProductQuestionView;
}
const trans = question.translations?.[0];
return {
id: item.id.toString(),
title: trans?.title ?? '',
content: trans?.content ?? '',
} satisfies ProductQuestionView;
});
}
/**
* 将 Directus 返回的 ProductDocument 数据转换为 ProductDocumentView 视图模型
*
* @param raw: 原始的ProductDocument 数据
* @returns 转换后的 ProductDocumentView 对象
*
* @example
* const view = toProductDocumentView(rawDocument);
*/
export function toProductDocumentView(
raw: (ProductsProductDocument | string)[]
): ProductDocumentView[] {
return (raw ?? []).map((item) => {
if (!isObject<ProductsProductDocument>(item)) {
return {
id: item,
fileId: '',
filename: '',
size: 0,
title: '',
url: '',
} satisfies ProductDocumentView;
}
const document = item.product_documents_id;
if (!isObject<ProductDocument>(document)) {
return {
id: item.id.toString(),
fileId: '',
filename: '',
size: 0,
title: '',
url: '',
} satisfies ProductDocumentView;
}
const file = isObject<DirectusFile>(document.file)
? document.file
: undefined;
const url = file ? `/api/assets/${file.id}` : '';
const trans = document.translations?.[0];
return {
id: item.id.toString(),
fileId: file?.id ?? '',
filename: file?.filename_download ?? '',
size: file?.filesize ?? 0,
title: trans?.title ?? '',
url: url,
} satisfies ProductDocumentView;
});
} }
/** /**
@ -108,42 +207,16 @@ export function toProductSpecView(raw: ProductSpec): ProductSpecView {
export function toProductView(raw: Product): ProductView { export function toProductView(raw: Product): ProductView {
const trans = raw.translations?.[0]; const trans = raw.translations?.[0];
const images = (raw.images ?? []) const images = toProductImageView(raw.images ?? []);
.filter(isObject<ProductsProductImage>)
.map((item) => item.product_images_id)
.filter(isObject<ProductImage>)
.map((item) => {
const image = isObject<DirectusFile>(item.image) ? item.image.id : '';
return {
id: item.id.toString(),
image: image,
caption: item.translations?.[0]?.caption || '',
};
});
const type = isObject<ProductType>(raw.product_type) const specs = toProductSpecGroupView(raw.specs ?? []);
? (raw.product_type.translations?.[0]?.name ?? '')
: '';
const specs = (raw.specs ?? []) const faqs = toProductQuestionView(raw.faqs ?? []);
.filter(isObject<ProductSpecGroup>)
.map((item) => toProductSpecGroupView(item));
const faqs = (raw.faqs ?? []) const documents = toProductDocumentView(raw.documents ?? []);
.filter(isObject<ProductsQuestion>)
.map((item) => item.questions_id)
.filter(isObject<Question>)
.map((item) => toProductQuestionView(item));
const documents = (raw.documents ?? [])
.filter(isObject<ProductsProductDocument>)
.map((item) => item.product_documents_id)
.filter(isObject<ProductDocument>)
.map((item) => toProductDocumentView(item));
return { return {
id: raw.id.toString(), id: raw.id.toString(),
product_type: type,
name: trans?.name ?? '', name: trans?.name ?? '',
summary: trans?.summary ?? '', summary: trans?.summary ?? '',
images: images, images: images,
@ -151,5 +224,5 @@ export function toProductView(raw: Product): ProductView {
specs: specs, specs: specs,
faqs: faqs, faqs: faqs,
documents: documents, documents: documents,
}; } satisfies ProductView;
} }

View File

@ -1,9 +1,5 @@
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import { import { toQuestionListView, toQuestionTypeView } from './questionMapper';
toProductQuestionView,
toQuestionListView,
toQuestionTypeView,
} from './questionMapper';
/** /**
* 单元测试: toQuestionTypeView * 单元测试: toQuestionTypeView
@ -47,42 +43,6 @@ describe('toQuestionTypeView', () => {
}); });
}); });
/**
* 单元测试: toProductQuestionView
*/
describe('toProductQuestionView', () => {
const baseData: Question = {
id: 1,
translations: [
{ id: 1, title: 'Question Title', content: 'Question Answer' },
],
};
test('convert raw data to ProductQuestionView correctly', () => {
const rawData: Question = {
...baseData,
};
expect(toProductQuestionView(rawData)).toEqual({
id: '1',
title: 'Question Title',
content: 'Question Answer',
});
});
test('convert raw data with missing translations', () => {
const rawData: Question = {
...baseData,
translations: [],
};
expect(toProductQuestionView(rawData)).toEqual({
id: '1',
title: '',
content: '',
});
});
});
/** /**
* 单元测试: toQuestionListView * 单元测试: toQuestionListView
*/ */

View File

@ -26,25 +26,6 @@ export function toQuestionTypeView(
}; };
} }
/**
* 将 Directus 返回的 Question 数据转换为 ProductQuestionView 视图模型
*
* @param raw: 原始的 Question 数据
* @returns 转换后的 ProductQuestionView 对象
*
* @example
* const view = toProductQuestionView(rawQuestion);
*/
export function toProductQuestionView(raw: Question): ProductQuestionView {
const trans = raw.translations?.[0];
return {
id: raw.id.toString(),
title: trans?.title ?? '',
content: trans?.content ?? '',
};
}
/** /**
* 将 Directus 返回的 Question 数据转换为 QuestionListView 视图模型 * 将 Directus 返回的 Question 数据转换为 QuestionListView 视图模型
* *

View File

@ -1,7 +1,5 @@
import { import { toProductView } from '~~/server/mappers/productMapper';
toProductView, import { toProductListView } from '~~/server/mappers/productListMapper';
toProductListView,
} from '~~/server/mappers/productMapper';
export const productService = { export const productService = {
async getProductList(locale: string) { async getProductList(locale: string) {

View File

@ -2,9 +2,6 @@ export * from './solution-view';
export * from './solution-list-view'; export * from './solution-list-view';
export * from './product-view'; export * from './product-view';
export * from './product-list-view'; export * from './product-list-view';
export * from './product-spec-group-view';
export * from './product-document-view';
export * from './product-question-view';
export * from './document-list-view'; export * from './document-list-view';
export * from './question-list-view'; export * from './question-list-view';
export * from './company-profile-view'; export * from './company-profile-view';

View File

@ -1,23 +0,0 @@
/**
* 文档视图模型
* 用于文档列表渲染的数据结构
*/
export interface ProductDocumentView {
/** 唯一标识符 **/
id: string;
/** 文件UUID **/
fileId: string;
/** 文件名 **/
filename: string;
/** 文档标题 **/
title: string;
/** 文档大小 **/
size: number;
/** 文档链接 **/
url: string;
}

View File

@ -1,14 +0,0 @@
/**
* 常见问题视图模型
* 用于产品页常见问题渲染的数据结构
*/
export interface ProductQuestionView {
/** 唯一标识符 **/
id: string;
/** 问题标题 **/
title: string;
/** 问题内容 **/
content: string;
}

View File

@ -1,29 +0,0 @@
/**
* 产品规格模型
* 用于产品规格渲染的数据结构
*/
export interface ProductSpecView {
/** 唯一标识符 **/
id: string;
/** 规格名称 **/
key: string;
/** 规格值 **/
value: string;
}
/**
* 产品规格表模型
* 用于产品规格表渲染的数据结构
*/
export interface ProductSpecGroupView {
/** 唯一标识符 **/
id: string;
/** 规格组名称 **/
name: string;
/** 规格组 **/
specs: ProductSpecView[];
}

View File

@ -1,13 +1,82 @@
import type { ProductSpecGroupView } from './product-spec-group-view'; /**
import type { ProductQuestionView } from './product-question-view'; * 产品图片视图模型
import type { ProductDocumentView } from './product-document-view'; * 用于产品详情页(/products/[slug])中的产品图片数据结构
*/
interface ImageView { export interface ProductImageView {
id: string; id: string;
image: string; image: string;
caption: string; caption: string;
} }
/**
* 产品规格模型
* 用于产品规格渲染的数据结构
*/
export interface ProductSpecView {
/** 唯一标识符 **/
id: string;
/** 规格名称 **/
key: string;
/** 规格值 **/
value: string;
}
/**
* 产品规格表模型
* 用于产品规格表渲染的数据结构
*/
export interface ProductSpecGroupView {
/** 唯一标识符 **/
id: string;
/** 规格组名称 **/
name: string;
/** 规格组 **/
specs: ProductSpecView[];
}
/**
* 常见问题视图模型
* 用于产品页常见问题渲染的数据结构
*/
export interface ProductQuestionView {
/** 唯一标识符 **/
id: string;
/** 问题标题 **/
title: string;
/** 问题内容 **/
content: string;
}
/**
* 文档视图模型
* 用于文档列表渲染的数据结构
*/
export interface ProductDocumentView {
/** 唯一标识符 **/
id: string;
/** 文件UUID **/
fileId: string;
/** 文件名 **/
filename: string;
/** 文档标题 **/
title: string;
/** 文档大小 **/
size: number;
/** 文档链接 **/
url: string;
}
/** /**
* 产品视图模型 * 产品视图模型
* 用于产品详情页(/products/[slug])渲染的数据结构 * 用于产品详情页(/products/[slug])渲染的数据结构
@ -22,11 +91,8 @@ export interface ProductView {
/** 产品简介 **/ /** 产品简介 **/
summary: string; summary: string;
/** 产品类型 **/
product_type: string;
/** 产品图片 **/ /** 产品图片 **/
images: ImageView[]; images: ProductImageView[];
/** 产品详情 **/ /** 产品详情 **/
description: string; description: string;