From 7e7775ccc6461a1032a4eec6cd160dcdadd21d37 Mon Sep 17 00:00:00 2001 From: R2m1liA <15258427350@163.com> Date: Tue, 11 Nov 2025 15:52:27 +0800 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=E4=B8=BADirectus=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E6=8F=90=E4=BE=9BGraphQL=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在Nuxt中引入GraphQL加载模块 - 为Directus引入GraphQl, Query --- .graphqlrc.yaml | 5 +++++ app/plugins/directus.ts | 3 ++- nuxt.config.ts | 3 +++ package.json | 2 ++ pnpm-lock.yaml | 32 ++++++++++++++++++++++++++++++++ shared/types/graphql.d.ts | 9 +++++++++ 6 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .graphqlrc.yaml create mode 100644 shared/types/graphql.d.ts diff --git a/.graphqlrc.yaml b/.graphqlrc.yaml new file mode 100644 index 0000000..47690e9 --- /dev/null +++ b/.graphqlrc.yaml @@ -0,0 +1,5 @@ +schema: + - 'http://192.168.86.5:8055/graphql': + headers: + Authorization: 'Bearer ixSWeViHIqwj6_r7NM-uZVR3NNOyBa_W' +documents: 'app/graphql/**/*.{graphql,js,ts,jsx,tsx}' diff --git a/app/plugins/directus.ts b/app/plugins/directus.ts index a68bc1b..5d82cee 100644 --- a/app/plugins/directus.ts +++ b/app/plugins/directus.ts @@ -1,10 +1,11 @@ -import { createDirectus, rest, staticToken } from '@directus/sdk'; +import { createDirectus, rest, staticToken, graphql } from '@directus/sdk'; export default defineNuxtPlugin(() => { const config = useRuntimeConfig(); const directus = createDirectus(config.public.directus.url) .with(rest()) + .with(graphql()) .with(staticToken(config.public.directus.token || '')); return { provide: { directus }, diff --git a/nuxt.config.ts b/nuxt.config.ts index d9ca413..74921a3 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -1,4 +1,6 @@ // https://nuxt.com/docs/api/configuration/nuxt-config +import GraphQLLoader from 'vite-plugin-graphql-loader'; + export default defineNuxtConfig({ compatibilityDate: '2025-07-15', devtools: { enabled: true }, @@ -90,6 +92,7 @@ export default defineNuxtConfig({ }, }, }, + plugins: [GraphQLLoader()], }, devServer: { diff --git a/package.json b/package.json index 00f7eb4..267ae90 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,14 @@ "dompurify": "^3.2.6", "element-plus": "^2.10.7", "fuse.js": "^7.1.0", + "graphql": "^16.12.0", "markdown-it": "^14.1.0", "meilisearch": "^0.53.0", "nuxt": "^4.0.3", "nuxt-directus": "5.7.0", "sass": "^1.90.0", "sharp": "^0.34.3", + "vite-plugin-graphql-loader": "^4.0.4", "vue": "^3.5.18", "vue-router": "^4.5.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eae14c2..7067bfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: fuse.js: specifier: ^7.1.0 version: 7.1.0 + graphql: + specifier: ^16.12.0 + version: 16.12.0 markdown-it: specifier: ^14.1.0 version: 14.1.0 @@ -68,6 +71,9 @@ importers: sharp: specifier: ^0.34.3 version: 0.34.3 + vite-plugin-graphql-loader: + specifier: ^4.0.4 + version: 4.0.4 vue: specifier: ^3.5.18 version: 3.5.21(typescript@5.9.2) @@ -3498,6 +3504,16 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -5388,6 +5404,9 @@ packages: vue-tsc: optional: true + vite-plugin-graphql-loader@4.0.4: + resolution: {integrity: sha512-lYnpQ2luV2fcuXmOJADljuktfMbDW00Y+6QS+Ek8Jz1Vdzlj/51LSGJwZqyjJ24a5YQ+o29Hr6el/5+nlZetvg==} + vite-plugin-inspect@11.3.3: resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} engines: {node: '>=14'} @@ -9479,6 +9498,13 @@ snapshots: graphemer@1.4.0: {} + graphql-tag@2.12.6(graphql@16.12.0): + dependencies: + graphql: 16.12.0 + tslib: 2.8.1 + + graphql@16.12.0: {} + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -11725,6 +11751,12 @@ snapshots: optionator: 0.9.4 typescript: 5.9.2 + vite-plugin-graphql-loader@4.0.4: + dependencies: + graphql: 16.12.0 + graphql-tag: 2.12.6(graphql@16.12.0) + magic-string: 0.30.19 + vite-plugin-inspect@11.3.3(@nuxt/kit@3.19.2(magicast@0.3.5))(vite@7.1.5(@types/node@24.4.0)(jiti@2.5.1)(sass@1.92.1)(terser@5.44.0)(yaml@2.8.1)): dependencies: ansis: 4.1.0 diff --git a/shared/types/graphql.d.ts b/shared/types/graphql.d.ts new file mode 100644 index 0000000..909ad9a --- /dev/null +++ b/shared/types/graphql.d.ts @@ -0,0 +1,9 @@ +declare module '*.graphql' { + const Query: import('graphql').DocumentNode; + export default Query; + export const _queries: Record; + export const _fragments: Record< + string, + import('graphql').FragmentDefinitionNode + >; +} From 691dd34127e0ffaa1c1db65f3f92a9e3d6b2f305 Mon Sep 17 00:00:00 2001 From: R2m1liA <15258427350@163.com> Date: Tue, 11 Nov 2025 15:58:15 +0800 Subject: [PATCH 02/13] =?UTF-8?q?refactor:=20=E4=BA=A7=E5=93=81=E9=A1=B5?= =?UTF-8?q?=E4=B8=8E=E4=BA=A7=E5=93=81=E5=88=97=E8=A1=A8=E7=9A=84API?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将产品页与产品列表的API由REST重构为GraphQL - 修改Mapper与单元测试 --- app/composables/directus/useProduct.ts | 206 +++++++++--------- app/composables/directus/useProductList.ts | 39 +--- app/graphql/product.graphql | 66 ++++++ app/graphql/productList.graphql | 22 ++ app/models/mappers/productMapper.test.ts | 8 +- app/models/mappers/productMapper.ts | 7 +- .../products/{[...slug].vue => [slug].vue} | 15 +- app/pages/products/index.vue | 7 +- 8 files changed, 225 insertions(+), 145 deletions(-) create mode 100644 app/graphql/product.graphql create mode 100644 app/graphql/productList.graphql rename app/pages/products/{[...slug].vue => [slug].vue} (86%) diff --git a/app/composables/directus/useProduct.ts b/app/composables/directus/useProduct.ts index 67c18ed..6ac5c1e 100644 --- a/app/composables/directus/useProduct.ts +++ b/app/composables/directus/useProduct.ts @@ -1,4 +1,5 @@ -import { readItem } from '@directus/sdk'; +import GetProduct from '@/graphql/product.graphql'; +import { print } from 'graphql'; export const useProduct = (id: string) => { const { $directus } = useNuxtApp(); @@ -7,104 +8,111 @@ export const useProduct = (id: string) => { const locale = getDirectusLocale(); return useAsyncData(`product-${id}-${locale}`, async () => { - 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 }, - }, - }, - }, - }, - }, - }) + return await $directus.query<{ products_by_id: Product }>( + print(GetProduct), + { + id: id, + locale: 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 }, + // }, + // }, + // }, + // }, + // }, + // }) + // ); }); }; diff --git a/app/composables/directus/useProductList.ts b/app/composables/directus/useProductList.ts index 989c88d..d13739c 100644 --- a/app/composables/directus/useProductList.ts +++ b/app/composables/directus/useProductList.ts @@ -1,4 +1,5 @@ -import { readItems } from '@directus/sdk'; +import GetProductList from '@/graphql/productList.graphql'; +import { print } from 'graphql'; export const useProductList = () => { const { $directus } = useNuxtApp(); @@ -7,37 +8,11 @@ export const useProductList = () => { const locale = getDirectusLocale(); return useAsyncData(`product-list-${locale}`, async () => { - return await $directus.request( - readItems('products', { - fields: [ - 'id', - { translations: ['id', 'name', 'summary'] }, - 'cover', - { - product_type: [ - 'id', - { - translations: ['id', 'name'], - }, - 'sort', - ], - }, - ], - deep: { - translations: { - _filter: { - languages_code: { _eq: locale }, - }, - }, - product_type: { - translations: { - _filter: { - languages_code: { _eq: locale }, - }, - }, - }, - }, - }) + return await $directus.query<{ products: Product[] }>( + print(GetProductList), + { + locale: locale, + } ); }); }; diff --git a/app/graphql/product.graphql b/app/graphql/product.graphql new file mode 100644 index 0000000..5965746 --- /dev/null +++ b/app/graphql/product.graphql @@ -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 + } + } + } + } +} + diff --git a/app/graphql/productList.graphql b/app/graphql/productList.graphql new file mode 100644 index 0000000..788d12a --- /dev/null +++ b/app/graphql/productList.graphql @@ -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 + } + } +} diff --git a/app/models/mappers/productMapper.test.ts b/app/models/mappers/productMapper.test.ts index 1e996f0..ad9a837 100644 --- a/app/models/mappers/productMapper.test.ts +++ b/app/models/mappers/productMapper.test.ts @@ -10,7 +10,9 @@ describe('toProductListView', () => { translations: [ { id: 10, name: 'Product Name', summary: 'Product Summary' }, ], - cover: 'cover-file-uuid-1234', + cover: { + id: 'cover-file-uuid-1234', + }, product_type: { id: 1, translations: [{ id: 20, name: 'Type Name' }], @@ -35,7 +37,9 @@ describe('toProductListView', () => { const rawData: Product = { id: 1, translations: [], - cover: 'cover-file-uuid-1234', + cover: { + id: 'cover-file-uuid-1234', + }, product_type: { id: 20, translations: [], diff --git a/app/models/mappers/productMapper.ts b/app/models/mappers/productMapper.ts index 724faff..9c61a5e 100644 --- a/app/models/mappers/productMapper.ts +++ b/app/models/mappers/productMapper.ts @@ -33,12 +33,14 @@ export function toProductListView(raw: Product): ProductListView { ? toProductTypeView(raw.product_type) : undefined; + const cover = isObject(raw.cover) ? raw.cover.id : ''; + return { id: raw.id, product_type: type, name: trans.name, 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) .filter(isObject) .map((item) => { + const image = isObject(item.image) ? item.image.id : ''; return { id: item.id, - image: item.image.toString(), + image: image, caption: item.translations?.[0]?.caption || '', }; }); diff --git a/app/pages/products/[...slug].vue b/app/pages/products/[slug].vue similarity index 86% rename from app/pages/products/[...slug].vue rename to app/pages/products/[slug].vue index cca08a3..6eee50a 100644 --- a/app/pages/products/[...slug].vue +++ b/app/pages/products/[slug].vue @@ -35,11 +35,12 @@ 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(() => { if (rawProduct.value === null) { return null; @@ -60,10 +61,10 @@ }); // SEO - usePageSeo({ - title: product.value.name || $t('page-title.products'), - description: product.value.summary || '', - }); + // usePageSeo({ + // title: product.value.name || $t('page-title.products'), + // description: product.value.summary || '', + // });