elementui 后端排序-Vue+ElementUI+Express+MySQL搭建博客后台管理系统实践

大三只是想学点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。 不过也算是一个功能比较齐全的项目了,最终还是部署到了服务器上。 在网上体验了一下,感觉学到了很多东西。

也许以后有时间我会改一下,但是再改其实就是重画