springboot博客(四)(后台管理之博客)
后台管理
博客管理
效果图
功能分析
- 博客展示、博客新增页面搭建
- 博客分页查询:根据标题/分类/是否推荐查询(异步交互)
- 博客的增加、修改、删除
- 增加、修改时:标题、内容、首图、描述不能为空
步骤
博客分页查询
创建BlogService/BlogServiceImpl/BlogRepository
注意条件查询的语法
BlogServiceImpl
@Service public class BlogServiceImpl implements BlogService { @Autowired private BlogRepository blogRepository; @Override public Blog getBlog(Long id) { return blogRepository.findOne(id); } @Override public Page<Blog> listBlog(Pageable pageable, Blog blog) { /* 此方法需要两个参数, 1:Specification:用于根据情况拼接条件 2:pageable:分页查询 */ return blogRepository.findAll(new Specification<Blog>() { @Override public Predicate toPredicate(Root<Blog> root,//将对象映射成root,从中获得表的字段,属性名 CriteriaQuery<?> cq,//封装条件的容器 CriteriaBuilder cb) {//条件表达式,从中可以获取like,==等 //在此方法中写条件 List<Predicate> predicates = new ArrayList<>();//初期装条件用 if(!"".equals(blog.getTitle())&&blog.getTitle()!=null){ //如果前端增加了标题title条件,则拼接这个条件 predicates.add(cb.like(root.<String>get("title"),"%"+blog.getTitle()+"%")); //cb.like():就是sql语句中的like //参数1:表达式(就是表字段),通过root获取 //参数2:值(就是前端传来的Blog中的值) //拼接之后的结果:title like '%xxx%' } if(blog.getType().getId()!=null){ //如果前端增加了分类type条件,则拼接这个条件 predicates.add(cb.equal(root.<Type>get("type").get("id"),blog.getType().getId())); } if(blog.isCommentabled()){ //isCommentabled()就是boolean的get方法,如果勾选则为true predicates.add(cb.equal(root.<Boolean>get("recommend"),blog.isCommentabled())); } cq.where(predicates.toArray(new Predicate[predicates.size()])); //cq.where:最终封装所有条件 //参数:数组,并指定长度 //拼接之后的结果:where title like '%xxx%' and ... return null; } },pageable); } @Override public Blog saveBlog(Blog blog) { return blogRepository.save(blog); } @Override public Blog updateBlog(Long id, Blog blog) { Blog b = blogRepository.findOne(id); if(b == null){ throw new NotFindException("修改的博客不存在!"); } BeanUtils.copyProperties(blog,b); return blogRepository.save(b); } @Override public void deleteBlog(Long id) { blogRepository.delete(id); } }
BlogRepository
/* 继承关系 JpaRepository<Blog,Long>:用于使用Jpa语句 JpaSpecificationExecutor<Blog>:用于复杂条件查询,根据情况拼接条件 */ public interface BlogRepository extends JpaRepository<Blog,Long>, JpaSpecificationExecutor<Blog> { }
编写controller
进入Blog的方法:blogs(Pageable pageable,Blog blog,Model model)
@Controller @RequestMapping("/admin") public class BlogController { @Autowired private BlogService blogService; @GetMapping("/blogs") public String blogs(@PageableDefault(size = 2,sort = {"updateTime"},direction = Sort.Direction.DESC) Pageable pageable,//分页 Blog blog, //用于接收前端的blog数据 Model model){//用于存blog model.addAttribute("page",blogService.listBlog(pageable, blog)); return "/admin/blogs"; } }
修改前端页面
blogs.html
前端迭代取值,编辑删除按钮
<tr th:each="blog,iterStat : ${page.content}"> <td th:text="${iterStat.count}">1</td> <td th:text="${blog.title}">刻意练习清单</td> <td th="${blog.type.name}">认知升级</td> <td th="${blog.recommend}?'是':'否'">是</td> <td th="${blog.updateTime}">2017-10-02 09:45</td> <td> <a href="#" th:href="@{/admin/blogs/{id}/input(id=${blog.id})}" class="ui mini teal basic button">编辑</a> <a href="#" th:href="@{/admin/blogs/{id}/delete(id=${blog.id})}" class="ui mini red basic button">删除</a> </td>
上下一页(因为需要获取表单的数据一起提交,所以需要将上下一页换成onclick,当作表单提交):隐藏域,传递page,onclick,attr属性自定义属性,script方法将值绑定到隐藏域中,一起提交。
<input type="hidden" name="page"> <a onclick="page(this)" th:attr="data-page=${page.number}-1" class=" item" th:unless="${page.first}">上一页</a> <a onclick="page(this)" th:attr="data-page=${page.number}+1" class=" item" th:unless="${page.last}">下一页</a> //上下一页给hidden赋值 function page(obj) { $("[name='page']").val($(obj).data("page")); loaddata();<!--这里是第四步添加的--> }
异步提交(提交controller只刷新局部页面,需要thymeleaf片段刷新+ajax异步刷新)
实现功能:点击搜索/上下一页,只刷新博客列表的table
方法:
- 给table设置一个fragment片段
- 点击搜索/上下一页通过ajax发送请求给controller
- controller处理数据,返回一个片段,实现局部刷新
给table设置一个fragment片段
<table th:fragment="blogList" class="ui compact teal table">
增加controller方法,跳转片段
@PostMapping("/blogs/search")//post请求 public String search(@PageableDefault(size = 2,sort = {"updateTime"},direction = Sort.Direction.DESC) Pageable pageable,//分页 BlogQuery blog, //用于接收前端的blog数据 Model model){//用于存blog model.addAttribute("page",blogService.listBlog(pageable, blog)); return "/admin/blogs :: blogList";//片段 }
ajax异步刷新方法
function loaddata() { $("#table-container").load(/*[[@{admin/blogs/search}]]*/"/admin/blogs/search",{ title : $("[name='title']").val(), typeId : $("[name='typeId']").val(), recommend : $("[name='recommend']").prop('checked'), page : $("[name='page']").val() }); }
增加异步刷新方法的div,修改hidden隐藏域type为typeId
<div class="ui container"> <!--把表单包裹里面-->
page方法增加ajax方法
loaddata();
form表单改为div,search按钮变为button,通过jquery方法提交
<div class="ui secondary segment form"> <button type="button" id="search-btn" class="ui mini teal basic button"><i class="search icon"></i>搜索</button> $("#search-btn").click(function () { loaddata(); });
显示分类下拉菜单
在controller中使用typeService查询出所有分类
需要先定义查询所有分类的方法,不需要分页查询
public List<Type> listType() { return typeRepository.findAll(); }
存入model中
//在controller的blogs方法中添加,即第一次访问时查询分类 //保存type下拉菜单数据 model.addAttribute("types",typeService.listType());
前端渲染:
遍历赋值,data-value,text
<div th:each="type : ${types}" class="item" data-value="1" th:data-value="${type.id}" th:text="${type.name}">错误日志</div>
解决空指针(发现blog.gettype()是空指针)
新建一个包:vo,创建一个实体类BlogQuery用于封装查询信息
public class BlogQuery { private String title; private Long typeId; private boolean recommend;
修改controller、service的Blog换位BlogQuery类型
@GetMapping("/blogs") public String blogs(@PageableDefault(size = 2,sort = {"updateTime"},direction = Sort.Direction.DESC) Pageable pageable, BlogQuery blog, //这里从Blog对象换位了BlogQuery,专门存前端传过来的条件 Model model){ ...
判断条件改为查询对象中获取typeid
if(blog.getTypeId()!=null){//这里换成从blogQuery判断,不会出现空指针 //如果前端增加了分类type条件,则拼接这个条件 predicates.add(cb.equal(root.<Type>get("type").get("id"),blog.getTypeId())); }
博客添加
修改资源引入
将抽取的片段资源引入加入editmd
检查blogs-input.html是否引入正确
<link rel="stylesheet" href="../../static/lib/editormd/css/editormd.min.css" th:href="@{/lib/editormd/css/editormd.min.css}"> <script src="../../static/lib/editormd/editormd.min.js" th:src="@{/lib/editormd/editormd.min.js}"></script>
检查修改blogs-input.html中的表单name值是否与实体类匹配
<input type="hidden" name="type.id"> <input type="hidden" name="tagIds"> <input type="text" name="firstPicture" placeholder="首图引用地址"> <input type="checkbox" id="shareStatement" name="shareStatement" class="hidden"> <input type="checkbox" id="commentabled" name="commentabled" class="hidden">
保存、发布按钮的处理(将published值也传入表单中提交)
增加隐含域published
保存发布按钮修改
<button type="button" id="save-btn" class="ui secondary button">保存</button> <button type="button" id="publish-btn" class="ui teal button">发布</button>
修改form表单
<form id="blog-form" action="#" th:action="@{/admin/blogs}" method="post" class="ui form">
增加jquery方法提交表单
//保存按钮 $('#save-btn').click(function () { $('[name="published"]').val(false); $('#blog-form').submit(); }); //提交按钮 $('#publish-btn').click(function () { $('[name="published"]').val(true); $('#blog-form').submit(); })
前端非空校验
// 标题、内容、分类、首图非空 $('.ui.form').form({ fields : { title : { identifier: 'title', rules: [{ type : 'empty', prompt: '标题:请输入博客标题' }] }, content : { identifier: 'content', rules: [{ type : 'empty', prompt: '标题:请输入博客内容' }] }, typeId : { identifier: 'typeId', rules: [{ type : 'empty', prompt: '标题:请输入博客分类' }] }, firstPicture : { identifier: 'firstPicture', rules: [{ type : 'empty', prompt: '标题:请输入博客首图' }] } } });
跳转新增页面方法
优化return,
static final
private static final String INPUT = "/admin/blogs-input"; private static final String LIST = "/admin/blogs"; private static final String REDIRECT_LIST = "redirect:/admin/blogs"; public String blogs(...){ ... return LIST; }
controller定义方法
@GetMapping("/blogs/input") public String input(Model model){ model.addAttribute("blog",new Blog());//传入一个空的blog,避免新增、修改并用页面中的${blog}报空指针 //保存type下拉菜单数据 model.addAttribute("types",typeService.listType()); //保存tag下拉菜单数据 model.addAttribute("tags",tagService.listTag()); return INPUT; }
增加查询所有tag的方法
public List<Tag> listTag() { return tagRepository.findAll(); }
前端遍历分类、标签
<div th:each="type : ${types}" class="item" data-value="1" th:data-value="${type.id}" th:text="${type.name}">错误日志</div> <div th:each="tag : ${tags}" class="item" data-value="1" th:data-value="${tag.id}" th:text="${tag.name}">java</div>
前端blogs.html增加href属性,指向跳转方法
<a href="#" th:href="@{/admin/blogs/input}" class="ui mini right floated teal basic button">新增</a>
修改jquery的md编辑器的路径
$(function() { contentEditor = editormd("md-content", { width : "100%", height : 640, syncScrolling : "single", //path : "../../static/lib/editormd/lib/" path : "/lib/editormd/lib/" }); });
修改md编辑器的宽度
<div class="m-container m-padded-tb-big">
新增功能
controller新增方法
获取session中的user对象,存入blog对象中
设置type初始值到blog对象中
设置tag初始值到blog对象中
service增加查询tags的方法
前端传过来的是一个ids的标签id字符串,1,2,3。
将字符串按照,分割成数组,再遍历数组插入到集合当中
在blog实体类增加属性tagIds
- 只是一个属性值,不和数据库一一映射,@Transient
- set/get
保存
增加消息提示
@PostMapping("/blogs") public String post(Blog blog, HttpSession session, RedirectAttributes attributes){ blog.setUser((User) session.getAttribute("user"));//从session中将user传入blog blog.setType(typeService.getType(blog.getType().getId()));//从增加条件中的id获取真正type对象传入blog blog.setTags(tagService.listTag(blog.getIds()));//从增加条件中的ids获取tag对象传入blog Blog blog1 = blogService.saveBlog(blog);//保存方法 //增加消息提示 if(blog1!=null){ attributes.addFlashAttribute("message","增加成功!"); }else{ attributes.addFlashAttribute("message","增加失败!"); } return REDIRECT_LIST; }
Service保存方法
- 设置初始值
- createTime、updateTime、views
public Blog saveBlog(Blog blog) { //初始化值 blog.setCreateTime(new Date()); blog.setUpdateTime(new Date()); blog.setViews(0); return blogRepository.save(blog); }
- 设置初始值
service保存、更新、删除放在事务里
- @transactional
前端增加消息提示
增加提示框的div
```html