Spring boot开源项目之个人博客(12)—分类(标签)管理
Spring boot开源项目之个人博客(12)—分类(标签)管理
分类与标签管理功能存在高度重合的情况,则仅需记录分类管理相关内容。该功能涵盖了增删查改以及前端分页展示等功能,并且其中包含了一些必要的非空验证和重复验证操作。
1. 分页展示
前端分为两个页面:一个用于分页展示,并带有新增、编辑及删除等功能;分类与标签的管理不支持条件查询功能,在博客管理模块中实现其条件查询功能。另一个则是用于新增和编辑操作的表单提交页面。
该页面基于之前完成优化的博客管理系统分页展示模板进行构建,并详细阐述了如何利用theamleaf和springboot框架中的Pageable组件来实现前端分页展示的具体方法。
service层
@Transactional
@Override
public Page<Type> listType(Pageable pageable) {
return typeRepository.findAll(pageable);
}
AI写代码
定义了查询方法,返回Page<Type>类型的列表。
controller层
@GetMapping("/types")
public String types(@PageableDefault(size = 10, sort = {"id"}, direction = Sort.Direction.DESC)Pageable pageable, Model model){
model.addAttribute("page", typeServiceImpl.listType(pageable));
return "/admin/types";
}
AI写代码
把查到的分类列表用Model推到前端,page里的值有以下格式
{
"content":[
{"id":123,"title":"blog122","content":"this is blog content"},
{"id":122,"title":"blog121","content":"this is blog content"},
{"id":121,"title":"blog120","content":"this is blog content"},
{"id":120,"title":"blog119","content":"this is blog content"},
{"id":119,"title":"blog118","content":"this is blog content"},
{"id":118,"title":"blog117","content":"this is blog content"},
{"id":117,"title":"blog116","content":"this is blog content"},
{"id":116,"title":"blog115","content":"this is blog content"},
{"id":115,"title":"blog114","content":"this is blog content"},
{"id":114,"title":"blog113","content":"this is blog content"},
{"id":113,"title":"blog112","content":"this is blog content"},
{"id":112,"title":"blog111","content":"this is blog content"},
{"id":111,"title":"blog110","content":"this is blog content"},
{"id":110,"title":"blog109","content":"this is blog content"},
{"id":109,"title":"blog108","content":"this is blog content"}],
"last":false,
"totalPages":9,
"totalElements":123,
"size":15,
"number":0,
"first":true,
"sort":[{
"direction":"DESC",
"property":"id",
"ignoreCase":false,
"nullHandling":"NATIVE",
"ascending":false
}],
"numberOfElements":15
}
AI写代码
内容中的属性名称相当于实体类中的属性名称。除此之外还有一些 pagination information: 总页数和当前页数等。
@PageableDefault(size=10, sort="按id字段排序", direction="降序排列")Pageable pageable
通过在代码中标记分页属性来实现数据分页功能:具体包括每页最多容纳多少条数据以及排序方式等细节信息。若无特别标注,则会采用系统默认设置。具体来说,在本例中我们设置了每页最多十条数据,并按照id字段进行逆序排列。其作用是为了将新增分类项优先展示在顶部位置。
前端展示
<table class="ui celled table">
<thead>
<tr>
<th></th>
<th>名称</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="type,iterStat : ${page.content}">
<td th:text="${iterStat.count}">1</td>
<td th:text="${type.name}">刻意练习清单</td>
<td>
<a href="#" th:href="@{/admin/types/{id}/input(id=${type.id})}" class="ui mini teal basic button">编辑</a>
<a href="#" th:href="@{/admin/types/{id}/delete(id=${type.id})}" class="ui mini red basic button">删除</a>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<th colspan="6">
<div class="ui mini pagination menu">
<a class="item" th:href="@{/admin/types(page=${page.number}-1)}" th:unless="${page.first}">上一页</a>
<a class="item" th:href="@{/admin/types(page=${page.number}+1)}" th:unless="${page.last}">下一页</a>
</div>
<a href="#" th:href="@{/admin/types/input}" class="ui mini right floated teal basic button">新增</a>
</th>
</tr>
</tfoot>
</table>
AI写代码
将content中的内容发送至type中。利用theamleaf提供的$th:each功能来完成遍历操作。iterStat的功能是按照顺序编号来进行分页处理,并确保每一页都从数字1开始计算编号。
th:text="${type.name}"会把对应属性中的值取出来填充到表格中相应的位置。
此处判断了是否处于首屏状态;当处于首屏状态时会隐藏上一屏按钮,并将该操作设置在前往上一屏的操作中执行。其中page.number用于表示当前的屏幕编号,默认从0开始计算。点击前往上一屏操作会将当前屏幕编号减小一个单位
下一页功能与上一页类似。
2. 增删查改
- 新增、编辑
新增与编辑共享同一个页面,在功能实现上也有高度相似之处。将其整合在一起作为记录保存。具体区别体现在:在回调处理中无需传递id字段;而编辑操作则需传递被编辑分类对应的id值。存储到数据库时同样地,前者不使用id字段而后者则会采用该id进行操作。
service层
//新增
@Transactional
@Override
public Type saveType(Type type) {
return typeRepository.save(type);
}
//编辑更新
@Transactional
@Override
public Type update(Long id, Type type) {
Type t = getType(id);
if (t == null){
throw new NotFoundException("不存在");
}
BeanUtils.copyProperties(type, t);
return typeRepository.save(t);
}
AI写代码
controller层
//新增
//跳转到新增页面
@GetMapping("/types/input")
public String input(Model model){
model.addAttribute("type", new Type());
return "/admin/types-input";
}
//保存到数据库,进行非空、重复验证
@PostMapping("/types")
public String save(@Valid Type type, BindingResult result, RedirectAttributes attributes){
Type type1 = typeServiceImpl.getType(type.getName());
if (type1 != null){
result.rejectValue("name", "nameError", "该分类已存在!");
}
if (result.hasErrors()){
return "/admin/types-input";
}
Type t = typeServiceImpl.saveType(type);
if (t == null){
//新增失败
attributes.addFlashAttribute("message", "新增失败");
}else {
//新增成功
attributes.addFlashAttribute("message", "新增成功");
}
return "redirect:/admin/types";
}
AI写代码
需要注意的是,在操作完成后必须使用特定的方式将结果重定向到分页展示页面,并通过RedirectAttributes将操作结果的信息传递给前端。此外,在重复检测方面,系统会获取用户的分类对象并调用getType(String name)方法查找对应的目标对象。如果返回的对象不为空,则表示该名称已存在。同时,在前端验证信息时会使用@Valid注解,并且在后端处理中也实现了类似的非空验证逻辑
//编辑
//带id跳转到新增页面
@GetMapping("/types/{id}/input")
public String editInput(@PathVariable Long id, Model model){
model.addAttribute("type", typeServiceImpl.getType(id));
return "/admin/types-input";
}
//带id更新编辑当前分类
@PostMapping("/types/{id}")
public String edit(@Valid Type type, BindingResult result, @PathVariable Long id, RedirectAttributes attributes){
Type type1 = typeServiceImpl.getType(type.getName());
if (type1 != null){
result.rejectValue("name", "nameError", "该分类已存在!");
}
if (result.hasErrors()){
return "/admin/types-input";
}
Type t = typeServiceImpl.update(id, type);
if (t == null){
//新增失败
attributes.addFlashAttribute("message", "更新失败");
}else {
//新增成功
attributes.addFlashAttribute("message", "更新成功");
}
return "redirect:/admin/types";
}
AI写代码
"@PathVariable负责获取请求路径中的参数信息。" 通过获取id值来定位当前需要编辑的对象,并将该对象的数据发送至前端端点。
typeServiceImpl.update(id, type)这一行代码表示在编辑操作中更新数据。由于这是简单的修改操作,只需将原有保存方法替换为update即可。
前端页面
<form action="#" method="post" th:object="${type}" th:action="*{id}==null ? @{/admin/types} : @{/admin/types/{id}(id=*{id})}" class="ui form">
<input type="hidden" name="id" th:value="*{id}">
<div class="required field">
<div class="ui left labeled input">
<label class="ui teal basic label">分类</label>
<input type="text" name="name" placeholder="分类的名称" th:value="*{name}">
</div>
</div>
<div class="ui error message"></div>
<!--/*/
<div class="ui negative message" th:if="${#fields.hasErrors('name')}">
<i class="close icon"></i>
<div class="header">
验证失败:
</div>
<p th:errors="*{name}">This is a special notification which you can dismiss if you're bored with it.</p>
</div>
/*/-->
<div class="ui center aligned container">
<button type="button" class="ui button" onclick="window.history.go(-1)">返回</button>
<button class="ui teal submit button">发布</button>
</div>
</form>
AI写代码
前端主要功能是一个form表单;采用post方式提交;用于通过三元条件判断实现的一个JavaScript表达式;在操作之前需要执行一些初始化步骤;首先,在controller层中获取type对象:当进行新增操作时会跳转至新增页面并创建新的Type实例;而当进行编辑操作时则会根据现有对象查找对应的Type实例;因此,在代码逻辑中可以通过检查是否有已存在的ID来确定当前的操作属于新增还是编辑;这里获取的值为'*{id}'(即已经定义好的object属性)。
- 删除
这个就比较简单:
service层
@Transactional
@Override
public void deleteType(Long id) {
typeRepository.deleteById(id);
}
AI写代码
controller层
@GetMapping("/types/{id}/delete")
public String delete(@PathVariable Long id, RedirectAttributes attributes){
typeServiceImpl.deleteType(id);
attributes.addFlashAttribute("message", "删除成功");
return "redirect:/admin/types";
}
AI写代码
最后需要注意用"redirect:/admin/types"的方式回到分页展示页。
3. 非空、重复验证
这一部分主要是对这个验证进行说明。前后端均进行了非空验证工作。前端实现相对较为简单,在登录功能的相关开发中也已完成基本实现,在稍作修改即可完成的基础上重点记录了后端的非空验证情况
主要采用了@Valid注解来实现后端非空验证功能;对于实体类中需要验证的属性,在其定义中添加了@NotBlank(message="分类名称不能为空")注解;其中message字段是可选配置项,在常规情况下建议设置该字段并将其内容展示至前端以供用户检查。
@NotBlank(message = "分类名称不能为空")
private String name;
AI写代码
然后需要在表单提交的回调方法中加上@Valid注解,这个才生效。
@PostMapping("/types")
public String save(@Valid Type type, BindingResult result, RedirectAttributes attributes)
AI写代码
由于引入了@NotBlank注解,在name属性为空时会触发校验失败,并通过以下步骤显示错误信息:首先定义type对象并绑定错误信息位置;其次将错误具体化并自定义显示内容;最后前端接收并处理后端返回的错误数据。
关于分类管理的相关知识点已基本完成记录;整体操作过程较为顺畅;接下来将转向博客的管理内容;其规模相比分类管理将有所提升;增加了额外的条件筛选功能;总体实现思路与之前一致。
