讲师:乌鸦哥
让我们的博客彻底"专业化"!
告别"原始时代",拥抱现代化 🚀
现在的博客就像"手工作坊" 🔨
升级到"现代化工厂" 🏭
制定我们的"搬家计划" 📦
就像搬家一样,要有条不紊
整理我们的"旧家当" 📂
// data/posts.json (示例)
[
{
"id": 1,
"title": "我的第一篇博客",
"content": "这是我第一次写博客...",
"author": "张三",
"createTime": "2024-01-01",
"tags": ["生活", "随想"]
},
{
"id": 2,
"title": "学习前端开发",
"content": "今天学习了Vue.js...",
"author": "李四",
"createTime": "2024-01-05",
"tags": ["技术", "前端"]
}
]
这些数据需要"搬进"数据库
// utils/dataTransform.js
export function transformPostData(fileData) {
return fileData.map(post => ({
title: post.title.trim(),
content: post.content.trim(),
author: post.author || '未知作者',
createdAt: new Date(post.createTime),
status: 'published',
tags: Array.isArray(post.tags) ? post.tags.join(',') : '',
// 添加缺失的字段
slug: generateSlug(post.title),
excerpt: generateExcerpt(post.content),
viewCount: 0
}))
}
function generateSlug(title) {
return title.toLowerCase()
.replace(/[^a-z0-9一-龥]/g, '-')
.replace(/-+/g, '-')
}
function generateExcerpt(content, length = 150) {
return content.substring(0, length) + '...'
}
通过管理后台"搬家" 🚚
<!-- pages/admin/import.vue -->
<template>
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-3xl font-bold mb-8">数据导入</h1>
<div class="bg-yellow-50 border border-yellow-200 rounded p-4 mb-6">
<h3 class="font-bold text-yellow-800">⚠️ 注意</h3>
<p class="text-yellow-700">此操作将导入所有历史文章到数据库,请确保数据备份完成</p>
</div>
<div class="space-y-4">
<button
@click="importPosts"
:disabled="importing"
class="bg-blue-500 text-white px-6 py-3 rounded-lg hover:bg-blue-600 disabled:opacity-50"
>
{{ importing ? '导入中...' : '开始导入文章' }}
</button>
<div v-if="importResult" class="mt-4 p-4 rounded-lg"
:class="importResult.success ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'">
{{ importResult.message }}
</div>
</div>
</div>
</template>
// pages/admin/import.vue 的 script 部分
<script setup>
const importing = ref(false)
const importResult = ref(null)
const importPosts = async () => {
importing.value = true
importResult.value = null
try {
const response = await $fetch('/api/admin/import-posts', {
method: 'POST'
})
importResult.value = {
success: true,
message: `成功导入 ${response.count} 篇文章!`
}
} catch (error) {
importResult.value = {
success: false,
message: '导入失败:' + error.message
}
} finally {
importing.value = false
}
}
</script>
// server/api/admin/import-posts.post.js
import fs from 'fs'
import path from 'path'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default defineEventHandler(async (event) => {
try {
// 验证管理员权限
const user = await getCurrentUser(event)
if (!user || user.role !== 'admin') {
throw createError({
statusCode: 403,
statusMessage: '需要管理员权限'
})
}
// 读取JSON文件
const dataPath = path.join(process.cwd(), 'data/posts.json')
const fileData = JSON.parse(fs.readFileSync(dataPath, 'utf8'))
// 转换数据格式
const transformedData = transformPostData(fileData)
// 批量插入数据库
const results = await prisma.post.createMany({
data: transformedData,
skipDuplicates: true // 跳过重复数据
})
return {
success: true,
count: results.count,
message: `成功导入 ${results.count} 篇文章`
}
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: '导入失败:' + error.message
})
}
})
让页面从数据库"取货" 🛒
<!-- pages/index.vue -->
<script setup>
// ❌ 旧方式:从文件读取
// import postsData from '~/data/posts.json'
// ✅ 新方式:从数据库获取
const { data: response } = await useFetch('/api/posts', {
query: {
limit: 6,
status: 'published'
}
})
const posts = computed(() => response.value?.posts || [])
</script>
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold text-center mb-8">最新文章</h1>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<article
v-for="post in posts"
:key="post.id"
class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow"
>
<div class="p-6">
<h2 class="text-xl font-bold mb-2">
<NuxtLink :to="`/posts/${post.id}`" class="hover:text-blue-600">
{{ post.title }}
</NuxtLink>
</h2>
<p class="text-gray-600 mb-4">
{{ post.excerpt || post.content.substring(0, 150) + '...' }}
</p>
<div class="flex justify-between items-center text-sm text-gray-500">
<span>作者:{{ post.author?.name || post.author }}</span>
<span>{{ formatDate(post.createdAt) }}</span>
</div>
</div>
</article>
</div>
</div>
</template>
<!-- pages/posts/[id].vue -->
<script setup>
const route = useRoute()
const postId = route.params.id
// ❌ 旧方式:从文件查找
// const post = postsData.find(p => p.id === parseInt(postId))
// ✅ 新方式:从数据库获取
const { data: post } = await useFetch(`/api/posts/${postId}`)
if (!post.value) {
throw createError({
statusCode: 404,
statusMessage: '文章不存在'
})
}
// 增加浏览量
await $fetch(`/api/posts/${postId}/view`, { method: 'POST' })
</script>
<template>
<article class="max-w-4xl mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-4xl font-bold mb-4">{{ post.title }}</h1>
<div class="flex items-center justify-between text-gray-600 border-b pb-4">
<div class="flex items-center space-x-4">
<span>作者:{{ post.author?.name || post.author }}</span>
<span>发布时间:{{ formatDate(post.createdAt) }}</span>
</div>
<span>浏览量:{{ post.viewCount }}</span>
</div>
</header>
<div class="prose prose-lg max-w-none">
<div v-html="formatContent(post.content)"></div>
</div>
<footer class="mt-8 pt-4 border-t">
<div v-if="post.tags" class="flex flex-wrap gap-2">
<span
v-for="tag in post.tags.split(',')"
:key="tag"
class="bg-blue-100 text-blue-800 px-2 py-1 rounded text-sm"
>
#{{ tag.trim() }}
</span>
</div>
</footer>
</article>
</template>
添加现代化博客必备功能 ✨
// server/api/posts/[id]/view.post.js
export default defineEventHandler(async (event) => {
try {
const postId = parseInt(getRouterParam(event, 'id'))
// 更新浏览量
await prisma.post.update({
where: { id: postId },
data: {
viewCount: {
increment: 1
}
}
})
return { success: true }
} catch (error) {
// 静默处理错误,不影响用户体验
console.error('更新浏览量失败:', error)
return { success: false }
}
})
// server/api/posts/search.get.js
export default defineEventHandler(async (event) => {
try {
const query = getQuery(event)
const keyword = query.q?.trim()
if (!keyword) {
return { posts: [], total: 0 }
}
const posts = await prisma.post.findMany({
where: {
AND: [
{ status: 'published' },
{
OR: [
{ title: { contains: keyword } },
{ content: { contains: keyword } },
{ tags: { contains: keyword } }
]
}
]
},
orderBy: { createdAt: 'desc' },
include: {
author: {
select: { name: true, avatar: true }
}
}
})
return {
posts,
total: posts.length,
keyword
}
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: '搜索失败'
})
}
})
// server/api/posts/categories.get.js
export default defineEventHandler(async (event) => {
try {
// 获取所有标签并统计使用次数
const posts = await prisma.post.findMany({
where: { status: 'published' },
select: { tags: true }
})
const tagCount = {}
posts.forEach(post => {
if (post.tags) {
post.tags.split(',').forEach(tag => {
const trimmedTag = tag.trim()
if (trimmedTag) {
tagCount[trimmedTag] = (tagCount[trimmedTag] || 0) + 1
}
})
}
})
// 按使用次数排序
const sortedTags = Object.entries(tagCount)
.sort(([,a], [,b]) => b - a)
.map(([tag, count]) => ({ tag, count }))
return { tags: sortedTags }
} catch (error) {
throw createError({
statusCode: 500,
statusMessage: '获取标签失败'
})
}
})
我们的"作品"完成了! 🎊
你已经完成了一个完整的全栈博客系统!
下一步:可以继续添加评论、点赞、分享等高级功能! 🚀