博客管理分两个页面,一个编辑博客页面,一个列表展示页面。
编辑博客页面
整个页面分为几个模块,分别是选择博客创造类型是原创还是转载,博客内容,博客分类及标签,博客描述,博客图片地址(目前没有支持本地文件上传,并且只能选择一张作为展示图,博客内容中可以有多张图片),是否打开推荐/转载声明/赞赏/留言功能,保存草稿还是直接发布。
前端核心代码
HTML代码
<div class="m-container-big m-padded-tn-big">
<div class="ui container">
<form id="blog-form" action="#" th:object="${blog}" th:action="@{/admin/blogs}" method="POST" class="ui form">
<!-- 隐含域 获取是否发布 -->
<input type="hidden" name="published">
<!-- 隐含域 获取博客id -->
<input type="hidden" name="id" th:value="*{id}">
<!-- required 必须输入 -->
<div class="required field">
<div class="ui left labeled input">
<div class="ui selection compact teal basic dropdown label">
<input type="hidden" value="创作类型" name="flag" th:value="*{flag}">
<i class="dropdown icon"></i>
<div class=" text"> 创作类型</div>
<div class="ui menu">
<div class="item" data-value="原创">原创</div>
<div class="item" data-value="转载">转载</div>
<div class="item" data-value="翻译">翻译</div>
</div>
</div>
<input type="text" name="title" placeholder="标题" th:value="*{title}" >
</div>
</div>
<div class="required field">
<div id="md-content" style="z-index: 1 !important;">
<textarea name="content" placeholder="博客内容" style="display: none" th:text="*{content}">
[TOC]
#### Disabled options
- TeX (Based on KaTeX);
- Emoji;
- Task lists;
</textarea>
</div>
</div>
<div class="two fields">
<div class="required field">
<div class="ui left labeled action input">
<label class="ui compact teal basic label">分类</label>
<div class="ui fluid selection dropdown ">
<input type="hidden" name="type.id" th:value="*{type}!=null ? *{type.id}">
<i class="dropdown icon"></i>
<div class="default text">分类</div>
<div class="menu">
<div th:each="type : ${types}" th:data-value="${type.id}" th:text="${type.name}" class="item" >1</div>
</div>
</div>
</div>
</div>
<div class="field">
<div class="ui left labeled action input">
<label class="ui compact teal basic label">标签</label>
<div class="ui fluid selection multiple search dropdown ">
<input type="hidden" name="Tagids" th:value="*{Tagids}">
<i class="dropdown icon"></i>
<div class="default text">标签</div>
<div class=" menu">
<div th:each="tag : ${tags}" th:data-value="${tag.id}" th:text="${tag.name}" class="item" >1</div>
</div>
</div>
</div>
</div>
</div>
<div class="required field">
<textarea name="description" placeholder="博客描述....." th:text="*{description}">
</textarea>
</div>
<div class="required field">
<div class="ui left labeled input">
<label class="ui teal basic label">首图</label>
<input type="text" name="firstPicture" placeholder="首图引用地址" th:value="*{firstPicture}">
</div>
</div>
<div class="inline fields">
<div class="field">
<div class="ui checkbox" >
<input type="checkbox" id="recommende" name="recommende" th:checked="*{recommende}" checked class="hidden">
<label for="recommende">推荐</label>
</div>
</div>
<div class="field">
<div class="ui checkbox" >
<input type="checkbox" id="shareStatement" name="shareStatement" th:checked="*{shareStatement}" class="hidden">
<label for="shareStatement">转载声明</label>
</div>
</div>
<div class="field">
<div class="ui checkbox" >
<input type="checkbox" id="appreciation" name="appreciation" th:checked="*{appreciation}" class="hidden">
<label for="appreciation">赞赏</label>
</div>
</div>
<div class="field">
<div class="ui checkbox" >
<input type="checkbox" id="commentable" name="commentable" th:checked="*{commentable}" class="hidden">
<label for="commentable">留言</label>
</div>
</div>
</div>
<!-- 错误信息会显示在这里 -->
<div class="ui error message"> </div>
<div class="ui right aligned container">
<button type="button" class="ui button" onclick="window.history.go(-1)">返回</button>
<button type="button" id="save-btn" class="ui secondary button">保存</button>
<button type="button" id="publish-btn" class="ui red button">发布</button>
</div>
</form>
</div>
</div>
整个博客都是在一个form表单中,因为新增博客和编辑博客是同一个页面,所以需要通过判断id是否存在来确定是新增还是编辑。这里同样设置了隐含域。
js代码
$('#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', //和表单里的name值一致
rules: [{
type: 'empty', //非空验证
prompt: '请输入博客标题',
}]
},
content:{
identifier: 'content', //和表单里的name值一致
rules: [{
type: 'empty', //非空验证
prompt: '请输入博客内容',
}]
},
typeId:{
identifier: 'typeId', //和表单里的name值一致
rules: [{
type: 'empty', //非空验证
prompt: '请选择博客分类',
}]
},
firstPicture:{
identifier: 'firstPicture', //和表单里的name值一致
rules: [{
type: 'empty', //非空验证
prompt: '请选择博客首图',
}]
},
description:{
identifier: 'description', //和表单里的name值一致
rules: [{
type: 'empty', //非空验证
prompt: '请输入博客描述',
}]
},
}
})
后端核心代码
进入新增博客时初始化
首先是创作类型、分类、标签选择,大致是一样的,不过创作类型是固定的,而分类和标签则需要从数据库中获取,查找到所有的分类和标签。直接调用JPA提供的findAll()方法就行。
Controller层
@GetMapping("/blogs/input")
public String input(Model model){
//初始化
model.addAttribute("tags",tagService.listTag());
model.addAttribute("types",typeService.listType());
model.addAttribute("blog",new Blog());
return INPUT;
}
新增/编辑博客
博客的新增/编辑其实和之前的分类/标签差不多,都是根据id是否存在进行判断。不过需要对标签进行多一步的处理,因为标签在数据库中存储的是标签id组成的字符串 如 1,2,3 ,所以我们在新增博客时需要将标签id拼接后存储,而在编辑时则需要将标签字符串分割后,根据标签id查到标签名再返回到前端。对分类的处理就简单许多,因为一篇博客只做了一个分类。
controller层
//新增博客
@PostMapping("/blogs")
public String addblog(Blog blog, RedirectAttributes redirectAttributes, HttpSession
session){
blog.setUser((User) session.getAttribute("user"));
blog.setType(typeService.getTypebyId(blog.getType().getId()));
blog.setTags(tagService.listTag(blog.getTagids()));
Blog b=blogService.saveBlog(blog);
if(b==null){
redirectAttributes.addFlashAttribute("message","操作失败");
}else{
redirectAttributes.addFlashAttribute("message","操作成功");
}
return "redirect:/admin/blogs";
}
//编辑博客
@GetMapping("/blogs/{id}/input")
public String editblog(Model model ,@PathVariable Long id){
//初始化
model.addAttribute("tags",tagService.listTag());
model.addAttribute("types",typeService.listType());
Blog blog=blogService.getBlog(id);
blog.init(); // 处理tag id
model.addAttribute("blog",blog); //根据id获取博客内容 返回到发布博客页面
return INPUT;
}
service层
博客新增时对标签的处理,在JPA中存在这个方法List<T> findAllById(Iterable<ID> ids); 可以对集合进行迭代,查到数据。所以我们可以将标签字符串转化成list集合后调用这个方法,查到对应的标签集合。
@Override
public List<Tag> listTag(String ids) { //根据前端传过来的 {1,2,3} 查到标签
return tagRepository.findAllById(idlist(ids));
}
private List<Long> idlist( String ids){
List<Long >list=new ArrayList<>();
if(!"".equals(ids) && ids!=null){
String [] idarr =ids.split(",");
for(String id:idarr){
list.add(new Long(id));
}
}
return list;
}
编辑博客时对标签的处理 ,这里是在博客实体类Blog中处理的,加上@Transient注解的字段不会被添加到数据库中。
@Transient
private String Tagids;
// 将标签id 的数组 转换成字符串
public void init(){
this.Tagids=toTagids(this.getTags());
}
public String toTagids(List<Tag>tagarr){
if(!tagarr.isEmpty()){
boolean flag=false;
StringBuffer sb=new StringBuffer();
for(Tag tag:tagarr){
if(flag){
sb.append(",");
}else{
flag=true;
}
sb.append(tag.getId());
}
return sb.toString();
}
return Tagids;
}
因为新增和编辑共用一个页面所以不管是保存为草稿,还是新增或者编辑最后都是调用同一个方法,不过编辑除了修改博客本身的内容,只需要再修改更新时间就行,不用修改创建时间和阅读次数,所以这里做了一个判断。
@Transactional
@Override
public Blog saveBlog(Blog blog) {
if(blog.getId()<1){ //说明id不存在 是新增博客
blog.setCreatTime(new Date());
blog.setUpdateTime(new Date());
blog.setViews(0);
}else {
blog.setUpdateTime(new Date());
}
return blogRepository.save(blog);
}
列表展示页面
博客的列表展示页面和标签/分页的列表展示页面基本相似,但是多了一个动态查询功能。可以根据单独的标签/分类进行查询,也可以综合起来查询。
前端核心代码
<div class="ui secondary segment form">
<!-- 定义一个隐含域 -->
<input type="hidden" name="page">
<div class="inline fields ">
<div class="field">
<input type="text" name="title" placeholder="标题">
</div>
<div class="field">
<div class="ui selection dropdown">
<input type="hidden" name="typeId">
<i class="dropdown icon"></i>
<div class="default text">分类</div>
<div class="menu">
<div th:each="type : ${types}" th:data-value="${type.id}"
th:text="${type.name}" class="item" >1</div>
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="recommende" name="recommende">
<label for="recommende">推荐</label>
</div>
</div>
<div class="field">
<button type="button" id="search-btn" class="ui mini teal basic
button"><i class="search icon"></i>搜索</button>
</div>
</div>
</div>
分页
<tfoot>
<tr>
<th colspan="7">
<div class="ui mini pagination menu"
th:if="${page.totalPages>1}">
<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>
</div>
<a href="#" th:href="@{/admin/blogs/input}" class="ui mini
right floated teal basic button">新增</a>
</th>
</tr>
</tfoot>
function page(obj){ //给隐含域赋值
$("[name='page']").val($(obj).data("page"));
loadlist();
}
$('#search-btn').click(function(){
$("[name='page']").val(0);
loadlist();
});
function loadlist() {
$("#table-container").load(/*[[@{/admin/blogs/search}]]*/"/admin/blogs/search",
{
title : $("[name='title']").val(),
id : $("[name='typeId']").val(),
recommende : $("[name='recommende']").prop('checked'),
page : $("[name='page']").val()
});
}
因为整个项目的分页功能不是一次性查出所有数据,然后整体做分页,而是每点击一次分页就查一次数据库。这里的分页和之前的不一样,当有条件搜索时,点击分页也应该满足搜索条件,负责下一页会出现不在查询条件中的数据。如果将搜索部分的条件也拼接到“{page.number}-1”这个后面,那代码可读性太差。因此在搜索部分添加了一个page的隐含域,让page可以动态的更新。th:attr="data-page=${page.number}-1"
data-xxx 是自定义属性数据 th:attr是用thymeleaf模板进行解析。
后端核心代码
service层
@Override
public Page<Blog> listBlog(Pageable pageable, BlogVo blog) {
return blogRepository.findAll(new Specification<Blog>() {
@Override
public Predicate toPredicate(Root<Blog> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
List<Predicate>predicates=new ArrayList<>();
if(!"".equals(blog.getTitle())&&(blog.getTitle()!=null)){
predicates.add(cb.like(root.
<String>get("title"),"%"+blog.getTitle()+"%"));
}
if(blog.getId()!=null){
predicates.add(cb.equal(root.
<Type>get("type").get("id"),blog.getId()));
}
if (blog.isRecommende()){
predicates.add(cb.equal(root.
<Boolean>get("recommende"),blog.isRecommende()));
}
// 相当于 sql 里面的where 传一个数组
query.where(predicates.toArray(new Predicate[predicates.size()]));
return null;
}
},pageable);
}
这个地方为了方便条件查询,将三个条件放在了一个vo里面,然后通过Specification查询Specification查询来得到数据,Specification有三个参数,root就是你要查询的对象,CriteriaQuery 主要用于对查询结果的处理,包括groupBy、orderBy、having、distinct等操作。 CriteriaBuilder 主要用于各种条件查询、模拟sql函数等。
controller层
@RequestMapping("/blogs/search")
private String search(Model model , @PageableDefault(size = 2,sort =
{"updateTime"},direction = Sort.Direction.DESC) Pageable pageable, BlogVo blog){
model.addAttribute("page",blogService.listBlog(pageable,blog));
return "admin/blogs :: bloglist";//返回该页面下的 bloglist 的片段
}
admin/blogs :: bloglist 这里通过Ajax请求结合thymeleaf模板里的fragment完成动态局部刷新。
前端核心代码
<table th:fragment="bloglist" class="ui compact table">
<thead>
<tr >
<th></th>
<th>标题</th>
<th>类型</th>
<th>推荐</th>
<th>状态</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="blog,iterStat : ${page.content}">
<td th:text="${iterStat.count}">1</td>
<td th:text="${blog.title}">Java并发编程的艺术</td>
<td th:text="${blog.type.name}"> java</td>
<td th:text="${blog.recommende} ? '是' : '否'">是</td>
<td th:text="${blog.published} ? '发布' : '草稿'">是</td>
<td th:text="${blog.updateTime}">2017-10-02</td>
<td>
<a href="#"
th:href="@{/admin/blogs/{id}/input(id=${blog.id})}" class="ui mini positive basic button">编辑</a>
<a href="#"
th:href="@{/admin/blogs/{id}/delete(id=${blog.id})}" class="ui mini negative basic button">删除</a>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th colspan="7">
<div class="ui mini pagination menu"
th:if="${page.totalPages>1}">
<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>
</div>
<a href="#" th:href="@{/admin/blogs/input}" class="ui mini
right floated teal basic button">新增</a>
</th>
</tr>
</tfoot>
</table>
th:fragment="bloglist" 这里就是定义的动态更新的区域。
$('#search-btn').click(function(){
$("[name='page']").val(0);
loadlist();
});
function loadlist() {
$("#table-container").load(/*[[@{/admin/blogs/search}]]*/"/admin/blogs/search",
{
title : $("[name='title']").val(),
id : $("[name='typeId']").val(),
recommende : $("[name='recommende']").prop('checked'),
page : $("[name='page']").val()
});
}
最后的删除和前面的一样,比较简单。
SpringBoot开发一个小而美的个人博客(五)分类、标签管理_舒克、舒克的博客-CSDN博客
SpringBoot开发一个小而美的个人博客(四)实体类构建、使用JPA建数据库表,实现后台登录_舒克、舒克的博客-CSDN博客
SpringBoot开发一个小而美的个人博客(三) 框架搭建_舒克、舒克的博客-CSDN博客_springboot开发个人博客
SpringBoot开发一个小而美的个人博客(二) 前端页面(二)_舒克、舒克的博客-CSDN博客
SpringBoot开发一个小而美的个人博客(一) 前端页面(一)_舒克、舒克的博客-CSDN博客
这篇写了很久,但感觉还是没有写好,没有讲清楚,还得多看看视频,加油加油