feat(support): 为文档库/相关问题页面添加类型筛选功能
All checks were successful
deploy to server / build-and-deploy (push) Successful in 3m1s

- 功能添加:文档库/FAQ页添加类型筛选功能
- 类型同步:前端补全问题类型与文档类型的类型标注,与后端移至
- 查询变更:GraphQL查询增添文档/问题类型查询
- 组件分离:将原先文档库与FAQ页共用的ProductFilter分离为两个不同的组件:DocumentFilter与QuestionFilter
- i18n文本补全:为新增的相关文本补全国际化翻译
This commit is contained in:
2025-12-03 18:00:03 +08:00
17 changed files with 510 additions and 101 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,12 +1,12 @@
<template> <template>
<div class="question-category"> <div class="question-category">
<el-row class="hide-on-mobile" :gutter="12"> <el-row :gutter="12">
<el-col :span="8"> <el-col :span="12" :xs="12">
<span class="select-label">{{ <span class="select-label">{{
$t('product-filter.product-type') $t('product-filter.product-type')
}}</span> }}</span>
<el-select <el-select
v-model="model.selectedType" v-model="model.selectedProductType"
:placeholder="$t('product-filter.select-product-type')" :placeholder="$t('product-filter.select-product-type')"
clearable clearable
> >
@ -19,7 +19,7 @@
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="12" :xs="12">
<span class="select-label">{{ <span class="select-label">{{
$t('product-filter.product-model') $t('product-filter.product-model')
}}</span> }}</span>
@ -37,54 +37,25 @@
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="8"> <el-col :span="12" :xs="24">
<span class="select-label">{{ $t('product-filter.keyword') }}</span> <span class="select-label">
<el-input {{ $t('product-filter.question-type') }}
v-model="model.keyword" </span>
:placeholder="$t('product-filter.enter-keyword')"
clearable
:prefix-icon="Search"
/>
</el-col>
</el-row>
<el-row class="display-on-mobile" :gutter="12">
<el-col :span="12">
<span class="select-label">{{
$t('product-filter.product-type')
}}</span>
<el-select <el-select
v-model="model.selectedType" v-model="model.selectedQuestionType"
:placeholder="$t('product-filter.select-product-type')" :placeholder="$t('product-filter.select-question-type')"
clearable clearable
> >
<el-option <el-option
v-for="type in productTypeOptions" v-for="questionType in questionTypeOptions"
:key="type.id" :key="questionType.id"
:label="type.name" :label="questionType.name"
:value="type.id" :value="questionType.id"
/> />
</el-select> </el-select>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12" :xs="24">
<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-row>
<el-row class="display-on-mobile">
<span class="select-label">{{ $t('product-filter.keyword') }}</span> <span class="select-label">{{ $t('product-filter.keyword') }}</span>
<el-input <el-input
v-model="model.keyword" v-model="model.keyword"
@ -92,6 +63,7 @@
clearable clearable
:prefix-icon="Search" :prefix-icon="Search"
/> />
</el-col>
</el-row> </el-row>
</div> </div>
</template> </template>
@ -101,19 +73,24 @@
defineProps({ defineProps({
productTypeOptions: { productTypeOptions: {
type: Array as () => Array<{ id: string; name: string }>, type: Array as () => Array<QuestionListProductType>,
default: () => [], default: () => [],
}, },
productOptions: { productOptions: {
type: Array as () => Array<{ id: string; name: string }>, type: Array as () => Array<QuestionListProduct>,
default: () => [],
},
questionTypeOptions: {
type: Array as () => Array<QuestionTypeView>,
default: () => [], default: () => [],
}, },
}); });
const model = defineModel<{ const model = defineModel<{
keyword: string;
selectedType: string | null;
selectedProduct: string | null; selectedProduct: string | null;
selectedProductType: string | null;
selectedQuestionType: string | null;
keyword: string;
}>(); }>();
</script> </script>
@ -132,18 +109,4 @@
height: 40px; height: 40px;
font-size: 0.9rem; font-size: 0.9rem;
} }
.display-on-mobile {
display: none;
}
@media (max-width: 768px) {
.hide-on-mobile {
display: none;
}
.display-on-mobile {
display: flex;
margin-bottom: 1rem;
}
}
</style> </style>

View File

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

View File

@ -11,10 +11,11 @@
</div> </div>
<div class="page-content"> <div class="page-content">
<product-filter <question-filter
v-model="filters" v-model="filters"
:product-type-options="productTypeOptions" :product-type-options="productTypeOptions"
:product-options="productOptions" :product-options="productOptions"
:question-type-options="questionTypeOptions"
/> />
<question-list :questions="paginatedQuestions" /> <question-list :questions="paginatedQuestions" />
@ -37,8 +38,9 @@
const route = useRoute(); const route = useRoute();
const filters = reactive({ const filters = reactive({
selectedType: null as string | null, selectedQuestionType: null as string | null,
selectedProduct: null as string | null, selectedProduct: null as string | null,
selectedProductType: null as string | null,
keyword: '', keyword: '',
}); });
@ -57,6 +59,23 @@
const { data: questions, pending, error } = await useQuestionList(); const { data: questions, pending, error } = await useQuestionList();
const questionTypeOptions = computed(() => {
const types: QuestionTypeView[] = [];
questions.value.forEach((q: QuestionListView) => {
if (!types.some((t) => t.id === q.type.id)) {
if (q.type.id === '-1') {
types.push({
id: '-1',
name: $t('product-filter.misc'),
});
} else {
types.push(q.type);
}
}
});
return types;
});
const productTypeOptions = computed(() => { const productTypeOptions = computed(() => {
const types: QuestionListProductType[] = []; const types: QuestionListProductType[] = [];
questions.value.forEach((q: QuestionListView) => { questions.value.forEach((q: QuestionListView) => {
@ -71,12 +90,12 @@
}); });
const productOptions = computed(() => { const productOptions = computed(() => {
if (!filters.selectedType) return []; if (!filters.selectedProductType) return [];
const products: QuestionListProduct[] = []; const products: QuestionListProduct[] = [];
questions.value.forEach((q: QuestionListView) => { questions.value.forEach((q: QuestionListView) => {
q.products.forEach((product: QuestionListProduct) => { q.products.forEach((product: QuestionListProduct) => {
if ( if (
product.type.id === filters.selectedType && product.type.id === filters.selectedProductType &&
!products.some((p) => p.id === product.id) !products.some((p) => p.id === product.id)
) { ) {
products.push(product); products.push(product);
@ -98,14 +117,18 @@
(product: QuestionListProduct) => (product: QuestionListProduct) =>
product.id === filters.selectedProduct product.id === filters.selectedProduct
) )
: filters.selectedType : filters.selectedProductType
? question.products?.some( ? question.products?.some(
(product: QuestionListProduct) => (product: QuestionListProduct) =>
product.type.id === filters.selectedType product.type.id === filters.selectedProductType
) )
: true; : true;
return matchProduct; const matchQuestionType = filters.selectedQuestionType
? question.type.id === filters.selectedQuestionType
: true;
return matchProduct && matchQuestionType;
}); });
}); });
@ -138,7 +161,7 @@
); );
watch( watch(
() => filters.selectedType, () => filters.selectedProductType,
() => { () => {
filters.selectedProduct = null; filters.selectedProduct = null;
} }

View File

@ -79,7 +79,12 @@
"keyword": "Keyword", "keyword": "Keyword",
"select-product-type": "Select product type", "select-product-type": "Select product type",
"select-product-model": "Select product model", "select-product-model": "Select product model",
"enter-keyword": "Enter keyword" "enter-keyword": "Enter keyword",
"question-type": "Question Type",
"select-question-type": "Select question type",
"document-type": "Document Type",
"select-document-type": "Select document type",
"misc": "Misc"
}, },
"document-meta": { "document-meta": {
"size": "Size", "size": "Size",

View File

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

View File

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

View File

@ -78,7 +78,12 @@
"keyword": "关键词", "keyword": "关键词",
"select-product-type": "选择产品类型", "select-product-type": "选择产品类型",
"select-product-model": "选择产品系列", "select-product-model": "选择产品系列",
"enter-keyword": "输入关键词" "enter-keyword": "输入关键词",
"question-type": "问题类型",
"select-question-type": "选择问题类型",
"document-type": "文档类型",
"select-document-type": "选择文档类型",
"misc": "其他"
}, },
"document-meta": { "document-meta": {
"size": "大小", "size": "大小",

View File

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

View File

@ -1,6 +1,13 @@
query GetQuestionList($locale: String!) { query GetQuestionList($locale: String!) {
questions(filter: { status: { _eq: "published" } }) { questions(filter: { status: { _eq: "published" } }) {
id id
type {
id
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
name
}
}
translations(filter: { languages_code: { code: { _eq: $locale } } }) { translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id id
title title

View File

@ -1,6 +1,51 @@
import { describe, test, expect } from 'vitest'; 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 * 单元测试: toProductDocumentView
*/ */
@ -65,6 +110,15 @@ describe('toProductDocumentView', () => {
describe('toDocumentListView', () => { describe('toDocumentListView', () => {
const baseData: ProductDocument = { const baseData: ProductDocument = {
id: 1, id: 1,
type: {
id: 1,
translations: [
{
id: 1,
name: 'Type A',
},
],
},
file: { file: {
id: 'rand-om__-uuid-1234', id: 'rand-om__-uuid-1234',
filename_download: 'document.pdf', filename_download: 'document.pdf',
@ -94,6 +148,10 @@ describe('toDocumentListView', () => {
title: 'Document Title', title: 'Document Title',
url: '/api/assets/rand-om__-uuid-1234', url: '/api/assets/rand-om__-uuid-1234',
size: 2048, size: 2048,
type: {
id: '1',
name: 'Type A',
},
products: [ products: [
{ {
id: '1', id: '1',

View File

@ -1,5 +1,31 @@
import { isObject } from '../../server/utils/object'; 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 视图模型 * 将 Directus 返回的 Document 数据转换为 ProductDocumentView 视图模型
* *
@ -40,6 +66,8 @@ export function toProductDocumentView(
export function toDocumentListView(raw: ProductDocument): DocumentListView { export function toDocumentListView(raw: ProductDocument): DocumentListView {
const trans = raw.translations?.[0]; const trans = raw.translations?.[0];
const type = toDocumentTypeView(raw.type ?? null);
const file = isObject<DirectusFile>(raw.file) ? raw.file : undefined; const file = isObject<DirectusFile>(raw.file) ? raw.file : undefined;
const fileId = file?.id ?? ''; const fileId = file?.id ?? '';
@ -73,6 +101,7 @@ export function toDocumentListView(raw: ProductDocument): DocumentListView {
title: trans?.title ?? '', title: trans?.title ?? '',
url: url, url: url,
size: file?.filesize ?? 0, size: file?.filesize ?? 0,
type: type,
products: related_products, products: related_products,
}; };
} }

View File

@ -1,5 +1,51 @@
import { describe, expect, test } from 'vitest'; import { describe, expect, test } from 'vitest';
import { toProductQuestionView, toQuestionListView } from './questionMapper'; import {
toProductQuestionView,
toQuestionListView,
toQuestionTypeView,
} from './questionMapper';
/**
* 单元测试: toQuestionTypeView
*/
describe('toQuestionTypeView', () => {
const baseData: QuestionType = {
id: 1,
translations: [{ id: 1, name: 'Type Name' }],
};
test('convert raw data to QuestionTypeView correctly', () => {
const rawData: QuestionType = {
...baseData,
};
expect(toQuestionTypeView(rawData)).toEqual({
id: '1',
name: 'Type Name',
});
});
test('convert raw data with missing translations', () => {
const rawData: QuestionType = {
...baseData,
translations: [],
};
expect(toQuestionTypeView(rawData)).toEqual({
id: '1',
name: '',
});
});
test('convert null input to default QuestionTypeView', () => {
const rawData: QuestionType | null = null;
expect(toQuestionTypeView(rawData)).toEqual({
id: '-1',
name: '',
});
});
});
/** /**
* 单元测试: toProductQuestionView * 单元测试: toProductQuestionView
@ -43,6 +89,10 @@ describe('toProductQuestionView', () => {
describe('toQuestionListView', () => { describe('toQuestionListView', () => {
const baseData: Question = { const baseData: Question = {
id: 1, id: 1,
type: {
id: 1,
translations: [{ id: 1, name: 'Type Name' }],
},
translations: [ translations: [
{ id: 1, title: 'Question Title', content: 'Question Answer' }, { id: 1, title: 'Question Title', content: 'Question Answer' },
], ],
@ -68,6 +118,10 @@ describe('toQuestionListView', () => {
expect(toQuestionListView(rawData)).toEqual({ expect(toQuestionListView(rawData)).toEqual({
id: '1', id: '1',
type: {
id: '1',
name: 'Type Name',
},
title: 'Question Title', title: 'Question Title',
content: 'Question Answer', content: 'Question Answer',
products: [ products: [
@ -104,6 +158,10 @@ describe('toQuestionListView', () => {
expect(toQuestionListView(rawData)).toEqual({ expect(toQuestionListView(rawData)).toEqual({
id: '1', id: '1',
type: {
id: '1',
name: 'Type Name',
},
title: '', title: '',
content: '', content: '',
products: [ products: [

View File

@ -1,5 +1,31 @@
import { isObject } from '../../server/utils/object'; import { isObject } from '../../server/utils/object';
/**
* 将 Directus 返回的 QuestionType 类型转换为 QuestionTypeView 视图模型
*
* @param raw: 原始的 QuestionType 数据
* @returns 转换后的 QuestionTypeView 对象
*
* @example
* const view = toQuestionTypeView(rawQuestionType);
*/
export function toQuestionTypeView(
raw: QuestionType | string | null
): QuestionTypeView {
if (typeof raw === 'string' || raw === null) {
return {
id: '-1',
name: '',
} satisfies QuestionTypeView;
}
const trans = raw.translations?.[0];
return {
id: raw.id.toString(),
name: trans?.name ?? '',
};
}
/** /**
* 将 Directus 返回的 Question 数据转换为 ProductQuestionView 视图模型 * 将 Directus 返回的 Question 数据转换为 ProductQuestionView 视图模型
* *
@ -31,6 +57,8 @@ export function toProductQuestionView(raw: Question): ProductQuestionView {
export function toQuestionListView(raw: Question): QuestionListView { export function toQuestionListView(raw: Question): QuestionListView {
const trans = raw.translations?.[0]; const trans = raw.translations?.[0];
const type = toQuestionTypeView(raw.type ?? null);
const related_products: QuestionListProduct[] = (raw.products ?? []) const related_products: QuestionListProduct[] = (raw.products ?? [])
.filter(isObject<ProductsQuestion>) .filter(isObject<ProductsQuestion>)
.map((item) => item.products_id) .map((item) => item.products_id)
@ -57,6 +85,7 @@ export function toQuestionListView(raw: Question): QuestionListView {
return { return {
id: raw.id.toString(), id: raw.id.toString(),
type: type,
title: trans?.title ?? '', title: trans?.title ?? '',
content: trans?.content ?? '', content: trans?.content ?? '',
products: related_products, products: related_products,

View File

@ -1,3 +1,19 @@
export interface AiPrompt {
/** @primaryKey */
id: string;
sort?: number | null;
date_created?: string | null;
user_created?: DirectusUser | string | null;
date_updated?: string | null;
user_updated?: DirectusUser | string | null;
/** @required */
name: string;
status?: 'published' | 'draft' | 'archived' | null;
description?: string | null;
system_prompt?: string | null;
messages?: Array<{ role: 'user' | 'assistant'; text: string }> | null;
}
export interface CompanyProfile { export interface CompanyProfile {
/** @primaryKey */ /** @primaryKey */
id: number; id: number;
@ -26,6 +42,20 @@ export interface ContactInfoTranslation {
content?: string | null; content?: string | null;
} }
export interface DocumentType {
/** @primaryKey */
id: number;
translations?: DocumentTypesTranslation[] | null;
}
export interface DocumentTypesTranslation {
/** @primaryKey */
id: number;
document_types_id?: DocumentType | string | null;
languages_code?: Language | string | null;
name?: string | null;
}
export interface Homepage { export interface Homepage {
/** @primaryKey */ /** @primaryKey */
id: number; id: number;
@ -75,8 +105,9 @@ export interface ProductDocument {
id: number; id: number;
status?: 'published' | 'draft' | 'archived'; status?: 'published' | 'draft' | 'archived';
file?: DirectusFile | string | null; file?: DirectusFile | string | null;
products?: ProductsProductDocument[] | string[]; type?: DocumentType | string | null;
translations?: ProductDocumentsTranslation[] | null; translations?: ProductDocumentsTranslation[] | null;
products?: ProductsProductDocument[] | string[];
} }
export interface ProductDocumentsTranslation { export interface ProductDocumentsTranslation {
@ -209,10 +240,25 @@ export interface ProductsTranslation {
description?: string | null; description?: string | null;
} }
export interface QuestionType {
/** @primaryKey */
id: number;
translations?: QuestionTypesTranslation[] | null;
}
export interface QuestionTypesTranslation {
/** @primaryKey */
id: number;
question_types_id?: QuestionType | string | null;
languages_code?: Language | string | null;
name?: string | null;
}
export interface Question { export interface Question {
/** @primaryKey */ /** @primaryKey */
id: number; id: number;
status?: 'published' | 'draft' | 'archived'; status?: 'published' | 'draft' | 'archived';
type?: QuestionType | string | null;
/** @description i18n字段 */ /** @description i18n字段 */
translations?: QuestionsTranslation[] | null; translations?: QuestionsTranslation[] | null;
products?: ProductsQuestion[] | string[]; products?: ProductsQuestion[] | string[];
@ -741,10 +787,13 @@ export interface DirectusExtension {
} }
export interface Schema { export interface Schema {
ai_prompts: AiPrompt[];
company_profile: CompanyProfile; company_profile: CompanyProfile;
company_profile_translations: CompanyProfileTranslation[]; company_profile_translations: CompanyProfileTranslation[];
contact_info: ContactInfo; contact_info: ContactInfo;
contact_info_translations: ContactInfoTranslation[]; contact_info_translations: ContactInfoTranslation[];
document_types: DocumentType[];
document_types_translations: DocumentTypesTranslation[];
homepage: Homepage; homepage: Homepage;
homepage_files: HomepageFile[]; homepage_files: HomepageFile[];
languages: Language[]; languages: Language[];
@ -765,6 +814,8 @@ export interface Schema {
products_product_images: ProductsProductImage[]; products_product_images: ProductsProductImage[];
products_questions: ProductsQuestion[]; products_questions: ProductsQuestion[];
products_translations: ProductsTranslation[]; products_translations: ProductsTranslation[];
question_types: QuestionType[];
question_types_translations: QuestionTypesTranslation[];
questions: Question[]; questions: Question[];
questions_translations: QuestionsTranslation[]; questions_translations: QuestionsTranslation[];
solution_types: SolutionType[]; solution_types: SolutionType[];
@ -801,10 +852,13 @@ export interface Schema {
} }
export enum CollectionNames { export enum CollectionNames {
ai_prompts = 'ai_prompts',
company_profile = 'company_profile', company_profile = 'company_profile',
company_profile_translations = 'company_profile_translations', company_profile_translations = 'company_profile_translations',
contact_info = 'contact_info', contact_info = 'contact_info',
contact_info_translations = 'contact_info_translations', contact_info_translations = 'contact_info_translations',
document_types = 'document_types',
document_types_translations = 'document_types_translations',
homepage = 'homepage', homepage = 'homepage',
homepage_files = 'homepage_files', homepage_files = 'homepage_files',
languages = 'languages', languages = 'languages',
@ -825,6 +879,8 @@ export enum CollectionNames {
products_product_images = 'products_product_images', products_product_images = 'products_product_images',
products_questions = 'products_questions', products_questions = 'products_questions',
products_translations = 'products_translations', products_translations = 'products_translations',
question_types = 'question_types',
question_types_translations = 'question_types_translations',
questions = 'questions', questions = 'questions',
questions_translations = 'questions_translations', questions_translations = 'questions_translations',
solution_types = 'solution_types', solution_types = 'solution_types',

View File

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

View File

@ -7,6 +7,18 @@ export interface QuestionListProductType {
name: string; name: string;
} }
/**
* 问题类型
* 用于在常见问题列表中提供产品筛选功能
*/
export interface QuestionTypeView {
/** 唯一标识符 **/
id: string;
/** 类型名 **/
name: string;
}
/** /**
* 问题关联产品模型 * 问题关联产品模型
* 用于在常见问题列表中提供产品筛选功能 * 用于在常见问题列表中提供产品筛选功能
@ -25,6 +37,9 @@ export interface QuestionListView {
/** 唯一标识符 **/ /** 唯一标识符 **/
id: string; id: string;
/** 问题类型 **/
type: QuestionTypeView;
/** 问题标题 **/ /** 问题标题 **/
title: string; title: string;