Skip to content

Commit

Permalink
feat: 初步完成 nuxt content 与 postgreSQL 集成
Browse files Browse the repository at this point in the history
  • Loading branch information
nonhana committed Nov 17, 2024
1 parent 7d0d512 commit 318b77d
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 45 deletions.
33 changes: 16 additions & 17 deletions components/article/Category.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<script setup lang="ts">
const props = withDefaults(defineProps<{
title?: string
const props = defineProps<{
category: {
id: number
name: string
cover: string | null
articleCount: number
}
index: number
}>(), {
title: 'Category',
})
const { title } = toRefs(props)
const imgUrl = computed(() => `/categories/${flatStr(title.value)}.webp`)
}>()
const { data: articleData } = await useAsyncData(`articles-by-category-${title.value}`, () => queryContent('articles')
.where({ category: title.value })
const { data: articleData } = await useAsyncData(`articles-by-category-${props.category.name}`, () => queryContent('articles')
.where({ category: props.category.name })
.limit(6)
.only(['title', 'publishedAt', '_path', '_id'])
.sort({ publishedAt: -1 })
Expand Down Expand Up @@ -52,8 +51,8 @@ watch(() => props.index, () => resetAnimation)
<div
class="absolute inset-0 flex items-center justify-center rounded-lg text-xl font-bold text-white backface-hidden"
>
<NuxtImg :src="imgUrl" :alt="title" class="absolute -z-10 size-full rounded-lg object-cover" />
<span class="relative select-none">{{ title }}</span>
<NuxtImg :src="category.cover || ''" :alt="category.name" class="absolute -z-10 size-full rounded-lg object-cover" />
<span class="relative select-none">{{ category.name }}</span>
</div>
<div
class="absolute inset-0 rounded-lg bg-white p-4 shadow-md backface-hidden rotate-y-180"
Expand All @@ -65,9 +64,9 @@ watch(() => props.index, () => resetAnimation)
</svg>
<NuxtLink
class="leading-5 with-underline hover:text-hana-blue"
:to="`/articles/categories/${flatStr(title)}`"
:to="`/articles/categories/${flatStr(category.name)}`"
>
{{ title }}
{{ category.name }}
</NuxtLink>
</header>
<main class="mt-5 grid grid-cols-2">
Expand All @@ -90,9 +89,9 @@ watch(() => props.index, () => resetAnimation)
<path d="M14 2v4a2 2 0 0 0 2 2h4M10 9H8m8 4H8m8 4H8" />
</g>
</svg>
<span>{{ articleData?.length || 0 }} 篇文章</span>
<span>{{ category.articleCount }} 篇文章</span>
</div>
<HanaButton :to="`/articles/categories/${flatStr(title)}`">
<HanaButton :to="`/articles/categories/${flatStr(category.name)}`">
more
</HanaButton>
</footer>
Expand Down
5 changes: 5 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export default defineNuxtConfig({
preload: true,
download: true,
},
// server - 服务端 API 配置
nitro: {},
prisma: {
installStudio: false,
},
modules: [
'@nuxtjs/tailwindcss',
'@nuxtjs/i18n',
Expand Down
11 changes: 3 additions & 8 deletions pages/articles/categories/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@ definePageMeta({
name: 'categories',
})
const { data } = await useAsyncData('article-categories', () => queryContent('articles').only(['category']).find())
const { data } = await useAsyncData('article-categories', () => $fetch('/api/categories/list'))
const categories = computed(() => {
if (!data.value)
return []
const categorySet = new Set(data.value.map(article => article.category as string))
return Array.from(categorySet)
})
const categories = computed(() => data.value || [])
</script>

<template>
Expand All @@ -21,7 +16,7 @@ const categories = computed(() => {
</div>
</header>
<div class="grid grid-cols-1 gap-8 md:grid-cols-2">
<ArticleCategory v-for="(category, index) in categories" :key="`${category}-${index}`" :title="category" :index="index" />
<ArticleCategory v-for="(category, index) in categories" :key="`${category}-${index}`" :category="category" :index="index" />
</div>
</div>
</template>
15 changes: 2 additions & 13 deletions pages/articles/tags/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,9 @@ definePageMeta({
name: 'tags',
})
const { data } = await useAsyncData('article-tags', () => queryContent('articles').only(['tags']).find())
const { data } = await useAsyncData('article-tags', () => $fetch('/api/tags/list'))
const tags = computed(() => {
if (!data.value)
return []
const tagMap: Map<string, number> = new Map()
data.value.forEach((article) => {
article.tags.forEach((tag: string) => {
tagMap.set(tag, (tagMap.get(tag) || 0) + 1)
})
})
return Array.from(tagMap.entries())
.map(([name, count]) => ({ name, count }))
})
const tags = computed(() => data.value || [])
</script>

<template>
Expand Down
3 changes: 3 additions & 0 deletions prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ model Article {
tags Tag[] @relation("ArticleTags")
category Category? @relation(fields: [categoryId], references: [id])
categoryId Int?
comments Comment[] // 文章的评论
comments Comment[]
}

model Tag {
Expand Down
5 changes: 5 additions & 0 deletions server/api/articles/list.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default defineEventHandler(async (event) => {
return {
data: '123',
}
})
184 changes: 184 additions & 0 deletions server/api/articles/refresh.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import type { ParsedContent } from '@nuxt/content'
import type * as p from '@prisma/client'
import prisma from '~/lib/prisma'
import { flatStr } from '~/utils/handleStr'

// 从 Nuxt Content 中获取文章数据
async function getNuxtContent() {
const _params = { without: ['body'] }

const queryString = new URLSearchParams({
_params: JSON.stringify(_params),
}).toString()

const url = `/api/_content/query?${queryString}`

const result = await $fetch(url) as ParsedContent[]
return result
}

// 处理 tag 数据,返回 tagName 和 tagId 的映射
async function handleTags(articles: ParsedContent[]): Promise<Record<string, number>> {
// 统计所有文章的 tag
const tagMap = new Map<string, number>()
articles.forEach(({ tags }) => {
tags.forEach((tag: string) => {
tagMap.set(tag, (tagMap.get(tag) || 0) + 1)
})
})

const tagNames = Array.from(tagMap.keys())

const existingTags = await prisma.tag.findMany({
where: { name: { in: tagNames } },
select: { id: true, name: true, articleCount: true },
})

const existingTagNames = new Set(existingTags.map(tag => tag.name))
const newTagNames = tagNames.filter(tagName => !existingTagNames.has(tagName))

const updateTags = existingTags.map(tag => ({
where: { name: tag.name },
data: { articleCount: tagMap.get(tag.name) || tag.articleCount || 0 },
}))

const newTags = newTagNames.map(name => ({
name,
articleCount: tagMap.get(name) || 0,
}))

await prisma.$transaction([
...updateTags.map(update => prisma.tag.update(update)),
prisma.tag.createMany({ data: newTags }),
])

const updatedTags = await prisma.tag.findMany({
where: { name: { in: tagNames } },
select: { id: true, name: true },
})

return updatedTags.reduce((acc, tag) => {
acc[tag.name] = tag.id
return acc
}, {} as Record<string, number>)
}

// 处理 category 数据,返回 categoryName 和 categoryId 的映射
async function handleCategories(articles: ParsedContent[]): Promise<Record<string, number>> {
const categoryMap = new Map<string, number>()
articles.forEach(({ category }) => {
categoryMap.set(category, (categoryMap.get(category) || 0) + 1)
})

const categoryNames = Array.from(categoryMap.keys())

const existingCategories = await prisma.category.findMany({
where: { name: { in: categoryNames } },
})

const existingCategoryNames = new Set(existingCategories.map(category => category.name))
const newCategoryNames = categoryNames.filter(categoryName => !existingCategoryNames.has(categoryName))

const updateCategories = existingCategories.map(category => ({
where: { name: category.name },
data: {
articleCount: categoryMap.get(category.name) || category.articleCount || 0,
cover: category.cover || `/categories/${flatStr(category.name)}.webp`,
},
}))

const newCategories = newCategoryNames.map(name => ({
name,
articleCount: categoryMap.get(name) || 0,
cover: `/categories/${flatStr(name)}.webp`,
}))

await prisma.$transaction([
...updateCategories.map(update => prisma.category.update(update)),
prisma.category.createMany({ data: newCategories }),
])

const updatedCategories = await prisma.category.findMany({
where: { name: { in: categoryNames } },
select: { id: true, name: true },
})

return updatedCategories.reduce((acc, category) => {
acc[category.name] = category.id
return acc
}, {} as Record<string, number>)
}

// 处理 article 数据
async function handleArticles(articles: ParsedContent[]) {
// 获取 tags 和 categories 的映射
const tagMap = await handleTags(articles) // name -> id
const categoryMap = await handleCategories(articles) // name -> id

// 查询已有文章的 title
const existingArticles = await prisma.article.findMany({
select: { title: true, id: true },
})
const existingTitles = new Set(existingArticles.map(article => article.title))
const existingArticleMap = Object.fromEntries(existingArticles.map(article => [article.title, article.id]))

// 分类文章为新增和更新
const { createArticles, updateArticles } = articles.reduce(
(acc, article) => {
const baseData = {
title: article.title!,
description: article.description,
cover: article.cover,
alt: article.alt,
ogImage: article.ogImage,
publishedAt: new Date(article.publishedAt),
editedAt: new Date(article.editedAt),
published: article.published,
wordCount: article.wordCount,
// 关联 tags 和 category
tags: {
connect: article.tags.map((tag: string) => ({ id: tagMap[tag] })),
},
category: { connect: { id: categoryMap[article.category] } },
}

if (existingTitles.has(article.title!)) {
acc.updateArticles.push({ ...baseData, id: existingArticleMap[article.title!] })
}
else {
acc.createArticles.push({ ...baseData, title: article.title! })
}

return acc
},
{ createArticles: [], updateArticles: [] } as {
createArticles: p.Prisma.ArticleCreateInput[]
updateArticles: Array<p.Prisma.ArticleUpdateInput & { id: number }>
},
)

// 使用事务批量操作
await prisma.$transaction([
// 批量新增文章
...createArticles.map(article =>
prisma.article.create({ data: article }),
),
// 批量更新文章
...updateArticles.map(({ id, ...data }) =>
prisma.article.update({ where: { id }, data }),
),
])
}

export default defineEventHandler(async () => {
try {
// 获取文章数据
const articles = await getNuxtContent()
// 处理文章数据
await handleArticles(articles)
return { message: 'Refreshed articles' }
}
catch (error: any) {
return { message: error.message || 'Failed to refresh articles' }
}
})
11 changes: 11 additions & 0 deletions server/api/categories/list.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import prisma from '~/lib/prisma'

async function getCategories() {
const categories = await prisma.category.findMany()
return categories
}

export default defineEventHandler(async () => {
const categories = await getCategories()
return categories
})
6 changes: 0 additions & 6 deletions server/api/hello.ts

This file was deleted.

11 changes: 11 additions & 0 deletions server/api/tags/list.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import prisma from '~/lib/prisma'

async function getTags() {
const tags = await prisma.tag.findMany()
return tags.map(({ name, articleCount }) => ({ name, count: articleCount }))
}

export default defineEventHandler(async () => {
const tags = await getTags()
return tags
})

0 comments on commit 318b77d

Please sign in to comment.