项目使用了Node后端、Express和Vite搭建的全栈博客系统系列,将Vue 2项目重构为Vue 3版本。该系统包含了以下主要模块和功能:
登录和注册功能:用户可以通过注册账号和登录来访问博客系统。
分类列表:展示不同分类的文章,包括技术栈、精品短文和热门文章。
更多功能:
数据分析:提供文章数据的统计和分析功能。
评论审查:对用户评论进行审查和管理。
回收站:将已删除的文章移至回收站,可以进行恢复或永久删除。
浏览历史:记录用户的浏览历史,方便查看之前浏览过的文章。
搜索:提供文章标题搜索功能,方便用户查找感兴趣的文章。
详情:点击文章标题可以查看文章的详细内容。
评论:允许用户在文章下发表评论。
点赞:用户可以给喜欢的文章点赞。
基本的增删改查功能:支持对文章进行基本的增加、删除、修改和查询操作。
通过这个博客系统,用户可以方便地管理和浏览文章,以及与其他用户进行互动和讨论。系统使用了最新的Vue 3版本,提供了更好的性能和开发体验。
项目开源地址:https://gitee.com/hailang123/kao2-vite-mysql
路由
import { createRouter, createWebHistory } from "vue-router";
const routes = [
{
path: "/login",
name: "login",
component: () => import("../views/login.vue"),
},
{
path: "/register",
name: "register",
component: () => import("../views/register.vue"),
},
{
path: "/",
name: "index",
component: () => import("../views/index.vue"),
children: [
{
// 详情页,通过id来获取文章信息
path: "/detail/:id",
name: "detail",
component: () => import("../views/detail.vue"),
},
{
path: "/fabu",
name: "fabu",
component: () => import("../views/fabu.vue"),
},
{
path: "/biaoqian",
name: "biaoqian",
component: () => import("../views/biaoqian.vue"),
},
{
path: "/fenlei",
name: "fenlei",
component: () => import("../views/fenlei.vue"),
},
{
path: "/shouye",
name: "shouye",
component: () => import("../views/shouye.vue"),
},
{
path: "/lixiang",
name: "lixiang",
component: () => import("../views/lixiang.vue"),
},
{
path: "/jishuzhan",
name: "jishuzhan",
component: () => import("../views/jishuzhan.vue"),
},
{
path: "/fenxi",
name: "fenxi",
component: () => import("../views/fenxi.vue"),
},
{
path: "/duanwen",
name: "duanwen",
component: () => import("../views/duanwen.vue"),
},
{
path: "/labcloud",
name: "labcloud",
component: () => import("../views/labcloud.vue"),
},
{
path: "/hotpage",
name: "hotpage",
component: () => import("../views/hotpage.vue"),
},
{
path: "/newping",
name: "newping",
component: () => import("../views/newping.vue"),
},
{
path: "/history",
name: "history",
component: () => import("../views/history.vue"),
},
{
path: "/huishou",
name: "huishou",
component: () => import("../views/huishou.vue"),
}
],
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
详情
<template>
<div class="main">
<div>
<div class="title">{{ state.title }}</div>
<div class="content">{{ state.content }}</div>
<div class="author">作者:{{ state.author }}</div>
<div><Dianzan :articleId=state.id></Dianzan></div>
<div class="created_at">创建时间:{{ state.created_at }}</div>
</div>
<div>
<a-button @click="bankFn" type="primary" :size="size" class="bank"
>返回</a-button
>
<a-button @click="preFn" type="primary" :size="size" class="pre"
>上一篇</a-button
>
<a-button
@click="nestFn"
:disabled="isLastPage"
type="primary"
:size="size"
class="next"
>下一篇</a-button
>
</div>
<div>
<Pinglun :articleId="route.params.id" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import { getArticleById, showPassageApi } from "../utils/api";
import { useRoute, useRouter } from "vue-router";
import Pinglun from "../components/pinglun.vue";
import Dianzan from "../components/dianzan.vue";
const state = ref({});
const route = useRoute();
const router = useRouter();
const result = async () => {
const articleId = route.params.id;
const res = await getArticleById({ id: articleId });
state.value = res.data[0];
console.log(state.value);
// 获取当前浏览的时间
const time = new Date().toLocaleString();
// 将当前浏览详的时间添加到浏览详情数据中
state.value.time = time;
// 从localStorage获取已有的浏览记录
let history = JSON.parse(localStorage.getItem("history")) || [];
// 将当前浏览详情数据添加到浏览记录中
history.push(state.value);
// 将更新后的浏览记录保存回localStorage
localStorage.setItem("history", JSON.stringify(history));
};
result();
// 返回到首页
const bankFn = () => {
router.push("/shouye");
};
const preFn = async () => {
const articleId = route.params.id;
const res = await getArticleById({ id: articleId });
console.log(res.data[0].id);
if (res.data[0].id == 1) {
alert("已经是第一篇了");
} else {
router.push(`/detail/${res.data[0].id - 1}`);
state.value = res.data[0];
}
};
const nestFn = async () => {
const articleId = route.params.id;
const res = await getArticleById({ id: articleId });
console.log(res.code);
if (res.data.length === 0) {
// 阻止下一篇按钮的点击事件
return;
alert("已经是最后一篇了");
} else {
router.push(`/detail/${res.data[0].id + 1}`);
state.value = res.data[0];
}
};
const isLastPage = computed(() => {
showPassageApi().then((res) => {
// 计算一共有多少条数据
const total = res.data.length;
return state.value.id === total;
});
});
</script>
<style lang="less" scoped>
.main {
width: 100%;
height: 100%;
background-color: antiquewhite;
.title {
width: 100%;
height: 50px;
font-size: 20px;
font-weight: bold;
margin: 0 auto;
text-align: center;
padding: 15px;
}
.content {
width: 100%;
height: 100%;
font-size: 15px;
font-family: "微软雅黑";
margin: 0 auto;
padding: 15px;
// 文字缩进2
text-indent: 2em;
}
.author {
width: 100%;
height: 50px;
font-size: 15px;
font-family: "微软雅黑";
margin: 0 auto;
padding: 15px;
text-align: right;
}
.created_at {
width: 100%;
height: 50px;
font-size: 15px;
font-family: "微软雅黑";
margin: 0 auto;
padding: 15px;
text-align: right;
}
}
</style>
首页
<template>
<a-layout>
<a-layout-header class="header">
<a-menu
v-model:selectedKeys="selectedKeys1"
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">
<router-link to="/shouye"> 首页 </router-link>
</a-menu-item>
<a-menu-item key="2">
<router-link to="/fenlei">文章分类 </router-link>
</a-menu-item>
<a-menu-item key="3">
<router-link to="/biaoqian"> 标签 </router-link>
</a-menu-item>
<a-menu-item key="4">
<router-link to="/fabu"> 发布 </router-link>
</a-menu-item>
<a-menu-item key="5" class="yidong">
<router-link to="/login">登录</router-link>
</a-menu-item>
<a-menu-item key="6">
<router-link to="/register">注册</router-link>
</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span>
<user-outlined />
分类列表
</span>
</template>
<a-menu-item key="1">
<router-link to="/jishuzhan"> 技术栈 </router-link>
</a-menu-item>
<a-menu-item key="2">
<router-link to="/duanwen"> 精品短文 </router-link>
</a-menu-item>
<a-menu-item key="3">
<router-link to="/hotpage"> 热门文章 </router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span>
<laptop-outlined />
更多
</span>
</template>
<a-menu-item key="5">
<router-link to="/fenxi"> 数据分析 </router-link>
</a-menu-item>
<a-menu-item key="6">
<router-link to="/newping"> 评论审查 </router-link>
</a-menu-item>
<a-menu-item key="7">
<router-link to="/history"> 浏览历史 </router-link>
</a-menu-item>
<a-menu-item key="8">
<router-link to="/huishou"> 回收站 </router-link>
</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span>
<notification-outlined />
关于作者
</span>
</template>
<a-menu-item key="9">个人简介</a-menu-item>
<a-menu-item key="10">联系方式</a-menu-item>
<a-menu-item key="11">友情链接</a-menu-item>
<a-menu-item key="12">
<router-link to="/lixiang"> 立项要求 </router-link>
</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout style="padding: 0 24px 24px">
<a-breadcrumb style="margin: 16px 0">
<a-breadcrumb-item>
<router-link to="/shouye">首页</router-link>
</a-breadcrumb-item>
<a-breadcrumb-item>
{{ breadcrumbNameMap[$route.path] }}
</a-breadcrumb-item>
</a-breadcrumb>
<a-layout-content
:style="{
background: '#fff',
padding: '24px',
margin: 0,
minHeight: '280px',
}"
>
<router-view />
</a-layout-content>
</a-layout>
</a-layout>
<Footer class="main" />
</a-layout>
</template>
<script setup>
import { ref, computed } from "vue";
import Footer from "../components/footer.vue";
const selectedKeys1 = ref(["1"]);
const selectedKeys2 = ref(["2"]);
const collapsed = ref(false);
const openKeys = ref(["sub1"]);
// 定义面包屑
const breadcrumbNameMap = {
"/shouye": "首页",
"/fenlei": "文章分类",
"/biaoqian": "标签",
"/fabu": "发布",
"/login": "登录",
"/register": "注册",
"/lixiang": "立项要求",
};
const currentBreadcrumb = computed(() => breadcrumbNameMap[$route.path]);
</script>
<style>
.yidong {
margin-left: 900px;
}
#components-layout-demo-top-side-2 .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top-side-2 .logo {
float: right;
margin: 16px 0 16px 24px;
}
.site-layout-background {
background: #fff;
}
.main {
margin: 0 auto;
/* 固定底部 */
}
</style>
回收站模块
<template>
<div class="main">
<h1>回收站</h1>
<div v-for="item in state" :key="item.id">
<h4>标题:{{ item.title }}</h4>
<p>内容:{{ item.content }}</p>
<p>作者:{{ item.author }}</p>
<p>创建时间:{{ item.created_at }}</p>
<!-- 其他需要展示的内容 -->
<a-button @click="bankFn(item.id)">恢复</a-button>
<a-button>彻底删除</a-button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { showPassageApi, recoverPassageApi } from "../utils/api";
const state = ref({});
// 获取showPassageApi
const data = async () => {
const res = await showPassageApi();
console.log(res);
// 将过滤后的数据添加到state中
res.data.forEach((item) => {
// 过滤数据is_deleted为1的数据
if (item.is_deleted === 1) {
state.value[item.id] = item;
console.log(item);
}
});
};
onMounted(() => {
data();
});
// 恢复
const bankFn = (id) => {
console.log(id);
recoverPassageApi({ id: id }).then((res) => {
console.log(res);
});
// 删除当前行
delete state.value[id];
};
</script>
<style lang="less" scoped>
.main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
h3 {
margin-top: 20px;
}
div {
width: 100%;
height: 220px;
border: 1px solid #ccc;
margin-top: 20px;
padding: 10px;
h4 {
font-size: 20px;
}
p {
font-size: 16px;
}
}
}
</style>
历史记录模块
<template>
<div>
<h2>浏览记录</h2>
<ul v-for="page in history" :key="page">
<li >{{ page.title }}</li>
<li>{{ page.author }}</li>
<li>{{ page.content }}</li>
<li>{{ page.time }}</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
history: [],
};
},
created() {
// 读取本地存储的浏览记录
const savedHistory = localStorage.getItem("history");
if (savedHistory) {
this.history = JSON.parse(savedHistory);
}
},
methods: {
addToHistory(page) {
this.history.push(page);
// 存储浏览记录到localStorage
localStorage.setItem("history", JSON.stringify(this.history));
},
},
};
</script>
发布模块
<template>
<a-form :form="form" @submit="handleSubmit">
<a-form-item label="标题" name="title">
<a-input v-model:value="form.title" />
</a-form-item>
<a-form-item label="内容" name="content">
<a-textarea v-model:value="form.content" />
</a-form-item>
<a-form-item label="作者" name="author_id">
<a-select v-model:value="form.author_name">
<a-select-option
v-for="author in authors"
:key="author.id"
:value="author.name"
>
{{ author.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="分类" name="category_id">
<a-select v-model:value="form.category_id">
<a-select-option
v-for="category in categories"
:key="category.id"
:value="category.id"
>
{{ category.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">发布</a-button>
</a-form-item>
</a-form>
</template>
<script setup>
import { ref } from "vue";
import { message } from "ant-design-vue";
import { addPassageApi, showCategoryApi } from "../utils/api.js";
// 获取浏览器当前登录用户的信息
const currentUser = localStorage.getItem("username");
const currentUserId = localStorage.getItem("id");
const authors = [{ id: currentUserId, name: currentUser }];
const categories = ref([]);
const formatDate = (date) => {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
const seconds = date.getSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
const form = ref({
title: "",
content: "",
author_name: currentUser, // 使用当前用户名作为初始值
category_id: null,
});
const handleSubmit = () => {
const currentDate = new Date();
form.value.created_at = formatDate(currentDate);
form.value.updated_at = formatDate(currentDate);
form.value.published_at = formatDate(currentDate);
console.log(form.value);
addPassageApi({
title: form.value.title,
content: form.value.content,
author: form.value.author_name,
category_id: form.value.category_id,
created_at: form.value.created_at,
updated_at: form.value.updated_at,
published_at: form.value.published_at,
}).then((res) => {
console.log(res);
});
message.success("文章发布成功");
};
// 获取showCategoryApi()接口返回的数据
showCategoryApi().then((res) => {
categories.value = res.data.map((item) => ({
id: item.id,
name: item.name,
}));
console.log(categories.value);
});
</script>
<style scoped>
.a-form-item {
margin-bottom: 24px;
}
</style>
后端基本的接口
const express = require("express");
const router = express.Router();
const db = require("../utils/db");
// 文章接口
// 获取全部文章
router.post("/api/articleadd", (req, res) => {
let sql = `select * from articles`;
db.query(sql, (err, result) => {
if (err) {
res.json({
status: 500,
msg: "服务器内部错误",
});
} else {
res.json({
code: 200,
msg: "获取成功",
data: result,
});
}
});
});
// 添加文章
router.post("/api/article", (req, res) => {
const {
title,
content,
author,
category_id,
created_at,
updated_at,
published_at,
} = req.body;
console.log(req.body);
// 将文章信息保存到数据库
const sql =
"INSERT INTO articles (title, content, author, category_id, created_at, updated_at, published_at) VALUES (?, ?, ?, ?, ?, ?, ?)";
const values = [
title,
content,
author,
category_id,
created_at,
updated_at,
published_at,
];
db.query(sql, values, (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "保存文章信息失败" });
} else {
res.json({ message: "保存文章信息成功" });
}
});
});
// export const getArticleById = (params) =>request.post("/api/articleById",params);
router.post("/api/articleById", (req, res) => {
const { id } = req.body;
console.log(req.body);
const sql = "SELECT * FROM articles WHERE id = ?";
db.query(sql, [id], (err, result) => {
if (err) {
console.error(err);
res.send({ code: 500, msg: "获取文章信息失败" });
} else {
res.send({
code: 200,
msg: "获取成功",
data: result,
});
}
});
});
// export const getCategoryById = (params) =>request.post("/api/categoryById",params);
router.post("/api/categoryById", (req, res) => {
const { id } = req.body;
const sql = "SELECT * FROM categories WHERE id = ?";
db.query(sql, [id], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "获取分类信息失败" });
} else {
res.json(result);
}
});
});
// export const searchApi = (params) =>reque st.post("/api/search",params);
router.post("/api/search", (req, res) => {
const { searchValue } = req.body;
let keyword = searchValue;
const sql = `SELECT * FROM articles WHERE title LIKE '%${keyword}%'`;
db.query(sql, (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "搜索失败" });
} else {
res.send({
code: 200,
msg: "获取成功",
data: result,
});
}
});
});
// export const getTagBytag = (params) =>request.post("/api/tagById",params);
router.post("/api/tagBytag", (req, res) => {
const { tag } = req.body;
const name = tag;
const sql = "INSERT INTO tags (name) VALUES (?)"; // Modify the SQL statement to insert the name
db.query(sql, [name], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "插入标签信息失败" }); // Update the error message for insertion failure
} else {
res.json(result);
}
});
});
// export const showTagApi = () => request.post("/api/tagadd");
router.post("/api/tagadd", (req, res) => {
const sql = "SELECT * FROM tags";
db.query(sql, (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "获取标签信息失败" });
} else {
res.json(result);
}
});
});
// export const addCommentApi = (params) => request.post("/api/comment", params);
router.post("/api/comment", (req, res) => {
const { article_id, user_id, content, created_at, updated_at } = req.body;
console.log(req.body);
const sql =
"INSERT INTO comments (article_id,user_id, content, created_at, updated_at) VALUES (?, ?, ?, DEFAULT, DEFAULT)";
const values = [article_id, user_id, content, created_at, updated_at];
db.query(sql, values, (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "保存评论信息失败" });
} else {
res.json({ message: "保存评论信息成功" });
}
});
});
// 删除文章
// export const deletePassageApi = (params) => request.post("/api/articledelete", params);
router.post("/api/articledelete", (req, res) => {
const { id } = req.body;
const sql = "UPDATE articles SET is_deleted = 1 WHERE id = ?";
db.query(sql, [id], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "修改文章状态失败" });
} else {
res.send({
code: 200,
msg: "删除成功",
data: result,
});
}
});
});
// 恢复文章
// export const recoverPassageApi = (params) => request.post("/api/articlerecover", params);
router.post("/api/articlerecover", (req, res) => {
const { id } = req.body;
const sql = "UPDATE articles SET is_deleted = 0 WHERE id = ?";
db.query(sql, [id], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "修改文章状态失败" });
} else {
res.send({
code: 200,
msg: "恢复成功",
data: result,
});
}
});
});
// export const getCommentApi = (params) => request.post("/api/getComment", params);
// 根据文章id获取评论
router.post("/api/getComment", (req, res) => {
const { article_id } = req.body;
const sql = "SELECT * FROM comments WHERE article_id = ?";
db.query(sql, [article_id], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "获取评论信息失败" });
} else {
res.json(result);
}
});
});
// export const getAllCommentApi = () => request.post("/api/getAllComment");
// 获取全部评论
router.post("/api/getAllComment", (req, res) => {
const sql = "SELECT * FROM comments";
db.query(sql, (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "获取评论信息失败" });
} else {
res.send({
code: 200,
msg: "获取成功",
data: result,
});
}
});
});
// export const checkCommentApi = (params) => request.post("/api/checkComment", params);
// 审核评论
router.post("/api/checkComment", (req, res) => {
const { id, approved } = req.body;
console.log(req.body);
const sql = "UPDATE comments SET approved = ? WHERE id = ?";
db.query(sql, [approved, id], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "审核评论失败" });
} else {
res.json({ message: "审核评论成功" });
}
});
});
// export const addCategoryApi = (params) => request.post("/api/category", params);
router.post("/api/category", (req, res) => {
const { name } = req.body;
console.log(req.body);
const sql = "INSERT INTO categories (name) VALUES (?)";
db.query(sql, [name], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "添加分类失败" });
} else {
res.send({
code: 200,
msg: "添加成功",
data: result,
});
}
});
});
// export const showCategoryApi = () => request.post("/api/categoryadd");
router.post("/api/categoryadd", (req, res) => {
const sql = "SELECT * FROM categories";
db.query(sql, (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "获取分类信息失败" });
} else {
res.send({
code: 200,
msg: "获取成功",
data: result,
});
}
});
});
// export const deleteCategoryApi = (params) => request.post("/api/categorydelete", params);
router.post("/api/categorydelete", (req, res) => {
const { id } = req.body;
console.log(req.body);
const sql = "DELETE FROM categories WHERE id = ?";
db.query(sql, [id], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "删除分类失败" });
} else {
res.json({ message: "删除分类成功" });
}
});
});
// export const addLikeApi = (params) => request.post("/api/like", params);
router.post("/api/like", (req, res) => {
const { article_id, likes_count } = req.body;
console.log(req.body);
const sql =
"INSERT INTO articles_like (article_id, likes_count) VALUES (?, ?)";
const values = [article_id, likes_count];
db.query(sql, values, (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "保存点赞信息失败" });
} else {
// 获取点赞信息
res.send({
code: 200,
msg: "保存成功",
data: result,
});
}
});
});
// export const getLikeApi = (params) => request.post("/api/getLike", params);
router.post("/api/getLike", (req, res) => {
const { article_id } = req.body;
const sql = "SELECT * FROM articles_like WHERE article_id = ?";
db.query(sql, [article_id], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "获取点赞信息失败" });
} else {
res.send({
code: 200,
msg: "获取成功",
data: result,
});
}
});
});
// export const addCountApi = (params) => request.post("/api/count", params);
router.post("/api/count", (req, res) => {
// 根据articles表中的article_id字段,插入到views_count字段
const { id, views_count } = req.body;
console.log(req.body);
const sql = "UPDATE articles SET views_count = ? WHERE id = ?";
db.query(sql, [views_count, id], (err, result) => {
if (err) {
console.error(err);
res.status(500).json({ message: "保存浏览量信息失败" });
} else {
res.json({ message: "保存浏览量信息成功" });
}
});
});
module.exports = router;