技术文章页面开发
写这个主要是因为社区平台限制太多了,很多都不让发,二维码啥的,还有一个原因就是因为想要将自己写的文档汇总一个在线小册,避免散落的到处都是,也方便大家查看,也可以增加一些订阅
效果
前台
目前主要是根据分类展示文章列表,然后选择文章进入文章详情页,后序可以加上分类置顶,订阅等,这里主要是实现了一个交互式md文档,其实就是mdx也可以实现的,只是我想看看实现原理所以自己写了下
文章分类
交互式md文档
文章详情
后台
文章列表
主要是需要支持增加、删除、更新文章,这里需要实现一个自定义的md渲染器,在开源渲染器的基础上实现的,因为我要注入组件
管理分类
支持编辑、新增、删除分类
新建文档
在开源md编辑器的基础上实现的自定义渲染和编辑,主要是自定义了一些工具条和渲染
编辑文章
编辑文章标题,分类,标签,封面等,封面现在感觉不用还挺好看的,懒得加,看看后面要不要,
开发思路
交互式md实现思路
之前写过了,在这里 交互式md文档实现思路
根据开源md编辑器实现自定义编辑器
通过react-markdown + @uiw/react-md-editor 实现编辑和渲染md
- 支持渲染自定义组件
- 支持粘贴上传图片到oss
数据表设计
看注释
export interface Article {
// MongoDB ID
_id?: string;
// 文章标题
title: string;
// 文章链接(slug),目前废弃,之前是打算支持导入其他平台的文章,但是平台太多了,适配太麻烦了
url?: string;
// 文章分类
category?: string;
// 文章分类ID
categoryId?: string;
// 文章标签(可以有多个)
tags?: string[];
// 文章内容(Markdown格式)
content: string;
// OSS存储路径
ossPath: string;
// 文章状态(draft-草稿/published-已发布)
status: ArticleStatus;
// 文章摘要
summary?: string;
// 封面图片URL
coverImage?: string;
// 点赞数
likes?: number;
// 阅读数
views?: number;
// 创建时间
createdAt: string;
// 更新时间(可选)
updatedAt?: string;
}
// 文章状态枚举
export enum ArticleStatus {
DRAFT = 'draft',
PUBLISHED = 'published'
}
// 文章分类接口
export interface ArticleCategory {
_id?: string;
name: string;
description?: string;
createdAt: string;
updatedAt: string;
}
api设计
文章api
mongodb的基本增删改查,没啥可说的,用时间堆出来
// 创建新文章
export async function POST(request: Request) {
try {
const article = await request.json();
const db = await getDb();
const articleToInsert: IArticleDB = {
...article,
likes: 0,
views: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const result = await db.collection<IArticleDB>("articles").insertOne(articleToInsert);
return NextResponse.json({
_id: result.insertedId.toString(),
...article,
});
} catch (error: any) {
console.error("Error creating article:", error);
return NextResponse.json(
{ error: error.message || "Failed to create article" },
{ status: 500 }
);
}
}
// 获取文章列表或单篇文章
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const status = searchParams.get("status");
const id = searchParams.get("id");
const categoryId = searchParams.get("categoryId");
const db = await getDb();
// 如果有 ID,获取单篇文章
if (id) {
const article = await db.collection<IArticleDB>("articles").findOne({
_id: new ObjectId(id),
});
if (!article) {
return NextResponse.json(
{ error: "Article not found" },
{ status: 404 }
);
}
return NextResponse.json(toArticle(article));
}
// 否则获取文章列表
const query: any = {};
if (status) {
query.status = status;
}
if (categoryId) {
query.categoryId = categoryId;
}
const articles = await db
.collection<IArticleDB>("articles")
.find(query)
.sort({ updatedAt: -1 })
.toArray();
return NextResponse.json({ articles: articles.map(toArticle) });
} catch (error: any) {
console.error("Error fetching articles:", error);
return NextResponse.json(
{ error: error.message || "Failed to fetch articles" },
{ status: 500 }
);
}
}
// 更新文章
export async function PUT(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const article = await request.json();
if (!id) {
return NextResponse.json(
{ error: "Article ID is required" },
{ status: 400 }
);
}
const db = await getDb();
const articleToUpdate = {
...toDBArticle(article),
updatedAt: new Date().toISOString(),
};
const result = await db.collection<IArticleDB>("articles").updateOne(
{ _id: new ObjectId(id) },
{ $set: articleToUpdate }
);
if (result.matchedCount === 0) {
return NextResponse.json(
{ error: "Article not found" },
{ status: 404 }
);
}
return NextResponse.json({
_id: id,
...article,
});
} catch (error: any) {
console.error("Error updating article:", error);
return NextResponse.json(
{ error: error.message || "Failed to update article" },
{ status: 500 }
);
}
}
// 删除文章
export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
if (!id) {
return NextResponse.json(
{ error: "Article ID is required" },
{ status: 400 }
);
}
const db = await getDb();
const result = await db.collection<IArticleDB>("articles").deleteOne({
_id: new ObjectId(id),
});
if (result.deletedCount === 0) {
return NextResponse.json(
{ error: "Article not found" },
{ status: 404 }
);
}
return NextResponse.json({ success: true });
} catch (error: any) {
console.error("Error deleting article:", error);
return NextResponse.json(
{ error: error.message || "Failed to delete article" },
{ status: 500 }
);
}
}
文章分类api
也是一样
// 获取所有文章分类
export async function GET() {
try {
const db = await getDb();
const categories = await db
.collection<IArticleCategory>("articleCategories")
.find()
.sort({ name: 1 })
.toArray();
return NextResponse.json({ success: true, categories });
} catch (error) {
console.error("Error fetching article categories:", error);
return NextResponse.json(
{ error: "Failed to fetch article categories" },
{ status: 500 }
);
}
}
// 创建新分类
export async function POST(request: Request) {
try {
const { name, description } = await request.json();
const db = await getDb();
// 检查分类名是否已存在
const existingCategory = await db
.collection<IArticleCategory>("articleCategories")
.findOne({ name });
if (existingCategory) {
return NextResponse.json(
{ error: "Category already exists" },
{ status: 400 }
);
}
const categoryToInsert: IArticleCategory = {
name,
description,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const result = await db
.collection<IArticleCategory>("articleCategories")
.insertOne(categoryToInsert);
if (result.acknowledged) {
return NextResponse.json({
success: true,
category: { ...categoryToInsert, _id: result.insertedId },
});
}
throw new Error("Failed to insert category");
} catch (error) {
console.error("Error creating article category:", error);
return NextResponse.json(
{ error: "Failed to create article category" },
{ status: 500 }
);
}
}
// 更新分类
export async function PUT(request: Request) {
try {
const { id, name, description } = await request.json();
if (!name || name.trim().length === 0) {
return NextResponse.json(
{ error: "分类名称不能为空" },
{ status: 400 }
);
}
const db = await getDb();
// 检查分类是否存在
const category = await db
.collection<IArticleCategory>("articleCategories")
.findOne({ _id: new ObjectId(id) });
if (!category) {
return NextResponse.json(
{ error: "分类不存在" },
{ status: 404 }
);
}
// 检查是否存在同名分类(排除当前分类)
const existingCategory = await db
.collection<IArticleCategory>("articleCategories")
.findOne({ name, _id: { $ne: new ObjectId(id) } });
if (existingCategory) {
return NextResponse.json(
{ error: "分类名称已存在" },
{ status: 400 }
);
}
const result = await db.collection<IArticleCategory>("articleCategories").updateOne(
{ _id: new ObjectId(id) },
{
$set: {
name,
description,
updatedAt: new Date().toISOString(),
},
}
);
if (result.matchedCount === 0) {
return NextResponse.json(
{ error: "Category not found" },
{ status: 404 }
);
}
return NextResponse.json({
success: true,
message: "Category updated successfully",
});
} catch (error) {
console.error("Error updating article category:", error);
return NextResponse.json(
{ error: "Failed to update article category" },
{ status: 500 }
);
}
}
// 删除分类
export async function DELETE(request: Request) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json(
{ error: "分类 ID 不能为空" },
{ status: 400 }
);
}
const db = await getDb();
// 检查分类是否存在
const category = await db
.collection<IArticleCategory>("articleCategories")
.findOne({ _id: new ObjectId(id) });
if (!category) {
return NextResponse.json(
{ error: "分类不存在" },
{ status: 404 }
);
}
// 检查是否有文章使用该分类
const articlesCount = await db
.collection("articles")
.countDocuments({ categoryId: new ObjectId(id) });
if (articlesCount > 0) {
return NextResponse.json(
{ error: "该分类下还有文章,无法删除" },
{ status: 400 }
);
}
const result = await db
.collection<IArticleCategory>("articleCategories")
.deleteOne({ _id: new ObjectId(id) });
if (result.deletedCount === 0) {
return NextResponse.json(
{ error: "删除分类失败" },
{ status: 500 }
);
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("删除分类出错:", error);
return NextResponse.json(
{ error: "删除分类失败" },
{ status: 500 }
);
}
}
点赞 & 浏览量增加
就是改个值,通过文章id找到对应的数据改值,关键是业务的写法
import { getDb } from "@/lib/mongodb";
import { NextResponse } from "next/server";
import { ObjectId } from "mongodb";
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const db = await getDb();
const articleId = params.id;
const result = await db.collection("articles").updateOne(
{ _id: new ObjectId(articleId) },
{ $inc: { likes: 1 } }
);
if (result.modifiedCount === 0) {
return NextResponse.json(
{ error: "Article not found" },
{ status: 404 }
);
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error updating article likes:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
import { getDb } from "@/lib/mongodb";
import { NextResponse } from "next/server";
import { ObjectId } from "mongodb";
export async function POST(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const db = await getDb();
const articleId = params.id;
const result = await db.collection("articles").updateOne(
{ _id: new ObjectId(articleId) },
{ $inc: { views: 1 } }
);
if (result.modifiedCount === 0) {
return NextResponse.json(
{ error: "Article not found" },
{ status: 404 }
);
}
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error updating article views:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}