diff --git a/app/components/pages/support/DocumentFilter.vue b/app/components/pages/support/DocumentFilter.vue new file mode 100644 index 0000000..82c8fad --- /dev/null +++ b/app/components/pages/support/DocumentFilter.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/app/components/shared/ProductFilter.vue b/app/components/pages/support/QuestionFilter.vue similarity index 52% rename from app/components/shared/ProductFilter.vue rename to app/components/pages/support/QuestionFilter.vue index c59c76d..02634e2 100644 --- a/app/components/shared/ProductFilter.vue +++ b/app/components/pages/support/QuestionFilter.vue @@ -1,12 +1,12 @@ @@ -101,19 +73,24 @@ defineProps({ productTypeOptions: { - type: Array as () => Array<{ id: string; name: string }>, + type: Array as () => Array, default: () => [], }, productOptions: { - type: Array as () => Array<{ id: string; name: string }>, + type: Array as () => Array, + default: () => [], + }, + questionTypeOptions: { + type: Array as () => Array, default: () => [], }, }); const model = defineModel<{ - keyword: string; - selectedType: string | null; selectedProduct: string | null; + selectedProductType: string | null; + selectedQuestionType: string | null; + keyword: string; }>(); @@ -132,18 +109,4 @@ height: 40px; 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; - } - } diff --git a/app/pages/support/documents.vue b/app/pages/support/documents.vue index c03e8ea..8c0db6f 100644 --- a/app/pages/support/documents.vue +++ b/app/pages/support/documents.vue @@ -10,13 +10,15 @@
- - + + { + 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; } diff --git a/app/pages/support/faq.vue b/app/pages/support/faq.vue index df53b44..e403f68 100644 --- a/app/pages/support/faq.vue +++ b/app/pages/support/faq.vue @@ -11,10 +11,11 @@
- @@ -37,8 +38,9 @@ const route = useRoute(); const filters = reactive({ - selectedType: null as string | null, + selectedQuestionType: null as string | null, selectedProduct: null as string | null, + selectedProductType: null as string | null, keyword: '', }); @@ -57,6 +59,23 @@ 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 types: QuestionListProductType[] = []; questions.value.forEach((q: QuestionListView) => { @@ -71,12 +90,12 @@ }); const productOptions = computed(() => { - if (!filters.selectedType) return []; + if (!filters.selectedProductType) return []; const products: QuestionListProduct[] = []; questions.value.forEach((q: QuestionListView) => { q.products.forEach((product: QuestionListProduct) => { if ( - product.type.id === filters.selectedType && + product.type.id === filters.selectedProductType && !products.some((p) => p.id === product.id) ) { products.push(product); @@ -98,14 +117,18 @@ (product: QuestionListProduct) => product.id === filters.selectedProduct ) - : filters.selectedType + : filters.selectedProductType ? question.products?.some( (product: QuestionListProduct) => - product.type.id === filters.selectedType + product.type.id === filters.selectedProductType ) : true; - return matchProduct; + const matchQuestionType = filters.selectedQuestionType + ? question.type.id === filters.selectedQuestionType + : true; + + return matchProduct && matchQuestionType; }); }); @@ -138,7 +161,7 @@ ); watch( - () => filters.selectedType, + () => filters.selectedProductType, () => { filters.selectedProduct = null; } diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 1112bc2..ad02c64 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -79,7 +79,12 @@ "keyword": "Keyword", "select-product-type": "Select product type", "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": { "size": "Size", diff --git a/i18n/locales/es.json b/i18n/locales/es.json index adea164..64c5316 100644 --- a/i18n/locales/es.json +++ b/i18n/locales/es.json @@ -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", diff --git a/i18n/locales/ru.json b/i18n/locales/ru.json index 22462c9..33d549f 100644 --- a/i18n/locales/ru.json +++ b/i18n/locales/ru.json @@ -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": "Размер", diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index 50f27ce..f07808a 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -78,7 +78,12 @@ "keyword": "关键词", "select-product-type": "选择产品类型", "select-product-model": "选择产品系列", - "enter-keyword": "输入关键词" + "enter-keyword": "输入关键词", + "question-type": "问题类型", + "select-question-type": "选择问题类型", + "document-type": "文档类型", + "select-document-type": "选择文档类型", + "misc": "其他" }, "document-meta": { "size": "大小", diff --git a/server/assets/graphql/documentList.graphql b/server/assets/graphql/documentList.graphql index 194fea6..3a64458 100644 --- a/server/assets/graphql/documentList.graphql +++ b/server/assets/graphql/documentList.graphql @@ -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 { diff --git a/server/assets/graphql/questionList.graphql b/server/assets/graphql/questionList.graphql index 273ed77..bdad9c7 100644 --- a/server/assets/graphql/questionList.graphql +++ b/server/assets/graphql/questionList.graphql @@ -1,6 +1,13 @@ query GetQuestionList($locale: String!) { questions(filter: { status: { _eq: "published" } }) { id + type { + id + translations(filter: { languages_code: { code: { _eq: $locale } } }) { + id + name + } + } translations(filter: { languages_code: { code: { _eq: $locale } } }) { id title diff --git a/server/mappers/documentMapper.test.ts b/server/mappers/documentMapper.test.ts index 50b1efc..d62e449 100644 --- a/server/mappers/documentMapper.test.ts +++ b/server/mappers/documentMapper.test.ts @@ -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', diff --git a/server/mappers/documentMapper.ts b/server/mappers/documentMapper.ts index 9982dee..42ee54d 100644 --- a/server/mappers/documentMapper.ts +++ b/server/mappers/documentMapper.ts @@ -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(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, }; } diff --git a/server/mappers/questionMapper.test.ts b/server/mappers/questionMapper.test.ts index b6d45e7..a043db1 100644 --- a/server/mappers/questionMapper.test.ts +++ b/server/mappers/questionMapper.test.ts @@ -1,5 +1,51 @@ 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 @@ -43,6 +89,10 @@ describe('toProductQuestionView', () => { describe('toQuestionListView', () => { const baseData: Question = { id: 1, + type: { + id: 1, + translations: [{ id: 1, name: 'Type Name' }], + }, translations: [ { id: 1, title: 'Question Title', content: 'Question Answer' }, ], @@ -68,6 +118,10 @@ describe('toQuestionListView', () => { expect(toQuestionListView(rawData)).toEqual({ id: '1', + type: { + id: '1', + name: 'Type Name', + }, title: 'Question Title', content: 'Question Answer', products: [ @@ -104,6 +158,10 @@ describe('toQuestionListView', () => { expect(toQuestionListView(rawData)).toEqual({ id: '1', + type: { + id: '1', + name: 'Type Name', + }, title: '', content: '', products: [ diff --git a/server/mappers/questionMapper.ts b/server/mappers/questionMapper.ts index f30def6..041c21e 100644 --- a/server/mappers/questionMapper.ts +++ b/server/mappers/questionMapper.ts @@ -1,5 +1,31 @@ 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 视图模型 * @@ -31,6 +57,8 @@ export function toProductQuestionView(raw: Question): ProductQuestionView { export function toQuestionListView(raw: Question): QuestionListView { const trans = raw.translations?.[0]; + const type = toQuestionTypeView(raw.type ?? null); + const related_products: QuestionListProduct[] = (raw.products ?? []) .filter(isObject) .map((item) => item.products_id) @@ -57,6 +85,7 @@ export function toQuestionListView(raw: Question): QuestionListView { return { id: raw.id.toString(), + type: type, title: trans?.title ?? '', content: trans?.content ?? '', products: related_products, diff --git a/shared/types/directus/my-schema.ts b/shared/types/directus/my-schema.ts index ef138fb..2292e8c 100644 --- a/shared/types/directus/my-schema.ts +++ b/shared/types/directus/my-schema.ts @@ -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 { /** @primaryKey */ id: number; @@ -26,6 +42,20 @@ export interface ContactInfoTranslation { 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 { /** @primaryKey */ id: number; @@ -75,8 +105,9 @@ export interface ProductDocument { id: number; status?: 'published' | 'draft' | 'archived'; file?: DirectusFile | string | null; - products?: ProductsProductDocument[] | string[]; + type?: DocumentType | string | null; translations?: ProductDocumentsTranslation[] | null; + products?: ProductsProductDocument[] | string[]; } export interface ProductDocumentsTranslation { @@ -209,10 +240,25 @@ export interface ProductsTranslation { 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 { /** @primaryKey */ id: number; status?: 'published' | 'draft' | 'archived'; + type?: QuestionType | string | null; /** @description i18n字段 */ translations?: QuestionsTranslation[] | null; products?: ProductsQuestion[] | string[]; @@ -741,10 +787,13 @@ export interface DirectusExtension { } export interface Schema { + ai_prompts: AiPrompt[]; company_profile: CompanyProfile; company_profile_translations: CompanyProfileTranslation[]; contact_info: ContactInfo; contact_info_translations: ContactInfoTranslation[]; + document_types: DocumentType[]; + document_types_translations: DocumentTypesTranslation[]; homepage: Homepage; homepage_files: HomepageFile[]; languages: Language[]; @@ -765,6 +814,8 @@ export interface Schema { products_product_images: ProductsProductImage[]; products_questions: ProductsQuestion[]; products_translations: ProductsTranslation[]; + question_types: QuestionType[]; + question_types_translations: QuestionTypesTranslation[]; questions: Question[]; questions_translations: QuestionsTranslation[]; solution_types: SolutionType[]; @@ -801,10 +852,13 @@ export interface Schema { } export enum CollectionNames { + ai_prompts = 'ai_prompts', company_profile = 'company_profile', company_profile_translations = 'company_profile_translations', contact_info = 'contact_info', contact_info_translations = 'contact_info_translations', + document_types = 'document_types', + document_types_translations = 'document_types_translations', homepage = 'homepage', homepage_files = 'homepage_files', languages = 'languages', @@ -825,6 +879,8 @@ export enum CollectionNames { products_product_images = 'products_product_images', products_questions = 'products_questions', products_translations = 'products_translations', + question_types = 'question_types', + question_types_translations = 'question_types_translations', questions = 'questions', questions_translations = 'questions_translations', solution_types = 'solution_types', diff --git a/shared/types/views/document-list-view.ts b/shared/types/views/document-list-view.ts index a3cdefe..bab3047 100644 --- a/shared/types/views/document-list-view.ts +++ b/shared/types/views/document-list-view.ts @@ -1,3 +1,14 @@ +/** + * 文档类型视图模型 + * 用于在文档库中提供类型筛选功能 + */ +export interface DocumentTypeView { + /** 唯一标识符 **/ + id: string; + /** 类型名 **/ + name: string; +} + /** * 文档关联产品类型模型 * 用于在文档库中提供产品筛选功能 @@ -40,6 +51,9 @@ export interface DocumentListView { /** 文档链接 **/ url: string; + /** 文档类型 **/ + type: DocumentTypeView; + /** 相关产品 **/ products: DocumentListProduct[]; } diff --git a/shared/types/views/question-list-view.ts b/shared/types/views/question-list-view.ts index 4ea843c..362fd93 100644 --- a/shared/types/views/question-list-view.ts +++ b/shared/types/views/question-list-view.ts @@ -7,6 +7,18 @@ export interface QuestionListProductType { name: string; } +/** + * 问题类型 + * 用于在常见问题列表中提供产品筛选功能 + */ +export interface QuestionTypeView { + /** 唯一标识符 **/ + id: string; + + /** 类型名 **/ + name: string; +} + /** * 问题关联产品模型 * 用于在常见问题列表中提供产品筛选功能 @@ -25,6 +37,9 @@ export interface QuestionListView { /** 唯一标识符 **/ id: string; + /** 问题类型 **/ + type: QuestionTypeView; + /** 问题标题 **/ title: string;