个人网站开发记录-技术文章页面开发

962 阅读3分钟

技术文章页面开发

写这个主要是因为社区平台限制太多了,很多都不让发,二维码啥的,还有一个原因就是因为想要将自己写的文档汇总一个在线小册,避免散落的到处都是,也方便大家查看,也可以增加一些订阅

效果

前台

目前主要是根据分类展示文章列表,然后选择文章进入文章详情页,后序可以加上分类置顶,订阅等,这里主要是实现了一个交互式md文档,其实就是mdx也可以实现的,只是我想看看实现原理所以自己写了下

文章分类

交互式md文档

在线体验交互式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 }
    );
  }
}