玩命加载中 . . .

springboot博客(四)(后台管理之博客)


springboot博客(四)(后台管理之博客)

后台管理

博客管理

效果图

功能分析

  1. 博客展示、博客新增页面搭建
  2. 博客分页查询:根据标题/分类/是否推荐查询(异步交互)
  3. 博客的增加、修改、删除
  4. 增加、修改时:标题、内容、首图、描述不能为空

步骤

博客分页查询
  1. 创建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> {
    }
    
  2. 编写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";
        }
    }
    
  3. 修改前端页面

    1. blogs.html

      1. 前端迭代取值,编辑删除按钮

        <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>
        
      2. 上下一页(因为需要获取表单的数据一起提交,所以需要将上下一页换成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();<!--这里是第四步添加的-->
            }
        
      3. 异步提交(提交controller只刷新局部页面,需要thymeleaf片段刷新+ajax异步刷新)

        实现功能:点击搜索/上下一页,只刷新博客列表的table

        方法:

        1. 给table设置一个fragment片段
        2. 点击搜索/上下一页通过ajax发送请求给controller
        3. controller处理数据,返回一个片段,实现局部刷新
        1. 给table设置一个fragment片段

          <table th:fragment="blogList" class="ui compact teal table">
          
        2. 增加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";//片段
          }
          
        3. 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()
              });
          }
          
        4. 增加异步刷新方法的div,修改hidden隐藏域type为typeId

          <div class="ui container">
              <!--把表单包裹里面-->
          
        5. page方法增加ajax方法

           loaddata();
          
        6. 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();
          });
          
      4. 显示分类下拉菜单

        1. 在controller中使用typeService查询出所有分类

          1. 需要先定义查询所有分类的方法,不需要分页查询

            public List<Type> listType() {
                return typeRepository.findAll();
            }
            
          2. 存入model中

            //在controller的blogs方法中添加,即第一次访问时查询分类
            //保存type下拉菜单数据
            model.addAttribute("types",typeService.listType());
            
        2. 前端渲染:

          1. 遍历赋值,data-value,text

            <div th:each="type : ${types}" class="item" data-value="1" th:data-value="${type.id}" th:text="${type.name}">错误日志</div>
            
        3. 解决空指针(发现blog.gettype()是空指针)

          1. 新建一个包:vo,创建一个实体类BlogQuery用于封装查询信息

            public class BlogQuery {
                private String title;
                private Long typeId;
                private boolean recommend;
            
          2. 修改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){
            ...
            
          3. 判断条件改为查询对象中获取typeid

            if(blog.getTypeId()!=null){//这里换成从blogQuery判断,不会出现空指针
                //如果前端增加了分类type条件,则拼接这个条件
                predicates.add(cb.equal(root.<Type>get("type").get("id"),blog.getTypeId()));
            }
            
博客添加
  1. 修改资源引入

    1. 将抽取的片段资源引入加入editmd

    2. 检查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>
      
  2. 检查修改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">
    
  3. 保存、发布按钮的处理(将published值也传入表单中提交)

    1. 增加隐含域published

    2. 保存发布按钮修改

      <button type="button" id="save-btn" class="ui secondary button">保存</button>
      <button type="button" id="publish-btn" class="ui teal button">发布</button>
      
    3. 修改form表单

      <form id="blog-form" action="#" th:action="@{/admin/blogs}" method="post" class="ui form">
      
    4. 增加jquery方法提交表单

      //保存按钮
      $('#save-btn').click(function () {
          $('[name="published"]').val(false);
          $('#blog-form').submit();
      });
      //提交按钮
      $('#publish-btn').click(function () {
          $('[name="published"]').val(true);
          $('#blog-form').submit();
      })
      
  4. 前端非空校验

    // 标题、内容、分类、首图非空
    $('.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: '标题:请输入博客首图'
              }]
            }
          }
        });
    
  5. 跳转新增页面方法

    1. 优化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;
      }
      
    2. 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;
      }
      
    3. 增加查询所有tag的方法

      public List<Tag> listTag() {
          return tagRepository.findAll();
      }
      
    4. 前端遍历分类、标签

      <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>
      
    5. 前端blogs.html增加href属性,指向跳转方法

      <a href="#" th:href="@{/admin/blogs/input}" class="ui mini right floated teal basic button">新增</a>
      
    6. 修改jquery的md编辑器的路径

      $(function() {
          contentEditor = editormd("md-content", {
              width   : "100%",
              height  : 640,
              syncScrolling : "single",
              //path    : "../../static/lib/editormd/lib/"
              path    : "/lib/editormd/lib/"
          });
      });
      
    7. 修改md编辑器的宽度

      <div  class="m-container m-padded-tb-big">
      
  6. 新增功能

    1. controller新增方法

      1. 获取session中的user对象,存入blog对象中

      2. 设置type初始值到blog对象中

      3. 设置tag初始值到blog对象中

        1. service增加查询tags的方法

          前端传过来的是一个ids的标签id字符串,1,2,3。

          将字符串按照,分割成数组,再遍历数组插入到集合当中

        2. 在blog实体类增加属性tagIds

          1. 只是一个属性值,不和数据库一一映射,@Transient
          2. set/get
      4. 保存

      5. 增加消息提示

       @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;
          }
      
    2. Service保存方法

      1. 设置初始值
        1. createTime、updateTime、views
      public Blog saveBlog(Blog blog) {
          //初始化值
          blog.setCreateTime(new Date());
          blog.setUpdateTime(new Date());
          blog.setViews(0);
          return blogRepository.save(blog);
      }
      
    3. service保存、更新、删除放在事务里

      1. @transactional
    4. 前端增加消息提示

      1. 增加提示框的div

        ```html


文章作者: 小苏
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小苏 !
评论
  目录