⭐ 作者简介:码上言
⭐ 代表教程:Spring Boot + vue-element 开发个人博客项目实战教程
项目部署视频
https://www.bilibili.com/video/BV1sg4y1A7Kv/?vd_source=dc7bf298d3c608d281c16239b3f5167b
文章目录
前言
我们接着上一篇的来写,现在就剩下文章发布的功能了,但这个是最重要的部分,大家有什么问题欢迎随时留言或者私信我,或加我好友都可以。目录的话我就接着上一篇的来写了,不再重新写目录了,大家可以两篇合起来一起来看。
3.4、编写添加文章页面弹出层
3.4.1、添加标签和分类查询接口
在/src/api
文件下找到category.js
接口,我们将刚才新添加的分类查询接口加入进来
export function getCategory(data) { return request({ url: '/category/getCategory', method: 'post', data }) }
在/src/api
文件下找到tag.js
接口,我们将刚才新添加的标签查询接口加入进来
export function getTag(data) { return request({ url: '/tag/selectTag', method: 'post', data }) }
3.4.2、添加文章分类功能
文章分类功能是在我们弹出框里,为了方便大家学习,我现将业务逻辑分析一下,然后再来写具体的代码,这样就能清晰的分析代码,进而读懂代码了。
文章分类业务分析:
首先我们写完文章之后,点击发布文章,然后跳出一个弹出框,需要我们填写文章的一些属性,我们先重点来说分类,然后标签就和这类似了。上图是我点击发布文章之后弹出的,我们先设置一个添加分类的按钮,这时我们会触发查询分类的接口,将数据库中的所有分类全部查询出来。然后我们可以进行搜索或者自定义分类,使用了vue的el-autocomplete搜索框来实现。
搜索:当我们在输入框输入数据时,就会请求接口进行条件查询。
自定义:直接在输入框中输入完分类的名称之后,直接回车即可。
接下来我们用代码进行分析。
这里主要是分析弹出框里面的内容。
当你选择完分类之后,会在页面上展示出来分类的名称。
removeCategory() { this.article.categoryName = null; },
以下是选择分类的功能模块
el-popover 是ElementUI封装的一个弹窗组件,类似于el-tooltip,弹窗中也可以自定义内容等。
autocomplete 是一个可带输入建议的输入框组件,具体官方文档:https://element.eleme.cn/#/zh-CN/component/input
<!-- 分类选项 --> <el-popover placement="bottom-start" width="460" trigger="click" v-if="!article.categoryName"> <div class="popover-title">分类</div> <!-- 搜索框 --> <el-autocomplete style="width:100%" v-model="categoryName" :fetch-suggestions="findCategories" placeholder="请输入分类名搜索,如果自定义分类,输入完成之后直接回车即可!" :trigger-on-focus="false" @keyup.enter.native="saveCategory" @select="handleFindCategories" > <template slot-scope="{ item }"> <div>{{ item.categoryName }}</div> </template> </el-autocomplete> <!-- 分类数据展示 --> <div class="popover-container"> <div class="category-item" v-for="item of categoryList" :key="item.id" @click="addCategory(item)"> {{ item.categoryName }} </div> </div> <el-button type="success" plain slot="reference" size="small"> 添加分类 </el-button> </el-popover>
其余的也没什么好说的,大家看一下应该都能看懂,之前也讲过一部分,比如分类数据展示,就是个list数据的展示等操作。
js的部分主要实现一些方法的操作,这里我们还引入了分类的接口。后边还会引入标签的接口。
import { addArticle, updateArticle, getArticleById } from '@/api/article' import { getCategory } from '@/api/category' data() { return { showDialog: false, categoryName: "", categoryList: [], article: { id: "", title: "", categoryId: "", content: "", categoryName: null } } }, methods: { // 打开文章信息填写框 openDialog() { if (this.article.title.trim() == "") { this.$message.error("文章标题为空,请填写文章标题"); return false; } if (this.article.content.trim() == "") { this.$message.error("文章内容为空,请填写文章内容"); return false; } this.getCategoriesList(); this.showDialog = true; }, //------分类的业务处理开始------ getCategoriesList() { var categoryName = ""; getCategory({categoryName}).then(response => { this.categoryList = response.data; }) }, removeCategory() { this.article.categoryName = null; }, //搜索分类名称 findCategories(categoryName, cb) { getCategory({categoryName}).then(response => { cb(response.data); }) }, saveCategory() { if (this.categoryName.trim() != "") { this.addCategory({ categoryName: this.categoryName }); this.categoryName = ""; } }, addCategory(item) { this.article.categoryName = item.categoryName; }, handleFindCategories(item) { this.addCategory({ categoryName: item.categoryName }); }, //------分类的业务处理结束------ handleSubmit() { this.showDialog = true; var body = this.article; }, handleCancel() { this.showDialog = false; }, } //css .popover-title { margin-bottom: 1rem; text-align: center; } .category-item { cursor: pointer; padding: 0.6rem 0.5rem; } .category-item:hover { background-color: #f0f9eb; color: #67c23a; }
具体的代码我会将放在这一节的最后展示,大家先进行学习,有错误了再去对照完整的代码看一下。
3.4.3、添加文章标签功能
标签的功能和分类的功能差不多,大家可以先自己参照分类的思路先写,然后再接着往下看我写的作为参考,这样自己就能再脑子里回顾一下写的思路,以后写项目遇到类似的功能就能非常清楚的想起思路。这里我先将标签的主要代码列举出来,基本上和分类的一致,但是分类只有一个,标签有多个,前端限制的每一篇文章最多只有三个
<!-- ----------文章标签开始---------- --> <el-form-item label="文章标签"> <el-tag v-for="(item, index) of article.tagNameList" :key="index" style="margin:0 1rem 0 0" :closable="true" @close="removeTag(item)" > {{ item }} </el-tag> <!-- 标签选项 --> <el-popover placement="bottom-start" width="460" trigger="click" v-if="article.tagNameList.length < 3" > <div class="popover-title">标签</div> <!-- 标签搜索框 --> <el-autocomplete style="width:100%" v-model="tagName" :fetch-suggestions="findTags" placeholder="请输入标签名搜索,按回车可添加自定义标签" :trigger-on-focus="false" @keyup.enter.native="saveTag" @select="handleFindTag" > <template slot-scope="{ item }"> <div>{{ item.tagName }}</div> </template> </el-autocomplete> <!-- 标签数据展示 --> <div class="popover-container"> <div style="margin-bottom:1rem">添加标签</div> <el-tag v-for="(item, index) of tagList" :key="index" :class="tagClass(item)" @click.native="addTag(item)" > {{ item.tagName }} </el-tag> </div> <el-button type="primary" plain slot="reference" size="small"> 添加标签 </el-button> </el-popover> </el-form-item> <!-- ----------文章标签结束---------- -->
JS部分:
import { getTag } from '@/api/tag' //------标签的业务处理------ getTagsList() { var tagName = ""; getTag({tagName}).then(response => { this.tagList = response.data; }) }, //搜索标签名称 findTags(tagName, cb) { getTag({tagName}).then(response => { cb(response.data); }) }, handleFindTag(item) { this.addTag({ tagName: item.tagName }); }, saveTag() { if (this.tagName.trim() != "") { this.addTag({ tagName: this.tagName }); this.tagName = ""; } }, addTag(item) { console.log("标签展示:",item); if (this.article.tagNameList.indexOf(item.tagName) == -1) { this.article.tagNameList.push(item.tagName); } }, removeTag(item) { const index = this.article.tagNameList.indexOf(item); this.article.tagNameList.splice(index, 1); }, //------标签的业务处理结束------ //放到methods方法外层 computed: { tagClass() { return function(item) { const index = this.article.tagNameList.indexOf(item.tagName); return index != -1 ? "tag-item-select" : "tag-item"; }; } }
这里我只说一个知识点:
问题:vue中@click和@click.native.prevent的区别是什么?
@click是用在按钮上的语法
@click.native是给vue组件绑定事件时候,必须加上native ,否则会认为监听的是来自Item组件自定义的事件,prevent是用来阻止默认的事件。就相当于event.preventDefault(),父组件想在子组件上监听自己的click的话,需要加上native修饰符。
3.4.4、文章摘要
文章摘要也就是对文章的大体描述功能,这个就是个表单,没有难点。
<el-form-item label="文章摘要"> <el-input type="textarea" :rows="2" placeholder="请输入内容" v-model="article.description" style="width:220px" /> </el-form-item> return { showDialog: false, categoryName: "", categoryList: [], tagName: "", tagList: [], article: { id: "", title: "", categoryId: "", content: "", categoryName: null, tagNameList: [], description: "" } }
3.4.5、文章封面上传
文章封面上传功能是一个新的知识点,我们后端要添加新的接口,上传图片的接口,这里就涉及到文件的上传,保存路径等操作,这也是在以后的工作中经常使用的文件操作,这个也是一个重点,希望大家认真对待学习。我们先去写后端图片上传的功能,图片的存储等操作。希望大家能了解图片上传的业务逻辑思路。
1、后端功能
和之前的业务流程一样,我们先写一个图片上传的接口,打开ArticleService.java
/** * 上传文件 * * @param file * @return */ String uploadFile(MultipartFile file);
这里说一下MultipartFile对象,有些同学可能没有学过或不了解,我这里简单的说明一下。
使用MultipartFile这个类主要是来实现以表单的形式进行文件上传功能。首先MultipartFile是一个接口,并继承自InputStreamSource,且在InputStreamSource接口中封装了getInputStream方法,该方法的返回类型为InputStream类型,这也就是为什么MultipartFile文件可以转换为输入流。通过以下代码即可将MultipartFile格式的文件转换为输入流。
这个MultipartFile有一些常用的方法。
getName
getName方法获取的是前后端约定的传入文件的参数的名称,在SpringBoot后台中则是通过@Param("uploadFile")
注解定义的内容。getOriginalFileName
getOriginalFileName方法获取的是文件的完整名称,包括文件名称+文件拓展名。getContentType
getContentType方法获取的是文件的类型,注意是文件的类型,不是文件的拓展名。getSize
getSize方法用来获取文件的大小,单位是字节。getInputStream
getInputStream方法用来将文件转换成输入流的形式来传输文件,会抛出IOException异常。
还有一些其他的方法,大家可以自己去查找,以上这些我们会经常用到。
接口有了,我们再去写实现方法,打开ArticleServiceImpl.java
实现类。
通过上边对MultipartFile的解释,下面我们就使用到了。
@Override public String uploadFile(MultipartFile file) { try { // 获取文件md5值 String md5 = FileUtils.getMd5(file.getInputStream()); // 获取文件扩展名 String extName = FileUtils.getExtName(file.getOriginalFilename()); // 重新生成文件名 String fileName = md5 + extName; // 判断文件是否已存在 if (!exists(ARTICLE + fileName)) { // 不存在则继续上传 upload(ARTICLE, fileName, file.getInputStream()); } // 返回文件访问路径 return getFileAccessUrl(ARTICLE + fileName); } catch (Exception e) { e.printStackTrace(); log.error("文件上传失败"); } return null; }
1.首先我们拿到上传的图片之后,我们先进行MD5然后拿到文件的MD5值进行图片文件名重新命名,为的就是防止文件名重复,被覆盖,就会导致图片数据丢失。
2.然后通过getOriginalFilename获取图片的后缀格式。
3.拼接起来组成新的图片名称。
4.为了防止丢失数据,再次进行图片名称判断,有就直接返回地址,没有则继续上传图片或文件。
以上会用到几个方法和工具类如下:
FileUtils工具类:
package com.blog.personalblog.util; import cn.hutool.core.util.StrUtil; import lombok.extern.log4j.Log4j2; import org.apache.commons.codec.binary.Hex; import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; /** * 文件md5工具类 * */ @Log4j2 public class FileUtils { /** * 获取文件md5值 * * @param inputStream 文件输入流 * @return {@link String} 文件md5值 */ public static String getMd5(InputStream inputStream) { try { MessageDigest md5 = MessageDigest.getInstance("md5"); byte[] buffer = new byte[8192]; int length; while ((length = inputStream.read(buffer)) != -1) { md5.update(buffer, 0, length); } return new String(Hex.encodeHex(md5.digest())); } catch (Exception e) { e.printStackTrace(); return null; } finally { try { if (inputStream != null) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } /** * 得到文件扩展名 * * @param fileName 文件名称 * @return {@link String} 文件后缀 */ public static String getExtName(String fileName) { if (StrUtil.isBlank(fileName)) { return ""; } return fileName.substring(fileName.lastIndexOf(".")); } }
exists方法:
本地路径需要在配置文件中配置,打开application.yml配置文件,然后配置如下:
upload: local: path: /blog/uploadFile/ url: http://localhost:9090/blog
/** * 本地路径 */ @Value("${upload.local.path}") private String localPath; /** * 访问url */ @Value("${upload.local.url}") private String localUrl; private static final String ARTICLE = "articles/"; /** * 判断文件是否存在 * * @param filePath 文件路径 * @return */ public Boolean exists(String filePath){ return new File(localPath + filePath).exists(); }
getFileAccessUrl方法:
/** * 获取文件访问url * * @param filePath 文件路径 * @return */ public String getFileAccessUrl(String filePath) { return localUrl + localPath + filePath; }
upload方法:
private void upload(String path, String fileName, InputStream inputStream) throws IOException { File directory = new File(localPath + path); if (!directory.exists()) { if (!directory.mkdirs()) { log.error("创建目录失败"); } } // 写入文件 File file = new File(localPath + path + fileName); if (file.createNewFile()) { BufferedInputStream bis = new BufferedInputStream(inputStream); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); byte[] bytes = new byte[1024]; int length; while ((length = bis.read(bytes)) != -1) { bos.write(bytes, 0, length); } bos.flush(); inputStream.close(); bis.close(); bos.close(); } }
然后我们在去写controller层接口,打开ArticleController.java
,这个就不要记日志了,我们日志那边没有做文件的处理,所以会报错,这里暂时不需要记日志。
/** * 上传网站logo封面 * @param file * @return 返回logo地址 */ @ApiOperation(value = "上传网站logo封面") @PostMapping("upload") public JsonResult<String> uploadImg(@RequestParam(value = "file") MultipartFile file) { String s = articleService.uploadFile(file); return JsonResult.success(s); }
好了,上传图片的功能已经实现,我们接下来用postman测试一下。这时我们请求接口会报500错误,我们再看一下后端有没有报错信息。果然也报错了,需要我们去设置一下文件上传的大小限制。我们打开application.yml
,在spring下面添加一下配置:
servlet: multipart: enabled: true max-file-size: 10MB max-request-size: 10MB
然后重启项目,我们再次请求接口。看到了吧,有数据返回,这个就是我们刚才上传图片的地址,我们去查看一下,这个目录会自动创建的,是你项目当前路径下的地方,假如你的项目在D盘,则这个图片地址就会在D盘下面。
看到了吧,我的项目就在D盘,所以这个文件就在D盘下。
接下来我们就要去实现前端的功能了,在这之前我们还要加一个图片拦截的操作,防止前端访问不到图片的操作。
新建一个配置类:MyInterceptorConfig.java
,我放在了config包
下。
package com.blog.personalblog.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * 这个是访问图片拦截的 * * @author: SuperMan * @create: 2022-08-20 **/ @Configuration public class MyInterceptorConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/blog/uploadFile/articles/**")//前端url访问的路径,若有访问前缀,在访问时添加即可,这里不需添加。 .addResourceLocations("file:/blog/uploadFile/articles/");//映射的服务器存放图片目录。 } }
还有一个再写前端之前,我们将上传图片的接口过滤掉,不受登录的限制,还有个一图片预览的地址也要放开,否则请求图片地址会报302重定向错误这个是一个坑。
打开ShiroConfiguration.java
,再新增两个配置。
filterChainDefinitionMap.put("/blog/uploadFile/articles/**","anon"); filterChainDefinitionMap.put("/article/upload", "anon");
接下来打开前端项目,我们来写前端上传图片的页面。
<el-form-item label="上传封面"> <el-upload class="upload-cover" drag action="null" :http-request="importFile" multiple :before-upload="handleUploadBefore" > <i class="el-icon-upload" v-if="article.imageUrl == ''" /> <div class="el-upload__text" v-if="article.imageUrl == ''"> 将文件拖到此处,或<em>点击上传</em> </div> <img v-else :src="article.imageUrl" width="360px" height="180px" /> </el-upload> </el-form-item>
上边是上传图片的组件el-upload,这里面有几个注意点,大家可以看一下element官方文档,这里给大家说一下,在开发的过程中一定要去看开发文档,里面有很多的组件设置的属性,还有具体的使用方法。
我原来想使用组件自带的action直接使用上传的地址,但是测试了一下会出现跨域的问题,我们这个项目是统一走的api那边的路由,所以我直接换成了自定义上传的方式,我感觉自定义上传是的代码会更加的清晰,自定义上传的地址就要使用到:http-request,这个文档也有说明。大家一定要看文档
我们需要写两个方法,一个是图片上传的方法,另一个是图片上传之前进行验证的操作。
以下是上传之前校验图片大小和格式
handleUploadBefore(file) { const isJPGORPNG = file.type === "image/jpeg" || file.type === "image/png"; const isLt10M = file.size / 1024 / 1024 < 10; if (!isJPGORPNG) { this.$message.error('上传图片只能是 JPG 或 PNG 格式!'); } if (!isLt10M) { this.$message.error('上传图片大小不能超过 10MB!'); } return isJPGORPNG && isLt10M; },
图片上传接口请求。
importFile(param){ let fd = new FormData(); fd.append("file", param.file); // 传文件 uploadImg(fd).then(res => { if (res.data && res.data.code === 200) { this.$message({ type: 'success', message: '图片上传成功!' }) } this.article.imageUrl = res.data; }) },
这个FormData实现form表单数据的序列化,将数据以键值对 name/value 的形式传到后台,从而减少表单元素的拼接,提高工作效率。
向FormData中添加新的属性值使用append,我们后端接收的参数就是file,所以我们添加一个file即可。
然后我们将上传图片的接口加入到接口中。在article.js接口中。
export function uploadImg(data) { return request({ url: '/article/upload', method: 'post', data }) }
别忘了在import中引入该接口。
好啦,这个上传的功能基本上实现了,我们测试一下,好啦,文章上传功能就这些,下面我们来写文章的发布功能。
3.4.6、保存草稿功能
这里需要修改一下文章的数据库,有一些字段需要修改,多的字段暂时就不删除了,不然就改动的很多。例如这里的浏览量可以用redis
去实现,而不是存入数据库中实现。
DROP TABLE IF EXISTS `person_article`; CREATE TABLE `person_article` ( `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', `author` VARCHAR(128) NOT NULL COMMENT '作者', `title` VARCHAR(255) NOT NULL COMMENT '文章标题', `user_id` INT(11) NOT NULL COMMENT '用户id', `category_id` INT(11) NULL COMMENT '分类id', `content` LONGTEXT NOT NULL COMMENT '文章内容', `views` BIGINT NOT NULL DEFAULT 0 COMMENT '文章浏览量', `total_words` BIGINT NOT NULL DEFAULT 0 COMMENT '文章总字数', `commentable_id` INT NULL COMMENT '评论id', `art_status` TINYINT NOT NULL DEFAULT 1 COMMENT '发布,默认1, 1-发布, 2-仅我可见 3-草稿', `description` VARCHAR(255) NULL COMMENT '描述', `image_url` VARCHAR(255) NULL COMMENT '文章logo', `create_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间' ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic COMMENT '文章管理表';
还有文章和标签的关联表数据库也要修改一下,把表的id删除掉,这个字段多余的。
DROP TABLE IF EXISTS `person_article_tag`; CREATE TABLE `person_article_tag` ( `tag_id` INT(11) NOT NULL COMMENT '标签id', `article_id` INT(11) NOT NULL COMMENT '文章id' ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic COMMENT '文章和标签关联表';
相对应的实体类和xml这两个文件要把id删除掉,这里我就不展示了,只删除id即可。
保存草稿功能相对于保存文章比较简单,不需要进入到弹出框之前就结束了这个功能,先找到我们之前写的保存草稿页面,点击功能需要完善即可。这里就修改了一下点击的方法名称。
<el-button type="warning" size="medium" @click="saveDraft" style="margin-left:10px" v-if="article.id === '' || article.artStatus == 3" >保存草稿</el-button>
接下来我们来实现saveDraft
方法的功能。
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: '保存草稿成功!' }); } else { this.$message({ type: 'error', message: '保存草稿失败!' }); } }) },
从以上代码可以看出,我先校验了文章的标题和内容,然后调用了添加文章的接口进行文章保存。
这里还要修改一下文章保存的接口,我们之前设计的不太合理,现在需要再完善一下。
首先添加一个文章保存的对象:ArticleInsertBO.java
package com.blog.personalblog.bo; import lombok.Data; /** * @author: SuperMan * @create: 2022-10-02 */ @Data public class ArticleInsertBO { /** * 文章id */ private Integer id; /** * 文章标题 */ private String title; /** * 分类id */ private Integer categoryId; /** * 文章内容 */ private String content; /** * 发布,默认1, 1-发布, 2-仅我可见 3-草稿 */ private Integer artStatus; /** * 描述 */ private String description; /** * 文章logo */ private String imageUrl; /** * 分类名称 */ private String categoryName; /** * 文章标签 */ private List<String> tagNameList; }
然后修改接口,这里我将修改个添加整合到一个接口中,可以减少重复的代码
/** * 新建文章 * @param bo * @return */ void insertOrUpdateArticle(ArticleInsertBO bo);
分类还要新加一个根据分类名称查询分类的接口以及实现方法,我这里就直接把代码列出来,相信大家对这操作也都比较熟练了。
/** * 获取分类 * @param categoryName * @return */ Category getCategoryByName(String categoryName);
@Override public Category getCategoryByName(String categoryName) { Category category = categoryMapper.getCategoryByName(categoryName); return category; }
CategoryMapper.java:
Category getCategoryByName(String categoryName);
<select id="getCategoryByName" resultMap="BaseResultMap"> select * from person_category where category_name = #{categoryName, jdbcType=VARCHAR} </select>
再将文章的发布形式单独提出来,写成一个枚举类,方便以后维护。
package com.blog.personalblog.common; import lombok.AllArgsConstructor; import lombok.Getter; /** * @author: SuperMan * @create: 2022-10-10 **/ @Getter @AllArgsConstructor public enum ArticleArtStatusEnum { /** * 发布 */ PUBLISH(1, "发布"), /** * 仅我可见 */ ONLYME(2, "仅我可见"), /** * 草稿 */ DRAFT(3, "草稿"); /** * 状态 */ private final Integer status; /** * 描述 */ private final String desc; }
然后我们来写文章保存或修改的实现类。
这一块的逻辑相比较之前的有了一些变化,希望大家好好看一下这个逻辑,如果看不明白可以留言,我这里不再讲述了。
@Resource private CategoryService categoryService; @Resource private UserService userService; @Override public void insertOrUpdateArticle(ArticleInsertBO bo) { //分类添加 Category category = saveCategory(bo); Article article = BeanUtil.copyProperties(bo, Article.class); if (category != null) { article.setCategoryId(category.getCategoryId()); } String username = (String) SecurityUtils.getSubject().getPrincipal(); User user = userService.getUserByUserName(username); article.setUserId(user.getId()); article.setAuthor(user.getUserName()); article.setViews(0L); article.setTotalWords(0L); if (bo.getId() != null) { articleMapper.updateArticle(article); } else { articleMapper.createArticle(article); } articleMap.put(article.getId(), article); //添加文章标签 saveTags(bo, article.getId()); //添加文章发送邮箱提醒 try { String content = "【{0}】您好:\n" + "您已成功发布了标题为: {1} 的文章 \n" + "请注意查收!\n"; MailInfo build = MailInfo.builder() .receiveMail(user.getEmail()) .content(MessageFormat.format(content, user.getUserName(), article.getTitle())) .title("文章发布") .build(); SendMailConfig.sendMail(build); } catch (Exception e) { log.error("邮件发送失败{}", e.getMessage()); } } private Category saveCategory(ArticleInsertBO bo) { if (StrUtil.isEmpty(bo.getCategoryName())) { return null; } Category category = categoryService.getCategoryByName(bo.getCategoryName()); if (category == null && !ArticleArtStatusEnum.DRAFT.getStatus().equals(bo.getArtStatus())) { category.setCategoryName(bo.getCategoryName()); categoryService.saveCategory(category); } return category; } private void saveTags(ArticleInsertBO bo, Integer articleId) { //首先判断是不是更新文章 if (bo.getId() == null) { articleTagService.deleteTag(bo.getId()); } //添加文章标签 List<String> tagNameList = bo.getTagNameList(); List<Integer> tagIdList = new ArrayList<>(); if (CollUtil.isNotEmpty(tagNameList)) { //先查看添加的标签数据库里有没有 for (String tagName : tagNameList) { Tag one = tagService.findByTagName(tagName); if (one == null) { Tag tag = new Tag(); tag.setTagName(tagName); tagService.saveTag(tag); tagIdList.add(tag.getId()); } else { tagIdList.add(one.getId()); } } } articleTagService.deleteTag(articleId); if (tagIdList != null) { List<ArticleTag> articleTagList = tagIdList.stream().map(tagId -> ArticleTag.builder() .tagId(tagId) .articleId(articleId) .build()).collect(Collectors.toList()); articleTagService.insertBatch(articleTagList); } }
好啦,后端修改完成了,别忘了文章的数据库表更新成最新修改的。接下来我们运行项目,启动后端项目,然后我们测试一下数据有没有成功。显示操作成功了,说明接口是通的,然后我们再去看一下数据库有没有这条数据。有数据,那我们的保存草稿功能就完成了。
3.4.7、文章的发布状态
文章的发布状态有三种:
- 公开
- 自己可见
- 草稿
<el-form-item label="发布形式"> <el-radio-group v-model="article.artStatus"> <el-radio :label="1">全部可见</el-radio> <el-radio :label="2">仅我可见</el-radio> </el-radio-group> </el-form-item>
3.4.8、文章发布
前面已经将铺垫都准备好了,接下来我们还要实现一个发布的功能,这个和保存草稿的功能基本类似。
handleSubmit() { this.showDialog = true; if (this.article.title.trim() == "") { this.$message.error("文章标题不能为空"); return false; } if (this.article.content.trim() == "") { this.$message.error("文章内容不能为空"); return false; } if (this.article.categoryName == null) { this.$message.error("文章分类不能为空"); return false; } if (this.article.tagNameList.length == 0) { this.$message.error("文章标签不能为空"); return false; } var body = this.article; addArticle(body).then(res => { if(res.code === 200) { this.$message({ type: 'success', message: '文章发表成功!' }); } else { this.$message({ type: 'error', message: '文章发表失败!' }); } }) },
写完之后,我们再测试一下正式发布文章。看到数据库有数据了,我们的文章发布功能就全部完成了。
这里我只将添加文章的页面全部代码列出来,其余的小东西比较多,我统一上传到了gitee上,大家可以下载下来去查看代码。
后端gitee地址:https://gitee.com/xyhwh/personal_blog
前端gitee地址:https://gitee.com/xyhwh/personal_vue