⭐ 作者简介:码上言
⭐ 代表教程:Spring Boot + vue-element 开发个人博客项目实战教程
⭐专栏内容:零基础学Java、个人博客系统
后端代码gitee地址:https://gitee.com/xyhwh/personal_blog
前端代码gitee地址:https://gitee.com/xyhwh/personal_vue
项目部署视频
https://www.bilibili.com/video/BV1sg4y1A7Kv/?vd_source=dc7bf298d3c608d281c16239b3f5167b
文章目录
前言
还是接着上边的来写,下面就剩下文章的展示一块了,马上这个系统就完结了,我看了一下即将写了一年,断断续续的。感谢大家的支持!
4、列表
这个列表相信大家已经写了很多遍了,现在可以说大概的思路应该掌握在手,这里我对后端又进行了处理,以前的bug也修复了一些,我这里先把后端改的代码来说一下。
4.1、功能修改
1、首先修改了根据分类id查找分类信息
这里的改动不大,主要是改动了查询的sql语句。
<select id="getById" resultMap="BaseResultMap"> select * from person_category where category_id = #{categoryId, jdbcType=INTEGER} </select>
2、修改文章查询的接口,这里改动的稍微有点大。
这边我把文章的标签给拆开了,减少了数据库查询的语句。
@Override @PostConstruct public void init() { List<Article> articleList = articleMapper.findAll(); try { getTagsOrCategory(articleList); for(Article article : articleList) { articleMap.put(article.getId(), article); } log.info("文章缓存数据加载完成"); } catch (Exception e) { log.error("文章缓存数据加载失败!", e.getMessage()); } } @Override public List<Article> getArticlePage(ArticleBO articleBO) { int pageNum = articleBO.getPageNum(); int pageSize = articleBO.getPageSize(); PageHelper.startPage(pageNum,pageSize); List<Article> articleList = articleMapper.getArticlePage(articleBO); getTagsOrCategory(articleList); return articleList; } public void getTagsOrCategory(List<Article> list) { if (list != null) { for (Article article : list) { //查询分类 Category category = categoryService.findById(article.getCategoryId()); if (category == null) { article.setCategoryName("无分类"); } else { article.setCategoryName(category.getCategoryName()); } List<Tag> tagList = new ArrayList<>(); List<ArticleTag> articleTags = articleTagService.findArticleTagById(article.getId()); if (articleTags != null) { for (ArticleTag articleTag : articleTags) { Tag tag = tagService.findTagById(articleTag.getTagId()); tagList.add(tag); } } article.setTagList(tagList); } } }
查询的接口如参要再包一层body
/** * 文章列表 * @param articleBO * @return */ @ApiOperation(value = "文章列表") @PostMapping("list") public JsonResult<Object> listPage(@RequestBody @Valid PageRequestApi<ArticleBO> articleBO) { List<Article> articleList = articleService.getArticlePage(articleBO.getBody()); PageInfo pageInfo = new PageInfo(articleList); PageRequest pageRequest = new PageRequest(); pageRequest.setPageNum(articleBO.getBody().getPageNum()); pageRequest.setPageSize(articleBO.getBody().getPageSize()); PageResult pageResult = PageUtil.getPageResult(pageRequest, pageInfo); return JsonResult.success(pageResult); }
ArticleBO.java
类:
package com.blog.personalblog.bo; import lombok.Data; /** * @author: SuperMan * @create: 2021-12-31 */ @Data public class ArticleBO { /** * 分类id */ private Integer categoryId; /** * 发布,默认0, 0-发布, 1-草稿 */ private Integer artStatus; /** * 文章标题 */ private String title; /** * 页码 */ private int pageNum; /** * 每页的数据条数 */ private int pageSize; }
大家对以上的代码应该都能看的懂,我们再去修改一下xml文件。以下只是删掉了对标签的查询。
<resultMap id="BaseResultMap" type="com.blog.personalblog.entity.Article"> <result column="id" jdbcType="INTEGER" property="id"/> <result column="author" jdbcType="VARCHAR" property="author"/> <result column="title" jdbcType="VARCHAR" property="title"/> <result column="user_id" jdbcType="INTEGER" property="userId"/> <result column="category_id" jdbcType="INTEGER" property="categoryId"/> <result column="content" jdbcType="VARCHAR" property="content"/> <result column="views" jdbcType="BIGINT" property="views"/> <result column="total_words" jdbcType="BIGINT" property="totalWords"/> <result column="commentable_id" jdbcType="INTEGER" property="commentableId"/> <result column="art_status" jdbcType="INTEGER" property="artStatus"/> <result column="description" jdbcType="VARCHAR" property="description"/> <result column="image_url" jdbcType="VARCHAR" property="imageUrl"/> <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime"/> <result column="categoryname" jdbcType="VARCHAR" property="categoryName"></result> </resultMap> <select id="getArticlePage" resultMap="BaseResultMap" parameterType="com.blog.personalblog.bo.ArticleBO"> SELECT a.*, u.category_name categoryname FROM person_article a left JOIN person_category u on a.category_id = u.category_id <where> <if test="articleBO.title != null"> and a.title like '%${articleBO.title}%' </if> <if test="articleBO.categoryId != null"> and a.category_id = #{articleBO.categoryId} </if> <if test="articleBO.artStatus != null"> and a.art_status = #{articleBO.artStatus} </if> </where> </select>
3、根据文章id查找文章
这个功能我根据前端的需要,重新写了一个返回类,用做编辑的时候数据回显的时候使用。
新增了了一个vo包,然后在包内新建一个ArticleVO.java
package com.blog.personalblog.vo; import com.blog.personalblog.entity.Tag; import lombok.Data; import java.time.LocalDateTime; import java.util.List; /** * @author: SuperMan * @create: 2022-10-10 **/ @Data public class ArticleVO { /** * 文章id */ private Integer id; /** * 作者 */ private String author; /** * 文章标题 */ private String title; /** * 用户id */ private Integer userId; /** * 分类id */ private Integer categoryId; /** * 文章内容 */ private String content; /** * 文章浏览量 */ private Long views; /** * 文章总字数 */ private Long totalWords; /** * 评论id */ private Integer commentableId; /** * 发布,默认1, 1-发布, 2-仅我可见 3-草稿 */ private Integer artStatus; /** * 描述 */ private String description; /** * 文章logo */ private String imageUrl; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新时间 */ private LocalDateTime updateTime; /** * 文章标签 */ private List<Tag> tagList; private List<String> tagNameList; /** * 分类名称 */ private String categoryName; }
然后修改查询的接口
/** * 根据文章id查找文章 * @param articleId * @return */ ArticleVO findById(Integer articleId);
实现类:
@Override public ArticleVO findById(Integer articleId) { Article article = articleMap.get(articleId); ArticleVO articleVO = BeanUtil.copyProperties(article, ArticleVO.class); List<String> tagNameList = new ArrayList<>(); if (articleVO != null) { if (articleVO.getTagList() != null) { for (Tag tag : articleVO.getTagList()) { tagNameList.add(tag.getTagName()); } } } articleVO.setTagNameList(tagNameList); articleVO.setCategoryName(article.getCategoryName()); return articleVO; }
接口也要修改一下返回类。
/** * 根据文章id查找 * @param id * @return */ @ApiOperation(value = "根据文章id查找") @GetMapping("/getArticle/{id}") @OperationLogSys(desc = "根据文章id查找", operationType = OperationType.SELECT) public JsonResult<Object> getArticleById(@PathVariable(value = "id") int id) { ArticleVO article = articleService.findById(id); return JsonResult.success(article); }
基本上就修改了这几个地方,接下来开始写文章列表的页面。
首先在article.js文件中修改一下根据id获取文章的接口地址。
export function getArticleById(id){ return request({ url: '/article/getArticle/' + id, method: 'get' }) }
还有之前我们点击发布文章之后,没有返回到列表页,现在我们先添加上这个功能,只要在之前写的方法里添加上跳转的地址即可。
添加这一句话:this.$router.push("/articles/list");
var body = this.article; addArticle(body).then(res => { if(res.code === 200) { this.$notify({ title: "文章发表成功", message: `文章《${this.article.title}》发表成功!`, type: "success", }); this.$router.push("/articles/list"); } else { this.$notify({ title: "文章发表失败", message: `文章《${this.article.title}》发表失败!`, type: "error", }); } this.showDialog = false; })
同时在发布草稿的方法里也要加上这一句。
// ------- 保存草稿 saveDraft() { this.article.artStatus = 3; if (this.article.title.trim() == "") { this.$message.error("文章标题不能为空"); return false; } if (this.article.content.trim() == "") { this.$message.error("文章内容不能为空"); return false; } var body = this.article; addArticle(body).then(res => { if(res.code === 200) { this.$message({ type: 'success', message: '保存草稿成功!' }); this.$router.push("/articles/list"); } else { this.$message({ type: 'error', message: '保存草稿失败!' }); } }) },
4.2、列表页面
页面和其他的列表差不多,这里我按照模块展示。
我先将return方法里的参数写出来。
data() { return { list: null, listLoading: true, count: 0, listQuery: { pageNum: 1, pageSize: 10, categoryId: null, artStatus: null, title: null }, categoryId: null, categoryList: [], tagId: null, tagList: [], title: null, typeList: [ { value: 1, label: "发布" }, { value: 2, label: "仅我可见" }, { value: 3, label: "草稿" } ], artStatus: null, views: null, totalWords: null, description: null } },
然后将接口导入进来
import { articleList, deleteArticle } from '@/api/article' import { getCategory } from '@/api/category' import { getTag } from '@/api/tag'
接着写页面功能
<!-- 设置标题文章管理 --> <div slot="header" class="clearfix"> <span>文章列表</span> </div>
4.2.1、分类查询
废话不多说直接上代码,完整代码再最后,可直接跳过看完整代码。
<!-- 文章分类 --> <el-select clearable size="small" v-model="categoryId" filterable placeholder="请选择分类" style="margin-right:1rem" > <el-option v-for="item in categoryList" :key="item.id" :label="item.categoryName" :value="item.categoryId" /> </el-select>
这里遍历了categoryList
,而这个list则是查询全部分类获取的,所以我们要写一个方法来查询分类,还要在页面初始的时候就要查询出来。接口还是那个添加文章的分类下拉的。
getCategoriesList() { var categoryName = ""; getCategory({categoryName}).then(response => { this.categoryList = response.data; }) },
现在是有值了,然后再初始化页面的时候就加载完成。
created() { this.getList(); this.getCategoriesList(); },
好啦,这时候可以看一下页面:
4.2.2、文章类型查询
接下来我们再继续写文章的类型查询。
<!-- 文章类型 --> <el-select clearable v-model="artStatus" placeholder="请选择文章类型" size="small" style="margin-right:1rem" > <el-option v-for="item in typeList" :key="item.value" :label="item.label" :value="item.value" /> </el-select>
typeList
这个数据就是data方法中的数组,在上边已经列出来了
typeList: [ { value: 1, label: "发布" }, { value: 2, label: "仅我可见" }, { value: 3, label: "草稿" } ],
同样也要在页面加载的时候一起加载数据,将方法也要放到created中
created() { this.getList(); this.getCategoriesList(); this.getTagsList(); },
4.2.3、文章标题查询
还有一个标题的搜索和点击搜索的按钮,当点击搜索的按钮触发查询的接口。
<!-- 文章名 --> <el-input clearable v-model="title" prefix-icon="el-icon-search" size="small" placeholder="请输入文章名" style="width:200px" @keyup.enter.native="searchArticles" /> <el-button type="primary" size="small" icon="el-icon-search" style="margin-left:1rem" @click="searchArticles" > 搜索 </el-button>
这里用到了一个方法:searchArticles
searchArticles() { this.getList(); },
这里面又调用了getList
方法
getList() { this.listLoading = true this.listQuery.categoryId = this.categoryId; this.listQuery.title = this.title; this.listQuery.artStatus = this.artStatus; var body = this.listQuery; articleList({body}).then(response => { this.list = response.data.result this.count = response.data.totalSize this.listLoading = false }) },
这里面又调用了getList
方法
getList() { this.listLoading = true this.listQuery.categoryId = this.categoryId; this.listQuery.title = this.title; this.listQuery.artStatus = this.artStatus; var body = this.listQuery; articleList({body}).then(response => { this.list = response.data.result this.count = response.data.totalSize this.listLoading = false }) },
下面可以写数据展示的表格了,这个没什么好说的,直接上代码
<el-table v-loading="listLoading" :data="list" fit highlight-current-row style="width: 98%; margin-top:30px;"> <el-table-column align="center" label="ID" > <template slot-scope="scope"> <span>{{ scope.row.id }}</span> </template> </el-table-column> <el-table-column label="文章封面" width="180" align="center"> <template slot-scope="scope"> <img class="article-cover" :src=" scope.row.imageUrl" /> </template> </el-table-column> <!-- 文章标题 --> <el-table-column prop="title" label="标题" align="center" /> <!-- 文章分类 --> <el-table-column prop="categoryName" label="分类" width="110" align="center"/> <!-- 文章标签 --> <el-table-column prop="tagList" label="标签" width="170" align="center"> <template slot-scope="scope"> <el-tag v-for="item of scope.row.tagList" :key="item.id" style="margin-right:0.2rem;margin-top:0.2rem" > {{ item.tagName }} </el-tag> </template> </el-table-column> <!-- 文章浏览量 --> <el-table-column prop="views" label="浏览量" width="70" align="center" > <template slot-scope="scope"> <span v-if="scope.row.views"> {{ scope.row.views }} </span> <span v-else>0</span> </template> </el-table-column> <!-- 文章总字数 --> <el-table-column prop="totalWords" label="总字数" width="70" align="center" > <template slot-scope="scope"> <span v-if="scope.row.totalWords"> {{ scope.row.totalWords }} </span> <span v-else>0</span> </template> </el-table-column> <!-- 文章描述 --> <el-table-column prop="description" label="描述" align="center" /> <el-table-column align="center" label="操作" width="180"> <template slot-scope="scope"> <el-button type="primary" size="mini" icon="el-icon-edit" @click="editArticle(scope.row.id)">编辑</el-button> <el-button type="danger" size="small" icon="el-icon-delete" @click="deleteArticleById(scope.row.id)" >删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination class="pagination-container" background @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="this.listQuery.pageNum" :page-size="this.listQuery.pageSize" :total="count" :page-sizes="[10, 20, 30]" layout="total, sizes, prev, pager, next, jumper" />
方法:
handleSizeChange(pageSize) { this.listQuery.pageSize = pageSize this.getList() }, handleCurrentChange(pageNum) { this.listQuery.pageNum = pageNum this.getList() },
以上就是展示的功能,下面我们来写删除和编辑的,这个就比较简单了,和之前的基本上一样。
5、删除
删除后端的接口传的参数格式做了修改,我记得没有写。这里我先写出来,大家如果没有更改就修改一下,如果修改了就过掉。
/** * 删除文章 * @return */ @ApiOperation(value = "删除文章") @DeleteMapping("/delete") @OperationLogSys(desc = "删除文章", operationType = OperationType.DELETE) public JsonResult<Object> articleDelete(@RequestParam(value = "id") int id) { articleService.deleteArticle(id); return JsonResult.success(); }
前端接口:
export function deleteArticle(id) { return request({ url: '/article/delete', method: 'delete', params: { id } }) }
删除的方法:
deleteArticleById (id) { this.$confirm('此操作将永久删除该文章, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { deleteArticle(id).then(response => { this.$message({ type: 'success', message: '删除成功!' }) this.getList() }).catch(() => { console.log('error') }) }).catch(() => { this.$message({ type: 'error', message: '你已经取消删除该文章!' }) }) },
这里没啥好说的,和之前的删除操作基本上一样。
6、修改功能
修改功能稍微做了一点的改变,我们看一下表格的操作栏的编辑按钮
<el-button type="primary" size="mini" icon="el-icon-edit" @click="editArticle(scope.row.id)">编辑</el-button>
点击时间绑定了一个方法,我们要传入文章的id
editArticle(id) { this.$router.push({ name: 'Addrticles', params: { id: id }}); },
这个和之前的公告差不多,只是将这个跳转提取到了方法内实现。
相对应的,在add页面中进行接收。
created() { const id = this.$route.params.id; if(id) { getArticleById(id).then((res) => { console.log(res.data) this.article = res.data; }); } },
这是编辑的功能也修改好了。看着还可以,大家可以自己美化一下页面,到这里文章的所有功能基本上全部完成了。
以下是列表的全部代码:
<template> <el-card class="box-card"> <!-- 设置标题文章管理 --> <div slot="header" class="clearfix"> <span>文章列表</span> </div> <!-- 文章按条件查找 --> <div style="margin-left:auto"> <!-- 文章分类 --> <el-select clearable size="small" v-model="categoryId" filterable placeholder="请选择分类" style="margin-right:1rem" > <el-option v-for="item in categoryList" :key="item.id" :label="item.categoryName" :value="item.categoryId" /> </el-select> <!-- 文章类型 --> <el-select clearable v-model="artStatus" placeholder="请选择文章类型" size="small" style="margin-right:1rem" > <el-option v-for="item in typeList" :key="item.value" :label="item.label" :value="item.value" /> </el-select> <!-- 文章名 --> <el-input clearable v-model="title" prefix-icon="el-icon-search" size="small" placeholder="请输入文章名" style="width:200px" @keyup.enter.native="searchArticles" /> <el-button type="primary" size="small" icon="el-icon-search" style="margin-left:1rem" @click="searchArticles" > 搜索 </el-button> </div> <el-table v-loading="listLoading" :data="list" fit highlight-current-row style="width: 98%; margin-top:30px;"> <el-table-column align="center" label="ID" > <template slot-scope="scope"> <span>{{ scope.row.id }}</span> </template> </el-table-column> <el-table-column label="文章封面" width="180" align="center"> <template slot-scope="scope"> <img class="article-cover" :src=" scope.row.imageUrl" /> </template> </el-table-column> <!-- 文章标题 --> <el-table-column prop="title" label="标题" align="center" /> <!-- 文章分类 --> <el-table-column prop="categoryName" label="分类" width="110" align="center"/> <!-- 文章标签 --> <el-table-column prop="tagList" label="标签" width="170" align="center"> <template slot-scope="scope"> <el-tag v-for="item of scope.row.tagList" :key="item.id" style="margin-right:0.2rem;margin-top:0.2rem" > {{ item.tagName }} </el-tag> </template> </el-table-column> <!-- 文章浏览量 --> <el-table-column prop="views" label="浏览量" width="70" align="center" > <template slot-scope="scope"> <span v-if="scope.row.views"> {{ scope.row.views }} </span> <span v-else>0</span> </template> </el-table-column> <!-- 文章总字数 --> <el-table-column prop="totalWords" label="总字数" width="70" align="center" > <template slot-scope="scope"> <span v-if="scope.row.totalWords"> {{ scope.row.totalWords }} </span> <span v-else>0</span> </template> </el-table-column> <!-- 文章描述 --> <el-table-column prop="description" label="描述" align="center" /> <el-table-column align="center" label="操作" width="180"> <template slot-scope="scope"> <el-button type="primary" size="mini" icon="el-icon-edit" @click="editArticle(scope.row.id)">编辑</el-button> <el-button type="danger" size="small" icon="el-icon-delete" @click="deleteArticleById(scope.row.id)" >删除</el-button> </template> </el-table-column> </el-table> <!-- 分页 --> <el-pagination class="pagination-container" background @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="this.listQuery.pageNum" :page-size="this.listQuery.pageSize" :total="count" :page-sizes="[10, 20, 30]" layout="total, sizes, prev, pager, next, jumper" /> </el-card> </template> <script> import { articleList, deleteArticle } from '@/api/article' import { getCategory } from '@/api/category' import { getTag } from '@/api/tag' export default { name: 'articleList', created() { this.getList(); this.getCategoriesList(); this.getTagsList(); }, data() { return { list: null, listLoading: true, count: 0, listQuery: { pageNum: 1, pageSize: 10, categoryId: null, artStatus: null, title: null }, categoryId: null, categoryList: [], tagId: null, tagList: [], title: null, typeList: [ { value: 1, label: "发布" }, { value: 2, label: "仅我可见" }, { value: 3, label: "草稿" } ], artStatus: null, views: null, totalWords: null, description: null } }, methods: { getList() { this.listLoading = true this.listQuery.categoryId = this.categoryId; this.listQuery.title = this.title; this.listQuery.artStatus = this.artStatus; var body = this.listQuery; articleList({body}).then(response => { this.list = response.data.result this.count = response.data.totalSize this.listLoading = false }) }, editArticle(id) { this.$router.push({ name: 'Addrticles', params: { id: id }}); }, getCategoriesList() { var categoryName = ""; getCategory({categoryName}).then(response => { this.categoryList = response.data; }) }, getTagsList() { var tagName = ""; getTag({tagName}).then(response => { this.tagList = response.data; }) }, searchArticles() { this.getList(); }, handleSizeChange(pageSize) { this.listQuery.pageSize = pageSize this.getList() }, handleCurrentChange(pageNum) { this.listQuery.pageNum = pageNum this.getList() }, deleteArticleById (id) { this.$confirm('此操作将永久删除该文章, 是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { deleteArticle(id).then(response => { this.$message({ type: 'success', message: '删除成功!' }) this.getList() }).catch(() => { console.log('error') }) }).catch(() => { this.$message({ type: 'error', message: '你已经取消删除该文章!' }) }) }, }, } </script> <style rel="stylesheet/scss" lang="scss" scoped> .pagination-container { float: right; margin-top: 1.25rem; margin-bottom: 1.25rem; } .box-card { width: 98%; margin: 1%; } .clearfix:before, .clearfix:after { display: table; content: ""; } .clearfix:after { clear: both } .clearfix span { font-weight: 600; } .article-cover { position: relative; width: 100%; height: 90px; border-radius: 4px; } .article-cover::after { content: ""; background: rgba(0, 0, 0, 0.3); position: absolute; top: 0; bottom: 0; left: 0; right: 0; } </style>
7、总结
目前为止我们这个全部的功能已经基本上实现了,现在我们接下来要美化一下我们的系统,大家也发现当登录进来的首页还是空的,登录页面也比较的单调丑陋,所以可能还需要一篇来扩展一下我们的系统,可能也是最后一篇了。关于项目的发布,不知道还要不要写,项目在linux上所需要的环境搭建,我会放到我的公众号上,这里不再写搭建的内容,这里征求一下意见,如果有需要我就写一下或者直接私信我,没有我就不再写了,这个项目就完结了。