大三只是想学点Vue,正好还在学数据库,所以就写了这个小项目作为练习。
代码存储库在这里:[github]WoodenStone/article_admin
在博客上查看:WoodenStone 的博客
概述
Article Admin是一个前后端分离的文章/博客管理系统。 前端采用Vue2.x结合ElementUI,后端采用Node.js的Express框架,数据库为MySQL8.0。
预览
主要功能
- 注册
- 登录 / 注销
- 用户信息更改
- 文章管理
- 发布文章
- 删除文章
- 编辑文章
- 模糊搜索
- 点赞
- 收藏到特定收藏夹
- Markdown编辑器及图片插入
- 文章标签
- 按特定方式排序(时间倒序、赞数降序、评论数降序、收藏数降序)
- 评论回复
- 评论文章
- 回复用户
- 查看个人收到的评论、回复
- 收藏夹
- 添加 / 删除收藏夹
- 更改收藏夹名及描述
- 内部文章查看、删除、移动
- 用户关注
- 关注和取关
- 站内信
- 收 / 发站内信
- 阅读状态标记
- 输入错误地址时重定向至404
设计精化数据库概述
数据库设计总共包含8个实体和14个连接。
ER图:
补充说明意见-意见回复表
对于评论表单,数据库数组如下:
comment_id | publisher_id | recipient_id | article_commented_id | content | create_time | is_reply | comment_index
comment_id 是标识评论或回复的唯一 ID 的字段。
publisher_id、recipient_id和article_commented_id都是字段,分别对应user_info的用户信息表中的user_id、user_id和文章文章信息表中的idelementui 后端排序,分别表示发布者id、接收者id和评论文章的id。
其中comment_id、publisher_id、article_commented_id、content和create_time不能为空,这意味着需要唯一确定某个用户在某篇文章下发表的某条评论。 其中recipient_id可以为空,因为如果用户直接回复文章,则不需要指定收件人ID(即文章的作者ID); 相应地,如果不为空,则需要在is_reply中指定为1,并指定收件人ID和评论在文章中的索引。 comment_index字段的设置是因为用户可能在某一篇文章下发表多条评论,而直接找到comment_id太长,所以显式指定文章内的索引。 以下是api接口返回的示例:
[
{
"comment_id": 35,
"publisher_id": 1,
"recipient_id": null,
"article_commented_id": 20,
"content": "月が綺麗ですね",
"create_time": "2021-11-22T12:54:36.000Z",
"is_reply": null,
"comment_index": 0,
"children": [
{
"comment_id": 36,
"publisher_id": 1,
"recipient_id": 1,
"article_commented_id": 20,
"content": "月が綺麗ですね",
"create_time": "2021-11-22T12:54:40.000Z",
"is_reply": 1,
"comment_index": 0,
"publisher_name": "admin",
"recipient_name": "admin"
}
],
"publisher_name": "admin"
}
]
后端
后端没有完整的架构,仅提供RESTful API来操作数据库的增删改查。
API均以/api/为前缀,提供的功能在注释中有简单说明。 总共有四十多个,清单太繁琐了。 您可以在博客中查看。
这个项目的写一开始比较随意(没有经验),一般是一边构思一边写页面,使用的时候就去前端写了一个API,没想到有一个结构。 比较混乱。 而且刚开始写作的时候,我对RESTful风格也不太了解。 基本上都是以动词+名字的方式来命名。 一开始的方法只用了GET,后来学习了POST,全部做成POST,最后又改了。
补充说明文章标签
由于文章和标签是多对多的关系,数据库设计时分别提取文章标签id和文章id,形成描述映射关系,而文章表和标签表独立存在。 这使得更改标签变得很麻烦。
更新文章时,每个标签都可能被修改或删除,所以采用的方法是先删除文章原来的标签映射(article_tag表),然后更新标签表(tag)elementui 后端排序,最后重建文章和文章的映射标签(article_tag 表)。 其过程是:
delete tag mappings -> add tags -> add tag mappings
效率比较低,或许以后能找到更好的形式。
上传图片
图片上传使用multer中间件,用于上传用户头像和上传文章中的图片。 主要思想是:
评论和回复
评论和回复的sql逻辑不太明显,因为数据库表数组的设计造成了一些麻烦。
主要功能有两个:①查询某篇文章下的评论回复;②查询某个用户收到的评论回复。
查询某文章下的评论和回复
因为需要返回的是一棵高度最多为2层的树,例如:
- 评论1
- 回复1
- 回复2
- 评论2
- 回复1
- 回复2
- 评论3
因此,采取的方法是,首先查找文章下的所有评论,获取包含所有评论id的字段,然后依次查找每个评论下是否有回复。 如果有回复,就会拼接到children字段中。
在查找回复的sql中,不能指定接收者的id(即comments表中的recipient_id),因为回复可能是楼宇之间的通信,如:
- A 评论[content]
- B 回复 A # 回复1
- C 回复 B # 回复2
- C 回复 C # 回复3
如果指定了接收用户id,可能会导致回复2和回复3都无法支付。
这里同样提高效率的是,需要重复获取用户名(或者联表),因为数据库设计使用user_id作为主键关联。
查询对用户收到的评论的回复
以查询用户收到的评论为例,解释一下SQL的逻辑。
首先需要在文章表中找到用户发布的文章id,然后根据文章id在评论表中查询收到的评论(不包括自己发表的评论),最后需要拼接发布者的用户名和文章标题。
sql:
SELECT ar.title,
co.publisher_name,
co.publisher_id,
co.article_commented_id,
co.content,
co.create_time,
co.is_reply
FROM
(SELECT us.user_name AS publisher_name,
uc.publisher_id,
uc.article_commented_id,
uc.content,
uc.create_time,
uc.is_reply
FROM
(SELECT c.publisher_id,
c.article_commented_id,
c.content,
c.create_time,
is_reply
FROM comments c, article a, user_info u
WHERE c.article_commented_id = a.id
AND a.author_id = u.user_id
AND u.user_id = ${uid}
AND is_reply is null
AND publisher_id ${uid}) AS uc, user_info us
WHERE uc.publisher_id = us.user_id ) AS co, article ar
WHERE co.article_commented_id = ar.id;
${uid} 是传入的参数。
这两个函数分别使用db()返回一个Promise,最后使用Promise.all()一起处理,拼接得到的结果返回给后端。
前端概述
前端是根据数据库设计的。 主要包括登录注册、个人主页、文章、站内留言、收藏等。
使用vue-cli脚手架来构建项目,主要使用ES6句型来编译代码。 使用vue-router进行路由管理,Less作为CSS预处理器,使用axios进行前端数据交互。
补充说明 登录 注册
登录采用非常狭窄(不科学)的形式:用户输入用户名和密码并向服务器验证正确性。 如果正确的话,信息就存储在localStorage中,权限也硬编码在用户信息中(作为数据库表的数组存在)。 这是考虑到作为一个博客后台管理系统,或者是带有一些社交属性(私信、评论)的系统,管理员的权限不需要和普通用户有特别的区别。 登录信息过期通过代码设置localStorage的有效期为7天。
后来我了解到,通过token和cookie进行处理是一种更高尚的形式。
文章列表显示
文章列表主要组件位于components/ArticleList下,其功能是显示文章列表。 选项包括:
该组件可以在文章列表(路由/表格)、个人收藏夹页面(/user/favorite)、用户个人文章(包括自己和访问者看到的文章:/user/index和/user/visitor)中使用。
该组件也是一个高度耦合的组件。 我写的时候没感觉。 现在看来...
文章排序
排序方式为:默认时间倒序、点赞数倒序、收藏数倒序、评论数倒序。 后三个前端返回一个文章id字段,该字段按照指定的方法进行倒序排序。
例如,[6, 9, 10, 1]形式的字段按照点赞数倒序返回,表示点赞数为6>9>10>1>其他,没有出现的文章点赞数为0,前端根据该字段进行交换排序:
/**
* @description:: 根据传入的index数据对array进行交换排序
* @param {Array} index
* @param {Array} array
* @return {*}
* @author: WoodenStone
*/
interchange (index, array) {
for (let i = 0; i < index.length; i++) {
if (array[i].id !== index[i].id) {
let temp = {}
for (let j = i + 1; j < array.length; j++) {
if (array[j].id === index[i].id) {
// 这里赋值要用$set 否则视图不会更新
temp = array[j]
this.$set(array, j, array[i])
this.$set(array, i, temp)
}
}
}
}
},
这种方法很原始,但暂时还没有想到什么通用且方便的方法。
文章标签
核心组件位于/src/components/Tags,主要功能是输入标签,按Enter键添加标签,按DELETE删除标签。 单个标签的字数和一篇文章可以拥有的最大标签数量都有限制。
评论回复
找了一些开源的轮子,但是没有找到满意的,最后自己实现了一个。 核心组件位于/src/components/Comment下,分为单个回复(ReplyItem)、单个评论(包括回复、CommentItem)和所有评论(CommentGroup),通过传入正确的数据即可显示到索引。 评论最多有两层树,效果如下:
评论回复采用了ElementUI中对话框组件的实现,利用开源wheel封装的v-dialogDrag命令,使对话框可拖动。 回复和评论中的“回复”按钮组装在CommentGroup中,也就是说,CommentItem和ReplyItem是兄弟。 这样做的原因是为了尽可能集中“回复”这个需要调用socket并传输数据的功能,这样就不需要使用$emit()等方法来传递参数了。 不过,从设计的角度来看,设计亲子关系可能更直观。
参考
vue-element-admin 的基本模板
写在最后
第一次写vue,也是第一次使用nodeJS。 后端完全快,但前端更专注。 学到了很多以前没见过的东西,对数据库的操作有了一定的了解。 现在又过去一个月了,回头看这样的代码,我觉得那里写得不好,没有正确使用axios拦截器和vuex。 不过也算是一个功能比较齐全的项目了,最终还是部署到了服务器上。 在网上体验了一下,感觉学到了很多东西。
也许以后有时间我会改一下,但是再改其实就是重画
发表评论