refactor: 产品页与产品列表的API重构

- 将产品页与产品列表的API由REST重构为GraphQL
- 修改Mapper与单元测试
This commit is contained in:
2025-11-11 15:58:15 +08:00
parent 7e7775ccc6
commit 691dd34127
8 changed files with 225 additions and 145 deletions

View File

@ -1,4 +1,5 @@
import { readItem } from '@directus/sdk'; import GetProduct from '@/graphql/product.graphql';
import { print } from 'graphql';
export const useProduct = (id: string) => { export const useProduct = (id: string) => {
const { $directus } = useNuxtApp(); const { $directus } = useNuxtApp();
@ -7,104 +8,111 @@ export const useProduct = (id: string) => {
const locale = getDirectusLocale(); const locale = getDirectusLocale();
return useAsyncData(`product-${id}-${locale}`, async () => { return useAsyncData(`product-${id}-${locale}`, async () => {
return await $directus.request( return await $directus.query<{ products_by_id: Product }>(
readItem('products', id, { print(GetProduct),
fields: [
'id',
{ translations: ['id', 'name', 'summary', 'description'] },
{ {
images: [ id: id,
'id', locale: locale,
{ }
product_images_id: [
'id',
'image',
{ translations: ['id', 'caption'] },
],
},
],
},
{
specs: [
'id',
{
translations: ['*'],
},
{
specs: [
'id',
{
translations: ['id', 'key', 'value'],
},
],
},
],
},
{
faqs: [
'id',
{
questions_id: [
'id',
{
translations: ['id', 'title', 'content'],
},
],
},
],
},
{
documents: [
'id',
{
product_documents_id: [
'id',
{
file: ['id', 'filesize', 'filename_download'],
},
{
translations: ['id', 'title'],
},
],
},
],
},
],
deep: {
translations: {
_filter: {
languages_code: { _eq: locale },
},
},
images: {
product_images_id: {
translations: {
_filter: {
languages_code: { _eq: locale },
},
},
},
},
faqs: {
questions_id: {
translations: {
_filter: {
languages_code: { _eq: locale },
},
},
},
},
documents: {
documents_id: {
translations: {
_filter: {
languages_code: { _eq: locale },
},
},
},
},
},
})
); );
// return await $directus.request(
// readItem('products', id, {
// fields: [
// 'id',
// { translations: ['id', 'name', 'summary', 'description'] },
// {
// images: [
// 'id',
// {
// product_images_id: [
// 'id',
// 'image',
// { translations: ['id', 'caption'] },
// ],
// },
// ],
// },
// {
// specs: [
// 'id',
// {
// translations: ['*'],
// },
// {
// specs: [
// 'id',
// {
// translations: ['id', 'key', 'value'],
// },
// ],
// },
// ],
// },
// {
// faqs: [
// 'id',
// {
// questions_id: [
// 'id',
// {
// translations: ['id', 'title', 'content'],
// },
// ],
// },
// ],
// },
// {
// documents: [
// 'id',
// {
// product_documents_id: [
// 'id',
// {
// file: ['id', 'filesize', 'filename_download'],
// },
// {
// translations: ['id', 'title'],
// },
// ],
// },
// ],
// },
// ],
// deep: {
// translations: {
// _filter: {
// languages_code: { _eq: locale },
// },
// },
// images: {
// product_images_id: {
// translations: {
// _filter: {
// languages_code: { _eq: locale },
// },
// },
// },
// },
// faqs: {
// questions_id: {
// translations: {
// _filter: {
// languages_code: { _eq: locale },
// },
// },
// },
// },
// documents: {
// documents_id: {
// translations: {
// _filter: {
// languages_code: { _eq: locale },
// },
// },
// },
// },
// },
// })
// );
}); });
}; };

View File

@ -1,4 +1,5 @@
import { readItems } from '@directus/sdk'; import GetProductList from '@/graphql/productList.graphql';
import { print } from 'graphql';
export const useProductList = () => { export const useProductList = () => {
const { $directus } = useNuxtApp(); const { $directus } = useNuxtApp();
@ -7,37 +8,11 @@ export const useProductList = () => {
const locale = getDirectusLocale(); const locale = getDirectusLocale();
return useAsyncData(`product-list-${locale}`, async () => { return useAsyncData(`product-list-${locale}`, async () => {
return await $directus.request( return await $directus.query<{ products: Product[] }>(
readItems('products', { print(GetProductList),
fields: [
'id',
{ translations: ['id', 'name', 'summary'] },
'cover',
{ {
product_type: [ locale: locale,
'id', }
{
translations: ['id', 'name'],
},
'sort',
],
},
],
deep: {
translations: {
_filter: {
languages_code: { _eq: locale },
},
},
product_type: {
translations: {
_filter: {
languages_code: { _eq: locale },
},
},
},
},
})
); );
}); });
}; };

View File

@ -0,0 +1,66 @@
query GetProduct($id: ID!, $locale: String!) {
products_by_id(id: $id) {
id
status
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
name
summary
description
}
images {
id
product_images_id {
id
image {
id
}
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
caption
}
}
}
specs {
id
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
name
}
specs {
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
key
value
}
}
}
faqs {
id
questions_id {
id
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
title
content
}
}
}
documents {
id
product_documents_id {
id
file {
id
filesize
filename_download
}
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
title
}
}
}
}
}

View File

@ -0,0 +1,22 @@
query GetProductList($locale: String!) {
products(filter: { status: { _eq: "in-production" } }) {
id
status
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
name
summary
}
cover {
id
}
product_type {
id
translations(filter: { languages_code: { code: { _eq: $locale } } }) {
id
name
}
sort
}
}
}

View File

@ -10,7 +10,9 @@ describe('toProductListView', () => {
translations: [ translations: [
{ id: 10, name: 'Product Name', summary: 'Product Summary' }, { id: 10, name: 'Product Name', summary: 'Product Summary' },
], ],
cover: 'cover-file-uuid-1234', cover: {
id: 'cover-file-uuid-1234',
},
product_type: { product_type: {
id: 1, id: 1,
translations: [{ id: 20, name: 'Type Name' }], translations: [{ id: 20, name: 'Type Name' }],
@ -35,7 +37,9 @@ describe('toProductListView', () => {
const rawData: Product = { const rawData: Product = {
id: 1, id: 1,
translations: [], translations: [],
cover: 'cover-file-uuid-1234', cover: {
id: 'cover-file-uuid-1234',
},
product_type: { product_type: {
id: 20, id: 20,
translations: [], translations: [],

View File

@ -33,12 +33,14 @@ export function toProductListView(raw: Product): ProductListView {
? toProductTypeView(raw.product_type) ? toProductTypeView(raw.product_type)
: undefined; : undefined;
const cover = isObject<DirectusFile>(raw.cover) ? raw.cover.id : '';
return { return {
id: raw.id, id: raw.id,
product_type: type, product_type: type,
name: trans.name, name: trans.name,
summary: trans.summary, summary: trans.summary,
cover: raw.cover.toString(), cover: cover,
}; };
} }
@ -110,9 +112,10 @@ export function toProductView(raw: Product): ProductView {
.map((item) => item.product_images_id) .map((item) => item.product_images_id)
.filter(isObject<ProductImage>) .filter(isObject<ProductImage>)
.map((item) => { .map((item) => {
const image = isObject<DirectusFile>(item.image) ? item.image.id : '';
return { return {
id: item.id, id: item.id,
image: item.image.toString(), image: image,
caption: item.translations?.[0]?.caption || '', caption: item.translations?.[0]?.caption || '',
}; };
}); });

View File

@ -35,11 +35,12 @@
const localePath = useLocalePath(); const localePath = useLocalePath();
// //
const id = computed(() => route.params.slug as string); const id = route.params.slug as string;
const { data, pending, error } = await useProduct(id.value); const { data, pending, error } = await useProduct(id);
const rawProduct = computed(() => data.value.products_by_id ?? null);
const rawProduct = computed(() => data.value ?? null);
const product = computed(() => { const product = computed(() => {
if (rawProduct.value === null) { if (rawProduct.value === null) {
return null; return null;
@ -60,10 +61,10 @@
}); });
// SEO // SEO
usePageSeo({ // usePageSeo({
title: product.value.name || $t('page-title.products'), // title: product.value.name || $t('page-title.products'),
description: product.value.summary || '', // description: product.value.summary || '',
}); // });
</script> </script>
<style scoped> <style scoped>

View File

@ -37,7 +37,7 @@
const localePath = useLocalePath(); const localePath = useLocalePath();
const { getImageUrl } = useDirectusImage(); const { getImageUrl } = useDirectusImage();
const { data, pending, error } = useProductList(); const { data, pending, error } = await useProductList();
const activeNames = ref<string[]>([]); const activeNames = ref<string[]>([]);
@ -46,12 +46,13 @@
{ label: $t('navigation.products') }, { label: $t('navigation.products') },
]; ];
const productsRaw = computed(() => data.value ?? []); const productsRaw = computed(() => data.value.products ?? []);
const products = computed(() => const products = computed(() =>
productsRaw.value.map((item) => toProductListView(item)) productsRaw.value.map((item) => toProductListView(item))
); );
logger.debug('products: ', products.value); logger.debug('产品列表数据: ', products.value);
// 按类型分组 // 按类型分组
const groupedProducts = computed(() => { const groupedProducts = computed(() => {