⭐ 作者简介:码上言
⭐ 代表教程:Spring Boot + vue-element 开发个人博客项目实战教程
⭐专栏内容:零基础学Java、个人博客系统
👦 学习讨论群:530826149
后端代码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
})
},
下面可以写数据展示的表格了,这个没什么好说的,直接上代码
<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上所需要的环境搭建,我会放到我的公众号上,这里不再写搭建的内容,这里征求一下意见,如果有需要我就写一下或者直接私信我,没有我就不再写了,这个项目就完结了。
上一篇:Spring Boot + vue-element 开发个人博客项目实战教程(二十三、文章管理页面开发(2))
下一篇:Spring Boot + vue-element 开发个人博客项目实战教程(二十五、项目完善及扩展(前端部分))