一般写博客的时候,每篇文章我们都可以加上一些标签。很多主页会显示所有的标签,花花绿绿的那种。我们在博客主页也预留了这个地方。但是后台却没有它的位置,在其他功能都差不多(功能永远做不完)的时候,我们就可以把这个功能拉进来制作了。
导入tokens库
javascript对付我们要的标签输入的库很多,我这里就拿此处的:
https://www.cssscript.com/tags-input-token-autocomplete/www.cssscript.com点击页面上的download按钮,会下载到一个zip包,打开。
工程中,在3rdParty目录下建立一个目录,全名tokenautocomplete。在硬盘上找到该目录的位置,把刚才打开的zip包下的built目录下的token-autocomplete.js拖入,再把zip包里lib目录下的token-autocomplete.css拖入。
这样我们就在工程里导入了制作tag的库。
修改newArticle.ftl
打开newArticle.ftl,在<head>里面,加入一行下面的内容,位置在</head>上面的唯一的link元素后一行:
<link href="3rdParty/tokenautocomplete/token-autocomplete.css" rel="stylesheet">
在文件尾部,那些原来的script元素中,sweetalert2后面一行,加入:
<script src="3rdParty/tokenautocomplete/token-autocomplete.js"></script>
好,这样我们就绑定了导入的库。
这个库需要操作一个div,作为输入tag的地方,所以我们在上面的元素里面,找到属于栏目的部分,在栏目的整体div下方,平级的地方,加入以下内容:
<div class="row">
<div class="form-group col-3">
<div class="row">
<div class="col">
<label>标签</label>
</div>
</div>
<div class="row">
<div class="col">
<div id="tags-container"></div>
</div>
</div>
</div>
</div>
这样模板文件就好了。
整体的代码:
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>后台首页</title>
<!-- JQuery -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!-- Popper -->
<script src="https://cdn.bootcdn.net/ajax/libs/popper.js/2.4.4/umd/popper.js"></script>
<!-- Bootstrap -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.5.0/js/bootstrap.min.js"></script>
<!-- Font Awesome -->
<script src="https://cdn.bootcdn.net/ajax/libs/font-awesome/5.13.0/js/solid.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/font-awesome/5.13.0/js/fontawesome.js"></script>
<link href="css/style.css" rel="stylesheet">
<link href="3rdParty/tokenautocomplete/token-autocomplete.css" rel="stylesheet">
<style>
.ck-editor__editable_inline {
min-height: 400px;
}
</style>
</head>
<body>
<div class="wrapper">
<!-- Sidebar -->
<#include "./sidebar.ftl">
<!-- Content -->
<div id="content">
<#include "./sidebartoggle.ftl">
<section style="padding: 5px">
<div class="card card-primary">
<div class="card-header">
<h4>新的文章<span id="article-id" value=""></span></h4>
</div>
<div class="card-body">
<div class="form-group">
<label>题目</label>
<input type="text" id="article-title" class="form-control">
</div>
<div class="form-group">
<label>文章内容</label>
<div id="article-editor"></div>
</div>
<div class="row">
<div class="form-group col-3">
<label>分类</label>
<select id="catlog-selection" class="form-control custom-select" onchange="">
<option selected="" disabled="">选择类型</option>
</select>
</div>
</div>
<div class="row">
<div class="form-group col-3">
<div class="row">
<div class="col">
<label>标签</label>
</div>
</div>
<div class="row">
<div class="col">
<div id="tags-container"></div>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="row">
<div class="col-12">
<button class="btn btn-secondary" onclick="saveArticle()">保存</button>
<button class="btn btn-success" onclick="">发布</button>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</body>
<script src="3rdParty/ckeditor/ckeditor.js"></script>
<script src="3rdParty/sweetalert2/sweetalert2.all.min.js"></script>
<script src="3rdParty/tokenautocomplete/token-autocomplete.js"></script>
<script src="javascript/sidebartoggle.js"></script>
<script src="javascript/newarticle.js"></script>
</html>
怎么使用呢?来,follow me!
修改newarticle.js
token-autocomplete库只需要我们告诉它,它操作的div是哪一个就可以。
打开newarticle.js,在ready那个回调方法最后面,加入下面的代码:
var tokenAutocomplete = new TokenAutocomplete({
name: 'tags-container',
selector: '#tags-container'
});
整体的read方法是这样:
$(document).ready(function() {
var data = {};
$.ajax({
type: 'post',
async: true,
data: JSON.stringify(data),
url: document.location.origin + '/api/getcatalog',
dataType:'json',
contentType: "application/json; charset=utf-8",
success: function(data) {
if(data && data.length) {
var root = $('#catlog-selection');
for (var i = 0; i < data.length; ++i) {
var c = data[i];
var s = '<option value="' + c.id + '">' + c.name + '</option>';
root.append(s);
}
}
},
error: function (xhr) {
Toast.fire({
type: 'error',
title: "Ajax 发生错误: " + xhr.responseText
});
}
});
var tokenAutocomplete = new TokenAutocomplete({
name: 'tags-container',
selector: '#tags-container'
});
});
运行起来测试吧!显示没有问题,输入几个标签,每个标签完成就敲一次回车,截图如图一:
标签能够输入了。剩下的时候我们就是看怎么在保存的时候把这些标签信息也保存下来?
保存
先说一下思路:跟栏目不同,栏目中的信息,是数据库中已经存在的,所以提交给服务器的时候,服务器只要判断信息是否有效(可能浏览器信息会被劫持之类的,导致丢上来垃圾信息)即可。
但这里的标签不同,我们特意设计成用户直接输入,然后提交。这些数据可能在数据库中已经存在,可能还没有。
那么,服务器就需要能够通过名字,要么绑定已经存在的标签,要么先创建本来没有的标签,然后再绑定。
思路理清,直接实践!
1、工程中,目录Model下面,新建java类文件,全名Tag.java。里面的代码如下:
package DefaultMain.Model;
import org.springframework.data.annotation.Id;
import javax.validation.constraints.NotBlank;
public class Tag {
@Id
private String id;
@NotBlank(message = "name cannot be empty")
private String name;
private String createTime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
}
一个典型的Model类,每个标签我们目前来说都是3个属性:id、名字和创建时间。没有什么特别需要说明的。
有个这个数据结构的基础,下面是对应这个数据结构的数据操作类。
2、工程Database目录下,新建java类文件,全名TagRepository,里面代码如下:
package DefaultMain.Database;
import DefaultMain.Model.Tag;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TagRepository extends MongoRepository<Tag, String> {
List<Tag> findByName(String name);
}
跟以前我们编写的Repository类不同,原来的ArticleRepository等类,里面的内容是空的。这里我们写入了一个方法,findByName。
原因是我们上传标签的需求,唯一的有效信息就是名字。所以我们希望底层有这么一个方法,给定名字,能帮我们查询是否已经存在。而默认的Repository方法中,是直接给定一个实例,所以不符合我们这个要求。对于这种情况,SpringBoot也有解决方案,就是findByxxx方式,这种方式严格要求命名方式。
比如上面的,name是tag中的一个字段名,要查找该字段的信息,就需要findBy开头,然后name用大写开头。具体的规则可以看各个版本的SpringBoot文档,我们这里用的是SpringBoot 2.2.0RELEASE版本,文档在这里:
Spring Data Commons - Reference Documentationdocs.spring.io3、工程Service目录下,新建java文件,全名TagService,里面内容:
package DefaultMain.Service;
import DefaultMain.Model.Tag;
import java.util.Collection;
import java.util.List;
public interface TagService {
public abstract Tag create(Tag tag);
public abstract Collection<Tag> read();
public abstract Tag update(Tag tag);
public abstract void delete(String id);
public abstract boolean exists(String name);
public abstract List<Tag> findByName(String name);
}
这个接口除了CRUD,还有exist方法和findByName方法。后面看实现:
4、工程Service目录下,新建java文件,全名TagServiceImpl,里面内容:
package DefaultMain.Service;
import DefaultMain.Database.TagRepository;
import DefaultMain.Model.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
@Service
public class TagServiceImpl implements TagService {
@Autowired
TagRepository tagRepository;
@Override
public Tag create(Tag tag) {
return tagRepository.insert(tag);
}
@Override
public Collection<Tag> read() {
return null;
}
@Override
public Tag update(Tag tag) {
return null;
}
@Override
public void delete(String id) {
}
@Override
public boolean exists(String name) {
List<Tag> tags = tagRepository.findByName(name);
return tags.size() > 0;
}
@Override
public List<Tag> findByName(String name) {
return tagRepository.findByName(name);
}
}
这部分代码,其他就不说了。exists()方法,和findByName()方法,都用了底层的、上面2中提到的findByName方法。这就是开始为什么我们设计了这么一个接口的原因。
这样支撑的数据和方法就有了。我们可以给原来的Article数据中加入标签了。
5、打开Article类文件,给里面添加标签变量和getter、setter,最后成品:
package DefaultMain.Model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.DBRef;
import javax.validation.constraints.NotBlank;
import java.util.List;
public class Article {
@Id
private String id;
@NotBlank(message = "title cannot be empty")
private String title;
@NotBlank(message = "content cannot be empty")
private String content;
@DBRef
private Catalog catalog;
@DBRef
private List<Tag> tagList;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Catalog getCatalog() {
return catalog;
}
public void setCatalog(Catalog catalog) {
this.catalog = catalog;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public List<Tag> getTagList() {
return tagList;
}
public void setTagList(List<Tag> tagList) {
this.tagList = tagList;
}
}
文章的数据结构支撑也做完了。剩下就是在Api层改动代码,实现对tag的功能支撑了。
6、打开ArtileApi类文件,需要加入TagService,和对应的代码。最后成品:
package DefaultMain.API;
import DefaultMain.Model.Article;
import DefaultMain.Model.Tag;
import DefaultMain.Service.ArticleService;
import DefaultMain.Service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;
import java.util.List;
@RestController
public class ArticleApi {
@Autowired
private ArticleService articleService;
@Autowired
private TagService tagService;
@PostMapping("/api/savearticle")
public ResponseEntity<?> saveArticle(@Valid @RequestBody Article article) {
Article archive;
for (int i = 0; i < article.getTagList().size(); ++i) {
String name = article.getTagList().get(i).getName();
if (tagService.exists(name)) {
//已经存在的tag,通过得到id绑定
List<Tag> tags = tagService.findByName(name);
article.getTagList().get(i).setId(tags.get(0).getId());
}
else {
//还不存在的tag,需要先创建一个,然后再绑定
Tag t = new Tag();
t.setId(null);
t.setName(name);
t = tagService.create(t);
article.getTagList().get(i).setId(t.getId());
}
}
if (article.getId().isEmpty()) {
//MongoRepository的API决定了,只有id是null的时候,才会返回给数据库的id给我们
article.setId(null);
archive = articleService.create(article);
}
else {
archive = articleService.update(article);
}
if (archive == null) {
return ResponseEntity.badRequest().build();
}
else {
URI uri = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(archive.getTitle())
.toUri();
return ResponseEntity.created(uri)
.body(archive);
}
}
}
加入的代码一个是TagService,依旧用了 @Autowired注解。另外一个部分是方法开头处的for循环,用户前端送上来的是tag的对象列表,所以一次处理每个对象:查看名字是否已经在tag数据库中存在,存在则想办法取到它的id,然后绑定到参数代表的article中。不存在的话就直接创建,并且拿到id,绑定。为什么非得这么做:因为SpringBoot的Mongo这一层,还没有做到能控制发现一个记录还不存在,就帮你先创建记录的功能。
到此,万事俱备,只差前端。别忘了,写了这么多代码,前端却还没有实现把标签对象列表上传的代码!
所以,最后一步!
7、打开newarticle.js,我们需要依次:
取到html tag对象的列表:
var tagList = $('#tags-container').find('.token-autocomplete-token');
判读一下这个列表是否有效:
if (tagList.length == 0) {
console.log("标签不能是空的");
return;
}
然后处理这个列表成数组形式:
var tags = [];
for (var i = 0; i < tagList.length; ++i) {
var tag = {};
tag.name = tagList[i].getAttribute('data-text');
tags.push(tag);
}
最后把这些数据和上传的data绑定:
var data = {'title': title, 'content': content, 'catalog': { 'id': catalog }, 'tagList': tags, 'id': id}
整体的saveArticle方法代码变成如下:
function saveArticle() {
var title = $('#article-title').val();
var catalog = $("#catlog-selection").val();
var content = window.editor.getData();
var id = $('#article-id').attr('value').trim();
var tagList = $('#tags-container').find('.token-autocomplete-token');
if (title == null || title.trim() == '') {
console.log("题目不能是空的");
return;
}
if (catalog == null || catalog.trim() == '') {
console.log("分类不能是空的");
return;
}
if (content == null || content.trim == '') {
console.log("内容不能是空的");
return;
}
if (tagList.length == 0) {
console.log("标签不能是空的");
return;
}
var tags = [];
for (var i = 0; i < tagList.length; ++i) {
var tag = {};
tag.name = tagList[i].getAttribute('data-text');
tags.push(tag);
}
var data = {'title': title, 'content': content, 'catalog': { 'id': catalog }, 'tagList': tags, 'id': id}
$.ajax({
type: 'post',
async: true,
data: JSON.stringify(data),
url: document.location.origin + '/api/savearticle',
dataType:'json',
contentType: "application/json; charset=utf-8",
success: function(data) {
Toast.fire({
type: 'success',
title: '保存成功'
});
if (id == "") {
$('#article-id').attr('value', data.id);
}
},
error: function (xhr) {
Toast.fire({
type: 'error',
title: "Ajax 发生错误: " + xhr.responseText
});
}
});
}
一切都完成了。运行起来测试,尝试给文章写入必要的内容,选择栏目,输入标签。然后保存。我这里的结果如下(Robo 3T界面),图二:
休息~