在part 1中,我们使用Garchi CMS建立了博客的后端。现在让我们继续使用NUXT3。
来构建前端。要开始,请确保系统上安装了最新版本的节点JS。
在您的终端中,执行以下命令来创建新的NUXT 3应用程序。您可以自由地用您喜欢的任何其他名称替换“博客”。
npx nuxi@latest init blog
执行后,您应该看到以下输出:
现在,导航到“博客”文件夹并运行NPM i以安装必要的依赖项。
cd blog
npm i
为了加快这一过程,我将绕过NUXT 3和tailwind的基本面。您可以找到有关与Nuxt 3 here
集成的详细信息接下来,将在我们的nuxt.config.ts文件中的部分1中创建的API键并添加API基本URL,如图像所示。
我们将在app.vue中将nuxtwelcome与nuxtpage交换,并在“页面”目录中创建index.vue。
记住,由于API密钥的计费目的,建议使用GARCHI API服务器端。因此,让我们通过生成一个名为“ blog.get.ts”的文件来创建NUXT 3中的API路由,该文件将映射到 /api /blog get request。该文件应位于“服务器/API”目录中。
在'blog.get.ts'中包含以下代码:
export default defineEventHandler(async (event) => {
const categoryId = 71
const config = useRuntimeConfig()
const response = await $fetch(`${config.API_URL}/products/filter`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.API_KEY}`
},
body: {
categories: [categoryId]
}
})
return response
})
让我们解剖代码:
- 我们将利用Garchi CMS的过滤产品API,可以找到here 此API接受基于此的类别ID,空间ID或Pricesorder(用于电子商务)的列表。从我们的Garchi CMS仪表板中,我们知道类别ID并将使用它。由于API接受ID数组,因此我们将其作为数组传递。因此,这应该返回所有被归类为“博客”的项目,因为71(在我的情况下)是“博客”类别的ID。
- 我们向https://garchi.co.uk/api/v1/products/filter提出了邮政请求,目前,我们只是返回响应以检查输出。
由于这是NUXT端的GET API,因此我们可以在浏览器中执行它以查看结果。因此,我们将使用:
开始开发服务器
npm run dev
我们将使用此信息显示我们的博客文章列表。因此,我将在“组件”文件夹中构造一个组件,并将其标记为“ blogcard.vue”。但是首先,让我们根据收到的API响应创建类型。因此,在“资产/类型/post.ts”中包括此代码:
export type PostMeta = {
id: number
key: string
value: string
type: string
}
export type Post = {
product_id: number
slug: string
name: string
description?: string
categories: {
id: number
name: string
}[]
main_image: string
space: {
uid: string
name: string
}
product_meta: PostMeta[]
}
我们的“ blogcard.vue”中的代码应该看起来像这样:
<template>
<article class="flex flex-col items-start justify-between">
<div class="relative w-full">
<img
:src="post.main_image"
alt=""
class="aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover sm:aspect-[2/1] lg:aspect-[3/2]"
/>
<div
class="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10"
/>
</div>
<div class="max-w-xl">
<div class="mt-8 flex items-center gap-x-4 text-xs">
<NuxtLink
class="relative z-10 rounded-full bg-gray-50 px-3 py-1.5 font-medium text-gray-600 hover:bg-gray-100"
>
{{ categoryTitle }}
</NuxtLink>
</div>
<div class="group relative">
<h3
class="mt-3 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600"
>
<NuxtLink :to="`/blog/${post.slug}`">
<span class="absolute inset-0" />
{{ post.name }}
</NuxtLink>
</h3>
</div>
<div class="relative mt-8 flex items-center gap-x-4">
<img
:src="authorImage"
alt=""
class="h-10 w-10 rounded-full bg-gray-100"
/>
<div class="text-sm leading-6">
<p class="font-semibold text-gray-900">
<span>
<span class="absolute inset-0" />
{{ authorName }}
</span>
</p>
</div>
</div>
</div>
</article>
</template>
<script setup lang="ts">
import { Post, PostMeta } from "~/assets/types/Post";
const postProps = defineProps<{
post: Post;
}>();
const categoryTitle = computed(() =>
postProps.post?.categories.map((category) => category.name).join(", ")
);
const authorImage = computed(
() =>
postProps.post?.product_meta?.find((m) => m.key == "avatar")?.value ?? ""
)
const authorName = computed(
() =>
postProps.post?.product_meta?.find((m) => m.key == "author")?.value ?? ""
)
</script>
如果您注意到,我将作者图像,AuthorName和categoryTitle作为计算属性。这是因为当我们将这些值添加为额外字段时,作者图和authorname将在元细节(product_meta)内。类别将是一系列类别,因此我们将使用','。
加入它。我们的index内部。
<template>
<Head>
<Title>Blog</Title>
</Head>
<div class="max-w-6xl p-2 mx-auto xl:py-20
grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 md:gap-5">
<BlogCard v-for="(post, index) in data?.data" :key="index" :post="post" />
</div>
</template>
<script setup lang="ts">
const {data} = await useFetch("/api/blog")
</script>
接下来,我们将创建一个动态路线,该路由将在Click上渲染我们的博客文章。因此,在页面/博客中创建[slug] .vue文件。您还可以创建一个布局以减少代码重写。但是在此示例中,我将重写课程,对此感到抱歉:)
也可以创建一个布局,以使我们的代码重写更少。但是对于此示例,我将重写类。对此很抱歉:)
以前,我们创建了一个API路线。这次,我们可以将NUXT 3的USEASYNCDATA与服务器密钥设置为true一起使用,因此它在服务器上运行。我们将使用Garchi CMS的Get Product Details。
由于API在描述中返回HTML,因此我们需要使用Dompurify软件包清洁它。我们还可以仅允许YouTube标签标签,所以让我们来做。
首先,安装dompurify:
npm i dompurify
如果您使用的是打字稿,则可能需要添加类型:
npm i --save-dev @types/dompurify
我们的[slug]。vue应该看起来像这样:
<template>
<Head>
<Title>{{ article?.name }}</Title>
</Head>
<div class="max-w-6xl p-2 mx-auto xl:py-20 flex flex-col gap-9">
<img
:src="article?.main_image"
alt=""
class="aspect-square w-full xl:w-[30%] xl:h-1/2 mx-auto rounded-2xl bg-gray-100 object-cover object-center"
/>
<h1 class="text-4xl">
{{ article?.name }}
</h1>
<div class="flex items-center space-x-3">
<img
:src="authorImage"
:alt="authorName"
class="w-16 h-16 rounded-full ring-2 ring-gray-900 ring-offset-gray-800"
/>
<span class="text-gray-600 font-semibold text-lg">
By {{ authorName }}
</span>
</div>
<div
class="prose max-w-full"
v-html="description || article?.description"
></div>
</div>
</template>
<script setup lang="ts">
import { Post } from "assets/types/Post";
import * as DOMPurify from "dompurify";
const config = useRuntimeConfig();
const route = useRoute();
const { data, error } = await useAsyncData<{ data: Post[] }>(
`article-${route.params.slug}`,
() => {
return $fetch(`${config.API_URL}/product/${route.params.slug}`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${config.API_KEY}`,
},
});
},
{
server: true,
}
);
const article = computed<Post | null>(() => data.value?.data[0] ?? null);
const description = ref<string>("");
const authorImage = computed(
() => article.value?.product_meta?.find((m) => m.key == "avatar")?.value ?? ""
);
const authorName = computed(
() => article.value?.product_meta?.find((m) => m.key == "author")?.value ?? ""
);
if (error.value || !article.value) {
throw createError({ statusCode: 404, message: "Not found" });
}
onMounted(() => {
DOMPurify.addHook("uponSanitizeElement", (node: HTMLElement, data: any) => {
if (data.tagName == "iframe") {
const allowedSRC = "https://www.youtube.com/embed";
const src = node.getAttribute("src");
if (!src?.startsWith(allowedSRC)) {
return node.parentNode?.removeChild(node);
}
}
});
let content = DOMPurify.sanitize(article.value?.description, {
USE_PROFILES: { html: true },
ADD_TAGS: ["iframe"],
ADD_ATTR: ["autoplay", "allowfullscreen", "frameborder", "scrolling"],
});
description.value = content;
});
</script>
让我们逐步分解代码:
API以格式返回数据{data:post []}
我们有一个计算属性,该属性为我们提供了数据:post [0]。这是因为接受slug或项目ID作为URL参数的API将在响应数组中仅返回一个或不返回一个项目。
我们正在使用Dompurify对我们的文章进行消毒。以HTML格式进行的描述,仅允许某些IFRAME。由于挂钩的挂钩将在客户端发生,因此我正在显示Article.SERCRICTIO。我猜这可以完全在服务器端进行处理。
最后,我们使用尾风的版式插件来调整它。
最终输出是:
得出结论,这两部分文章的主要意图是证明Garchi CMS的使用和快速演示如何使用。
现在,希望您喜欢它:)