关于博客中使用的Guns版本问题请先阅读 Guns二次开发目录
上一篇博客中,我们实现了商品分类的修改功能,这篇博客我们继续实现增删改查里最后的删除功能。首先要明确,我们平时所说的删除功能,其实可以区分为两种:物理删除和逻辑删除。所谓物理删除,就是执行的“delete from mall_category where ...?”开头的sql语句,然后执行成功之后,就真的删除了(其实也不是真的删除,因为数据库底层做的其实也是逻辑删除,专业的DBA其实还是可以恢复被删除的记录的)。而逻辑删除,底层的sql执行的其实是 “update mall_category set ...?”语句,只不过是将数据库中的记录的某个状态字段设置成我们定义的表示已删除的值。拿我们 的 mall_category 表来说,表示状态的字段是 status,我这里对status的值的定义是:1,正常;2,停用;3,已删除。本项目中,我使用的是逻辑删除,而且我也推荐使用逻辑删除。理由如下:
(1)从用户的角度考虑删除的定义:一旦删除成功,用户余生都将看不到被删除的数据。因此,作为开发者,我是否可以理解为:你发起删除操作之后,我不让你再看到这条记录就行了,至于记录是否真的被删除,其实用户根本不在乎,即便在乎,那也没用,因为数据的拥有者并不仅仅只是用户,平台服务的提供者(公司)才是甲方!!!
(2)某些数据,比如电商平台的商品,已经决定不再售卖了,所以运营人员执行了删除操作,如果此时使用的是物理删除,那么就会导致前台用户查询历史订单的时候,无法查看到以前购买过的商品的信息,这样明显就是不合理的。所以需要使用逻辑删除。
(3)实际的开发环境和生产环境中,系统bug是永远存在的,旧的bug被解决,新的bug总会不经意间被创造。假设有这么一个bug,bug出现的地方恰巧已经成功执行了物理删除,那么你后期即便修补了这个bug,前期被物理删除的数据要恢复也是很困难的(特别是你的用户量很大的时候)。相比之下,如果使用的是逻辑删除,那技术人员恢复数据时只需要修改状态字段的值便可以了。
(4)对于数据拥有者(也就是公司)来说,用户的所有数据都是有价值的,因为大数据时代来了,所以不管是前台系统的用户,还是后台系统的管理员,都不应该具备拥有直接删除数据库数据的权限,而删除功能又必须提供给用户,于是使用逻辑删除就显得合情合理了。
当然了,以上纯属本人自己的观点,大家见仁见智。
继续我们今日的主题。
上一篇博客中有讲到使用乐观锁字段(verision)来做修改,删除的时候也是需要的,但是前面只是直接给出了代码,并没有具体的解释如何实现的,这里我来做补充说明。
如下图,这是实现成功之后的效果图。从图中可以看出,我每条记录只显示8个字段,但实际上每行记录携带了9个字段,我们的目的,就是可以随心所欲的在页面中携带一些不需要展示出来的隐藏字段。
如果按照原有的方法,要想在每条记录中展示某个值,只需要在 category.js中 添加相应的字段就可以,但是这样会有一个问题,那就是被添加的字段也会同时显示在列表页面中,而我的需求是:我只是需要这个字段,而并不需要展示这个字段。
为了实现这个效果,我可是死磕了几个夜晚,读了N遍bootstrap-treeable.js的源码啊。我的实现方式如下:
①首先在初始化表格的时候,添加一个自定义的属性 hiddenField ,这个hiddenField 的值是所有需要隐藏的字段,注意它的格式是:每个字段都由中括号[]括起来,如果有多个值,则用逗号分开。举例:假设要隐藏的字段有两个,是 version 和 name ,那么最终拼接好的 hiddenField : "[version],[name],"。同时不要忘记,下图中的B步骤也是必不可少的,否则会没有数据。
②然后是对boostrap-treeable.js源码的修改,主要有三处修改,请看截图:
到这里,功能就已经实现了。
注意,此处我删除业务的逻辑是这样定义的:删除当前分类的时候,需要递归删除当前分类下的所有子分类,同时因为我的商品是挂载在叶子节点分类下的,所以如果当前分类是叶子节点,或者当前分类的子分类是叶子节点,再删除他们之前都要额外判断这些叶子节点是否挂载有上架商品。如果有,则不允许删除,整个事务都应该回滚。
具体的实现逻辑,请直接读源码。
同样的,有些文件可能没有粘贴出来,主要还是前面的博客中都已经粘贴出来了,此处不想重复无用的操作,而且贴出来的代码只是作为一种参考,为了帮助理解本篇博客的主题。真正完整的代码,会在这个系列的博客结束之时贴出来,可能还有一段很长的时间。
(1)CategoryController.java
package cn.stylefeng.guns.elephish.controller;
import cn.stylefeng.guns.core.common.annotion.BussinessLog;
import cn.stylefeng.guns.core.common.annotion.Permission;
import cn.stylefeng.guns.core.common.node.ZTreeNode;
import cn.stylefeng.guns.core.log.LogObjectHolder;
import cn.stylefeng.guns.elephish.bean.PageInfo;
import cn.stylefeng.guns.elephish.bean.QueryParam;
import cn.stylefeng.guns.elephish.constants.dictmaps.CategoryDict;
import cn.stylefeng.guns.elephish.form.CategoryForm;
import cn.stylefeng.guns.elephish.utils.DBUtil;
import cn.stylefeng.guns.elephish.wrapper.CategoryWrapper;
import cn.stylefeng.roses.core.base.controller.BaseController;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.ui.Model;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestParam;
import cn.stylefeng.guns.elephish.service.ICategoryService;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
/**
* 分类管理控制器
*
* @author fengshuonan
*/
@Controller
@RequestMapping("/category")
public class CategoryController extends BaseController {
private Logger logger = LoggerFactory.getLogger(getClass());
private String PREFIX = "/elephish/category/";
@Autowired
private ICategoryService categoryService;
/**
* 跳转到添加分类管理
*/
@RequestMapping("/category_add")
public String categoryAdd(Integer parentId,String parentName,Integer depth,
Integer currentPage,HttpServletRequest request) {
request.setAttribute("parentId",parentId);
request.setAttribute("parentName",parentName);
request.setAttribute("depth",depth);
request.setAttribute("currentPage",currentPage);
return PREFIX + "category_add.html";
}
/**
* 跳转到修改分类管理
*/
@RequestMapping("/category_update")
public String categoryUpdate(@RequestParam("id") int id,@RequestParam("timeZone") String timeZone,
@RequestParam("currentPage") int currentPage,Model model) {
// Category map = categoryService.selectById(categoryId);
// LogObjectHolder.me().set(category);
Map map = categoryService.getCategoryDetails(id,timeZone);
map.put("currentPage",currentPage);//当前页码
model.addAttribute("item",map);
return PREFIX + "category_edit.html";
}
/**
* 跳转到分类管理首页
*/
@RequestMapping("")
public String index() {
return PREFIX + "category.html";
}
/**
* 获取分类管理列表
*/
@RequestMapping(value = "/list")
@ResponseBody
public Object list(QueryParam queryParam,PageInfo pageInfo) {
List
(2)CategoryServiceImpl.java
package cn.stylefeng.guns.elephish.service.impl;
import cn.stylefeng.guns.core.common.constant.factory.ConstantFactory;
import cn.stylefeng.guns.core.common.exception.BizExceptionEnum;
import cn.stylefeng.guns.core.common.node.ZTreeNode;
import cn.stylefeng.guns.core.log.LogObjectHolder;
import cn.stylefeng.guns.elephish.bean.PageInfo;
import cn.stylefeng.guns.elephish.bean.QueryParam;
import cn.stylefeng.guns.elephish.constants.LimitationConstant;
import cn.stylefeng.guns.elephish.constants.StatusConstant;
import cn.stylefeng.guns.elephish.constants.WrapperDictNameConstant;
import cn.stylefeng.guns.elephish.dao.ProductAttachMapper;
import cn.stylefeng.guns.elephish.dao.ProductAttributeGroupMapper;
import cn.stylefeng.guns.elephish.form.CategoryForm;
import cn.stylefeng.guns.elephish.model.Category;
import cn.stylefeng.guns.elephish.dao.CategoryMapper;
import cn.stylefeng.guns.elephish.model.ProductAttach;
import cn.stylefeng.guns.elephish.model.ProductAttributeGroup;
import cn.stylefeng.guns.elephish.service.ICategoryService;
import cn.stylefeng.guns.elephish.service.IProductAttributeGroupService;
import cn.stylefeng.guns.elephish.utils.DBUtil;
import cn.stylefeng.guns.elephish.utils.StringUtil;
import cn.stylefeng.guns.elephish.utils.TimeUtil;
import cn.stylefeng.roses.kernel.model.exception.ServiceException;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import com.baomidou.mybatisplus.plugins.Page;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.sun.javafx.sg.prism.NGEllipse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
/**
*
* 服务实现类
*
*
* @author hqq
*/
@Service
public class CategoryServiceImpl extends ServiceImpl
implements ICategoryService,StatusConstant {
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private IProductAttributeGroupService productAttributeGroupService;
@Autowired
private ProductAttributeGroupMapper productAttributeGroupMapper;
@Autowired
private ProductAttachMapper productAttachMapper;
@Transactional
@Override
public void updateStatus(int id, int version, int status) {
//判断状态参数是否正确
if(!(status==PRODUCT_CATEGORY_STATUS_START || status == PRODUCT_CATEGORY_STATUS_STOP)){
//ILLEGAL_PARAM(400,"非法参数"),
throw new ServiceException(BizExceptionEnum.ILLEGAL_PARAM);
}
//判断id是否存在
Category category= DBUtil.selectById(id,"分类id",CategoryMapper.class);
//判断版本是否冲突,如果冲突,则直接返回
if(version != category.getVersion()){
// FAIL_UPDATE_CONCURRENT(500,"并发修改异常,请稍后重试"),
throw new ServiceException(BizExceptionEnum.FAIL_UPDATE_CONCURRENT);
}
//如果当前状态已经是这样了,则不需要修改
if(status == category.getStatus()){
return;
}
//如果是启用,并且当前分类不是顶级分类菜单,就要同时启用已停用的父类菜单,
// 如果父类菜单有被删除的,则无法启用当前分类
if(status == PRODUCT_CATEGORY_STATUS_START && category.getParentId()!= 0 ){
//判断当前分类的所有父类是否有已被删除的
List ids = StringUtil.chunkSplitInt(category.getParentIds(), ",");
ids.remove(0);//去掉顶级菜单标识
Integer[] idArr = ids.toArray(new Integer[ids.size()]);
Wrapper wrapper = new EntityWrapper<>();
wrapper.ne("status",PRODUCT_CATEGORY_STATUS_DELETE)
.in("id",idArr);
Integer count = categoryMapper.selectCount(wrapper);
if(count != idArr.length){
//CATEGORY_PARENT_DELETED(500,"有父类菜单已被删除,无法执行此操作"),
throw new ServiceException(BizExceptionEnum.CATEGORY_PARENT_DELETED);
}
//同时启用当前分类的所有父类
Category cg = new Category();
cg.setStatus(PRODUCT_CATEGORY_STATUS_START);
wrapper = new EntityWrapper<>();
wrapper.eq("status",PRODUCT_CATEGORY_STATUS_STOP)
.in("id",idArr);
categoryMapper.update(cg,wrapper);
}
//递归修改所有的子节点状态
recursionUpdateStatus(id,version,status);
}
/**
* 递归修改所有的子节点的状态
* @param id
* @param version
* @param status
*/
private void recursionUpdateStatus(int id, int version, int status) {
if(status==PRODUCT_CATEGORY_STATUS_STOP){
//如果是停用,则要判断当前分类下是否有上架中的商品
//......
}
//对当前分类状态做状态修改,只要有一个失败了,就全部失败
Category category = new Category();
category.setVersion(version+1);
category.setStatus(status);
Wrapper wrapper = new EntityWrapper<>();
wrapper.eq("id",id)
.eq("version",version)
.ne("status",PRODUCT_CATEGORY_STATUS_DELETE);
int count = categoryMapper.update(category,wrapper);
if(count==0){
//FAIL_UPDATE_CONCURRENT(500,"并发修改异常,请稍后重试"),
throw new ServiceException(BizExceptionEnum.FAIL_UPDATE_CONCURRENT);
}
//查询所有需要上架/下架的所有子类分类,并递归执行上架和下架的操作
wrapper = new EntityWrapper<>();
wrapper.eq("parent_id",id)
.eq("status",status == PRODUCT_CATEGORY_STATUS_START?
PRODUCT_CATEGORY_STATUS_STOP:PRODUCT_CATEGORY_STATUS_START)
.setSqlSelect("id","version");//只查询id和version字段,提高封装效率
List list = categoryMapper.selectList(wrapper);
Category temp = null;
for(int i = 0 ;i wrapper = new EntityWrapper<>();
wrapper.eq("parent_id",param.getId())
.ne("status",PRODUCT_CATEGORY_STATUS_DELETE)
.setSqlSelect("id","version");//只查询id和version字段,提高封装效率
List list = categoryMapper.selectList(wrapper);
Category temp = null;
for(int i = 0 ;i wrapper = new EntityWrapper<>();
wrapper.eq("id",form.getId())
.eq("version", form.getVersion())
.ne("status",PRODUCT_CATEGORY_STATUS_DELETE);//不能修改已删除的记录
Integer count= categoryMapper.update(cg,wrapper);
if(count==0){
//FAIL_ADD_RECORD(500,"数据库中新增数据失败"),
throw new ServiceException(BizExceptionEnum.FAIL_ADD_RECORD);
}
//要判断除了自身之外,同一个父类菜单下,是否有重名记录,
//如果不加判断的修改,会产生重名记录
//判断分类名称是否已经存在。status=1 ,2 时才算重复
wrapper = new EntityWrapper<>();
wrapper.eq("parent_id",category.getParentId())
.eq("name",form.getName())
.in("status",new Integer[]{PRODUCT_CATEGORY_STATUS_START,PRODUCT_CATEGORY_STATUS_STOP});
count = categoryMapper.selectCount(wrapper);
if(count>1){
//ERROR_EXISTED_SAME_NAME_RECORD(500,"数据库中已经存在同名的记录"),
throw new ServiceException(BizExceptionEnum.ERROR_EXISTED_SAME_NAME_RECORD);
}
}
/**
* 获取分类管理的详情信息
*/
@Override
public Map getCategoryDetails(Integer categoryId, String timeZone) {
Category category = DBUtil.selectById(categoryId,"分类id",CategoryMapper.class);
JSONObject jo = JSONObject.parseObject(JSONObject.toJSONString(category), JSONObject.class);
//获取父类信息
Integer parentId = category.getParentId();
String parentName = "顶级";//顶级的分类信息
StringBuilder parentNames = new StringBuilder("[顶级]");//所有的分类信息
int parentDepth = 0;
//查找分类状态的字典解释
String statusName = ConstantFactory.me().getDictById(
WrapperDictNameConstant.MALL_CATEGORY_STATUS_ID, category.getStatus().toString());
jo.put("statusName",statusName);
//格式化时间的操作
int zoneHour = TimeUtil.formatZoneHour(timeZone);
if(category.getCreateTime()!=null){
jo.put("createTime",TimeUtil.unixTimestampToLocalDate(category.getCreateTime(),zoneHour));
}
if(category.getUpdateTime()!=null){
jo.put("updateTime",TimeUtil.unixTimestampToLocalDate(category.getUpdateTime(),zoneHour));
}
//获取所有的父类名称,并拼接成字符串如:'[体育]->[篮球]'
if(parentId!=0){
//获取父类信息
Category parent = DBUtil.selectById(parentId,"分类id",CategoryMapper.class);
parentName = parent.getName();
parentDepth = parent.getDepth();
//获取所有的父类信息
List ids = StringUtil.chunkSplitInt(category.getParentIds(), ",");
String name = null;
int num = 0;
for(int i=0;i[").append(name).append("]");
}
}
//再加上自己
parentNames.append("->[").append(category.getName()).append("]");
jo.put("parentName",parentName);//保存父类名称
jo.put("parentDepth",parentDepth);//保存父类深度
jo.put("parentNames",parentNames.toString());//保存所有的父类名称
return jo;
}
@Override
public List categoryTreeList() {
return categoryMapper.categoryTreeList(LimitationConstant.MALL_CATEGORY_TREE_MAX_DEPTH);
}
/**
* 自定义逻辑的添加商品分类实现
* @param form
*/
@Transactional
@Override
public void addCategory(CategoryForm form) {
int parentId = form.getParentId();
int status = form.getStatus();
//判断status字段是否合法
if(!(status==PRODUCT_CATEGORY_STATUS_START || status==PRODUCT_CATEGORY_STATUS_STOP)){
//ILLEGAL_PARAM(400,"非法参数"),
throw new ServiceException(BizExceptionEnum.ILLEGAL_PARAM);
}
/**
*虽然我前端做了必传字段的半段,但是我在以往的很多博客中都说过,
*永远不要相信前端传来的数据(即便前后端代码都是同一个人写),
*该做的判断还是要判断,
*/
//设置parentIds,默认值是0
String parentIds = "[0],";
int depth = 1;
//判断parentId是否合法
if(parentId>0){
Category cg = DBUtil.selectById(parentId,"分类id",CategoryMapper.class);
if(cg.getStatus() == PRODUCT_CATEGORY_STATUS_STOP){
//CATEGORY_ERROR_PARENT_DELETED(500,"不能为已废弃的父类菜单添加子菜单"),
throw new ServiceException(BizExceptionEnum.CATEGORY_ERROR_PARENT_DELETED);
}
//判断当前是否是叶子节点,如果是,则无法添加子分类
if(cg.getDepth() >= LimitationConstant.MALL_CATEGORY_TREE_MAX_DEPTH){
//CATEGORY_ERROR_NO_NEXT_NODE(500,"当前节点已是叶子节点,无法继续添加子分类"),
throw new ServiceException(BizExceptionEnum.CATEGORY_ERROR_NO_NEXT_NODE);
}
parentIds = cg.getParentIds()+ "["+parentId+"],";
depth = cg.getDepth()+1;
}
//开始执行添加操作
Category category = new Category();
category.setParentId(parentId);
category.setParentIds(parentIds);
category.setDepth(depth);
category.setSort(form.getSort());
category.setName(form.getName());
category.setVersion(1);
category.setStatus(status);//设置默认状态
int count = categoryMapper.insert(category);
if(count==0){
// ERROR_EXISTED_SAME_NAME_RECORD(500,"数据库中已经存在同名的记录"),
throw new ServiceException(BizExceptionEnum.ERROR_EXISTED_SAME_NAME_RECORD);
}
//判断分类名称是否已经存在。status=1 ,2 时才算重复
Wrapper wrapper = new EntityWrapper<>();
wrapper.eq("parent_id",parentId)
.eq("name",form.getName())
.in("status",new Integer[]{PRODUCT_CATEGORY_STATUS_START,PRODUCT_CATEGORY_STATUS_STOP});
count = categoryMapper.selectCount(wrapper);
//前面已经添加一次了,这时数据库应该只有一条同名记录,如果大于一条,说明名字重复
//此时抛出异常,那么整个事务都会回滚,添加操作失败
if(count>1){
//ERROR_EXISTED_SAME_NAME_RECORD(500,"数据库中已经存在同名的记录"),
throw new ServiceException(BizExceptionEnum.ERROR_EXISTED_SAME_NAME_RECORD);
}
}
@Override
public List> listCategory(QueryParam queryParam, PageInfo pageInfo) {
//设置排序
String sortField = "sort";//排序字段
boolean isAsc = true;//是否正序排序
//构建查询条件
Wrapper wrapper = buildWrapper(queryParam, sortField,isAsc);
Page> page=new Page<>(pageInfo.getCurrentPage(),pageInfo.getLimit());
List> maps = categoryMapper.selectMapsPage(page, wrapper);
//设置总页数
int total = (int) page.getTotal();
pageInfo.setTotalPage((int)Math.ceil(1.0*total/pageInfo.getLimit()));//总页数
pageInfo.setTotalCount(total);//总记录数
if(maps.isEmpty()){
return maps;
}
//设置查询到的本页记录数,因为默认值为0,所以大于0的时候才需要设置
pageInfo.setSize(maps.size());
//如果不查询子类菜单,直接返回
if(!queryParam.isSearchChild()){
return maps;
}
//遍历查询其子类
List> list = new ArrayList<>();
for(int i = 0; i buildWrapper(QueryParam queryParam,String sortField,boolean isAsc){
int status = queryParam.getStatus();
Wrapper wrapper = new EntityWrapper<>();
//设置排序字段和排序方式
wrapper.orderBy(sortField,isAsc);
//是否按照层级查询
Integer depth =queryParam.getDepth();
if(depth != null){
wrapper.eq("depth", depth);
}
//是否按照分类名称查询
if(StringUtils.isNotBlank(queryParam.getName())){
wrapper.like("name",queryParam.getName());
}else{
//只有不按分类名称查询,并且没有指定深度,才设置默认的parentId为0
if(depth == null){
wrapper.eq("parent_id", 0);
}
}
//是否按照状态查询
if(status> 0){
if(!(status==PRODUCT_CATEGORY_STATUS_START || status==PRODUCT_CATEGORY_STATUS_STOP)){
//ILLEGAL_STATUS_VALUE(400,"状态字段的值异常"),
throw new ServiceException(BizExceptionEnum.ILLEGAL_STATUS_VALUE);
}
wrapper.eq("status",queryParam.getStatus());
}else{
//否则,只查询未删除的记录
wrapper.in("status",new Integer[]{PRODUCT_CATEGORY_STATUS_START,PRODUCT_CATEGORY_STATUS_STOP});
}
return wrapper;
}
/**
* 递归算法,算出子级菜单
*/
private List> findChildCategory(List> result,
Map category,String sortField, boolean isAsc, int status){
result.add(category);
//封装子级菜单的查询条件,
// 子级菜单的查询条件只有一个parent_id和排序方式
Wrapper wrapper = new EntityWrapper<>();
wrapper.orderBy(sortField,isAsc)
.eq("parent_id", new Integer(category.get("id").toString()));
if(status>0){
wrapper.eq("status",status);
}else{
//否则,只查询未删除的记录
wrapper.in("status",new Integer[]{PRODUCT_CATEGORY_STATUS_START,PRODUCT_CATEGORY_STATUS_STOP});
}
//查找子节点,递归算法一定要有一个退出的条件
List> childList = categoryMapper.selectMaps(wrapper);
for (Map temp : childList) {
findChildCategory(result,temp,sortField,isAsc, status);
}
return result;
}
}
(3)ICategoryService.java
package cn.stylefeng.guns.elephish.service;
import cn.stylefeng.guns.core.common.node.ZTreeNode;
import cn.stylefeng.guns.elephish.bean.PageInfo;
import cn.stylefeng.guns.elephish.bean.QueryParam;
import cn.stylefeng.guns.elephish.form.CategoryForm;
import cn.stylefeng.guns.elephish.model.Category;
import com.baomidou.mybatisplus.service.IService;
import java.util.List;
import java.util.Map;
/**
*
* 服务类
*
*
* @author hqq
*/
public interface ICategoryService extends IService {
/**
* 获取分类管理列表
*/
List> listCategory(QueryParam queryParam, PageInfo pageInfo);
/**
* 自定义逻辑的添加商品分类实现
*/
void addCategory(CategoryForm categoryForm);
/**
* 获取分类管理的详情信息
*/
Map getCategoryDetails(Integer categoryId, String timeZone);
/**
* 获取菜单列表树
*/
List categoryTreeList();
/**
* 更新分类管理菜单的信息
* @param categoryForm
*/
void updateCategory(CategoryForm categoryForm);
/**
* (逻辑)删除商品分类及其所有子类
* @param id
* @param categoryId
*/
void deleteCategoryById(int id, int categoryId);
/**
* 停用或启用商品分类及其所有子类
* @param id
* @param version
* @param status
*/
void updateStatus(int id, int version, int status);
}
(4)bootstrap-treetable.js
/**
* 查找当前这个节点的所有节点(包含子节点),并进行折叠或者展开操作
*
* @param item 被点击条目的子一级条目
* @param target 整个bootstrap tree table实例
* @param globalCollapsedFlag 如果为true,则表示当前操作是收缩(折叠),如果是false,表示当前操作是展开
* @param options 存放了一些常量,例如展开和收缩的class
*/
function extracted($, item, target, globalCollapsedFlag, options) {
var itemCodeName = $(item).find("td[name='"+options.code+"']").text();
// var itemCodeName = $(item).find("td[name='code']").text();
var subItems = target.find("tbody").find(".tg-" + itemCodeName);//下一级,改为下所有级别
if (subItems.size() > 0) {
$.each(subItems, function (nIndex, nItem) {
extracted($, nItem, target, globalCollapsedFlag, options);
});
}
$.each(subItems, function (pIndex, pItem) {
//如果是展开,判断当前箭头是开启还是关闭
var expander = $(item).find("td[name='name']").find(".treetable-expander");
if (!globalCollapsedFlag) {
var hasExpander = expander.hasClass(options.expanderExpandedClass);
if (hasExpander) {
$(pItem).css("display", "table");
} else {
$(pItem).css("display", "none");
}
} else {
//如果是折叠,就把当前开着的都折叠掉
$(pItem).css("display", "none");
expander.removeClass(options.expanderExpandedClass);
expander.addClass(options.expanderCollapsedClass);
}
});
}
(function ($) {
"use strict";
$.fn.bootstrapTreeTable = function (options, param) {
var allData = null;//用于存放格式化后的数据
// 如果是调用方法
if (typeof options == 'string') {
return $.fn.bootstrapTreeTable.methods[options](this, param);
}
// 如果是初始化组件
options = $.extend({}, $.fn.bootstrapTreeTable.defaults, options || {});
// 是否有radio或checkbox
var hasSelectItem = false;
var target = $(this);
// 在外层包装一下div,样式用的bootstrap-table的
var _main_div = $("");
target.before(_main_div);
_main_div.append(target);
target.addClass("table table-hover treetable-table table-bordered");
if (options.striped) {
target.addClass('table-striped');
}
// 工具条在外层包装一下div,样式用的bootstrap-table的
if (options.toolbar) {
var _tool_div = $("");
var _tool_left_div = $("");
_tool_left_div.append($(options.toolbar));
_tool_div.append(_tool_left_div);
_main_div.before(_tool_div);
}
// 格式化数据,优化性能
target.formatData = function (data) {
var _root = options.rootCodeValue ? options.rootCodeValue : null
$.each(data, function (index, item) {
// 添加一个默认属性,用来判断当前节点有没有被显示
item.isShow = false;
// 这里兼容几种常见Root节点写法
// 默认的几种判断
var _defaultRootFlag = item[options.parentCode] == '0'
|| item[options.parentCode] == 0
|| item[options.parentCode] == null
|| item[options.parentCode] == '';
if (!item[options.parentCode] || (_root ? (item[options.parentCode] == options.rootCodeValue) : _defaultRootFlag)) {
if (!allData["_root_"]) {
allData["_root_"] = [];
}
allData["_root_"].push(item);
} else {
if (!allData["_n_" + item[options.parentCode]]) {
allData["_n_" + item[options.parentCode]] = [];
}
allData["_n_" + item[options.parentCode]].push(item);
}
});
}
// 得到根节点
target.getRootNodes = function () {
return allData["_root_"];
};
// 递归获取子节点并且设置子节点
target.handleNode = function (parentNode, lv, tbody) {
var _ls = allData["_n_" + parentNode[options.code]];
var tr = target.renderRow(parentNode, _ls ? true : false, lv);
tbody.append(tr);
if (_ls) {
$.each(_ls, function (i, item) {
target.handleNode(item, (lv + 1), tbody)
});
}
};
//### 添加隐藏参数修改点1(共3处修改):获取需要修改的列
var hiddenFields = "";
//获取是否有需要隐藏的字段,获取到的值的示例如: "[version],"
if(options.columns[0] && options.columns[0].hiddenField ){
hiddenFields = options.columns[0].hiddenField;
//如果这个隐藏字段的类型不是字符串,则忽略,默认为没有需要修改的字段
if(typeof(hiddenFields)!='string' ){
hiddenFields = "";
}
}
//###
// 绘制行
target.renderRow = function (item, isP, lv) {
// 标记已显示
item.isShow = true;
var tr = $(' ');
var _icon = options.expanderCollapsedClass;
if (options.expandAll) {
tr.css("display", "table");
_icon = options.expanderExpandedClass;
} else if (options.expandFirst && lv <= 2) {
tr.css("display", "table");
_icon = (lv == 1) ? options.expanderExpandedClass : options.expanderCollapsedClass;
} else {
tr.css("display", "none");
_icon = options.expanderCollapsedClass;
}
$.each(options.columns, function (index, column) {
// 判断有没有选择列
if (index == 0 && column.field == 'selectItem') {
hasSelectItem = true;
var td = $(' ');
if (column.radio) {
var _ipt = $('');
td.append(_ipt);
}
if (column.checkbox) {
var _ipt = $('');
td.append(_ipt);
}
tr.append(td);
} else {
//### 添加隐藏参数修改点2(共3处修改):修改列中的数据为隐藏项
//判断一个字符串中是否包含另一个字符串,假设第一步获取到 hiddenFields="[version],"
//而此时的 column.field = 'version' ,前后拼接[]后得到的是 "[version]"
//此时 "[version],".indexOf("[version]") 得到的值肯定大于-1,于是这个if判断成立
if(hiddenFields.indexOf("["+column.field+"]")!=-1){
//拼装自定义的标签,这个标签和其它的一样,不同点是多了一个隐藏属性 style="display: none;",
// 也正是通过这个属性达到隐藏效果,但这还不够,表头的列也必须加上隐藏属性,否则排版会出问题
var td=$(' ');
tr.append(td);
return true;//结束本次循环,进入下一个循环
}
//###
var td = $(' ');
// 增加formatter渲染
if (column.formatter) {
td.html(column.formatter.call(this, item[column.field], item, index));
} else {
td.text(item[column.field]);
}
if (options.expandColumn == index) {
if (!isP) {
td.prepend('')
} else {
td.prepend('')
}
for (var int = 0; int < (lv - 1); int++) {
td.prepend('')
}
}
tr.append(td);
}
});
return tr;
}
// 加载数据
target.load = function (parms) {
// 加载数据前先清空
allData = {};
// 加载数据前先清空
target.html("");
// 构造表头
var thr = $(' ');
$.each(options.columns, function (i, item) {
var th = null;
// 判断有没有选择列
if (i == 0 && item.field == 'selectItem') {
hasSelectItem = true;
th = $(' ');
} else {
th = $(' ');
}
//### 添加隐藏参数修改点3(共3处修改):修改表头的列为隐藏
//为了保证排版不出问题,表头的列也必须有,这个这个列的属性也是隐藏的
if(hiddenFields.indexOf("["+item.field+"]")!= -1){
th = $(' ');
}
//###
th.text(item.title);
thr.append(th);
});
var thead = $('');
thead.append(thr);
target.append(thead);
// 构造表体
var tbody = $('');
target.append(tbody);
// 添加加载loading
var _loading = '正在努力地加载数据中,请稍候…… '
tbody.html(_loading);
// 默认高度
if (options.height) {
tbody.css("height", options.height);
}
$.ajax({
type: options.type,
url: options.url,
data: parms ? parms : options.ajaxParams,
dataType: "JSON",
success: function (result, textStatus, jqXHR) {
//### 开始修改guns原来的bootstrap-treetable.js ###
var data =result ;
//判断是否是标准的列表查询,这个很重要,
// 因为我是直接在bootstrap-treetable.js修改的,
//新作的修改必须保证原来的功能不受影响。
if(result && typeof(result.pageInfo)!='undefined'){
data = result.data;
PageTool.buildPageDiv(result.pageInfo);
}
//### 结束修改gun v5.1-final 原来的bootstrap-treetable.js ###
// 加载完数据先清空
tbody.html("");
if (!data || data.length <= 0) {
var _empty = '没有找到匹配的记录 '
tbody.html(_empty);
return;
}
target.formatData(data);
// 开始绘制
var rootNode = target.getRootNodes();
if (rootNode) {
$.each(rootNode, function (i, item) {
target.handleNode(item, 1, tbody);
});
}
// 下边的操作主要是为了查询时让一些没有根节点的节点显示
$.each(data, function (i, item) {
if (!item.isShow) {
var tr = target.renderRow(item, false, 1);
tbody.append(tr);
}
});
target.append(tbody);
//动态设置表头宽度
thead.css("width", tbody.children(":first").css("width"));
// 行点击选中事件
target.find("tbody").find("tr").click(function () {
if (hasSelectItem) {
var _ipt = $(this).find("input[name='select_item']");
if (_ipt.attr("type") == "radio") {
_ipt.prop('checked', true);
target.find("tbody").find("tr").removeClass("treetable-selected");
$(this).addClass("treetable-selected");
} else {
if (_ipt.prop('checked')) {
_ipt.prop('checked', false);
$(this).removeClass("treetable-selected");
} else {
_ipt.prop('checked', true);
$(this).addClass("treetable-selected");
}
}
}
});
// 小图标点击事件--展开缩起
target.find("tbody").find("tr").find(".treetable-expander").click(function () {
var tr = $(this).parent().parent();
var _code = tr.find("input[name='select_item']").val();
if (options.id == options.code) {
_code = tr.find("input[name='select_item']").val();
} else {
_code = tr.find("td[name='" + options.code + "']").text();
}
var _ls = target.find("tbody").find(".tg-" + _code);//下一级,改为下所有级别
if (_ls && _ls.length > 0) {
var _flag = $(this).hasClass(options.expanderExpandedClass);
$.each(_ls, function (index, item) {
//查找当前这个节点的所有节点(包含子节点),如果是折叠都显示为不显示,如果是展开,则根据当前节点的状态
extracted($, item, target, _flag, options);
$(item).css("display", _flag ? "none" : "table");
});
if (_flag) {
$(this).removeClass(options.expanderExpandedClass)
$(this).addClass(options.expanderCollapsedClass)
} else {
$(this).removeClass(options.expanderCollapsedClass)
$(this).addClass(options.expanderExpandedClass)
}
}
});
},
error: function (xhr, textStatus) {
var _errorMsg = '' + xhr.responseText + ' '
tbody.html(_errorMsg);
debugger;
},
});
}
if (options.url) {
target.load();
} else {
// 也可以通过defaults里面的data属性通过传递一个数据集合进来对组件进行初始化....有兴趣可以自己实现,思路和上述类似
}
return target;
};
// 组件方法封装........
$.fn.bootstrapTreeTable.methods = {
// 返回选中记录的id(返回的id由配置中的id属性指定)
// 为了兼容bootstrap-table的写法,统一返回数组,这里只返回了指定的id
getSelections: function (target, data) {
// 所有被选中的记录input
var _ipt = target.find("tbody").find("tr").find("input[name='select_item']:checked");
var chk_value = [];
// 如果是radio
if (_ipt.attr("type") == "radio") {
var _data = {id: _ipt.val()};
var _tds = _ipt.parent().parent().find("td");
_tds.each(function (_i, _item) {
if (_i != 0) {
_data[$(_item).attr("name")] = $(_item).text();
}
});
chk_value.push(_data);
} else {
_ipt.each(function (_i, _item) {
var _data = {id: $(_item).val()};
var _tds = $(_item).parent().parent().find("td");
_tds.each(function (_ii, _iitem) {
if (_ii != 0) {
_data[$(_iitem).attr("name")] = $(_iitem).text();
}
});
chk_value.push(_data);
});
}
return chk_value;
},
// 刷新记录
refresh: function (target, parms) {
if (parms) {
target.load(parms);
} else {
target.load();
}
},
// 组件的其他方法也可以进行类似封装........
};
$.fn.bootstrapTreeTable.defaults = {
id: 'id',// 选取记录返回的值
code: 'id',// 用于设置父子关系
parentCode: 'parentId',// 用于设置父子关系
rootCodeValue: null,//设置根节点code值----可指定根节点,默认为null,"",0,"0"
data: [], // 构造table的数据集合
type: "GET", // 请求数据的ajax类型
url: null, // 请求数据的ajax的url
ajaxParams: {}, // 请求数据的ajax的data属性
expandColumn: null,// 在哪一列上面显示展开按钮
expandAll: true, // 是否全部展开
expandFirst: false, // 是否默认第一级展开--expandAll为false时生效
striped: false, // 是否各行渐变色
columns: [],
toolbar: null,//顶部工具条
height: 0,
expanderExpandedClass: 'glyphicon glyphicon-chevron-down',// 展开的按钮的图标
expanderCollapsedClass: 'glyphicon glyphicon-chevron-right'// 缩起的按钮的图标
};
})(jQuery);
(5)category.js
/**
* 分类管理管理初始化
*/
var Category = {
id: "CategoryTable", //表格id
seItem: null, //选中的条目
table: null,
layerIndex: -1,
maxDepth: 3 //最大的深度
};
/**
* 初始化表格的列
*/
Category.initColumn = function () {
return [
{field: 'selectItem', radio: true , hiddenField:"[version],"},
{title: '分类名称', field: 'name', visible: true, align: 'center', valign: 'middle'},
{title: '分类编号', field: 'id', visible: true, align: 'center', valign: 'middle'},
{title: '分类父编号', field: 'parentId', visible: true, align: 'center', valign: 'middle'},
{title: '层级', field: 'depth', align: 'center', valign: 'middle', sortable: true},
{title: '排序', field: 'sort', visible: true, align: 'center', valign: 'middle'},
{title: '状态', field: 'statusName', visible: true, align: 'center', valign: 'middle'},
{title: '创建时间', field: 'createTime', visible: true, align: 'center', valign: 'middle',
formatter: function (value) {
return typeof(value)=="undefined"?"":moment(+value).format('YYYY-MM-DD HH:mm:ss');
}
},
{title: '更新时间', field: 'updateTime', visible: true, align: 'center', valign: 'middle',
formatter: function (value) {
return typeof(value)=="undefined"?"":moment(+value).format('YYYY-MM-DD HH:mm:ss');
}
},
{title: '版本', field: 'version', align: 'center', valign: 'middle'}
];
};
/**
* 检查是否选中
*/
Category.check = function () {
var selected = $('#' + this.id).bootstrapTreeTable('getSelections');;
if(selected.length == 0){
Feng.info("请先选中表格中的某一记录!");
return false;
}else{
Category.seItem = selected[0];
return true;
}
};
/**
* 点击添加分类管理
*/
Category.openAddCategory = function () {
//默认用户是添加顶级分类
var parentId = 0;
var parentName = '顶级';
var depth = 1;
//尝试获取用户选中的item标签
var selected = $('#' + this.id).bootstrapTreeTable('getSelections');
//如果用户选中了某个单选框,说明是在这个单选框下添加
if(selected.length > 0){
var item = selected[0];
parentName = item.name;//分类名
//如果当前选中的分类已经被废弃了,那么就不允许添加子分类
if(item.statusName == "已废弃"){
Feng.info("分类【"+parentName+"】已被废弃,无法添加子分类!");
return;
}
parentId = item.id;//分类id
depth = item.depth;//分类的深度
//我项目设计了分类管理项目的最大层级是3级,如果超过3级就不能添加
if(depth >= Category.maxDepth){
Feng.error("当前节点已是叶子节点,无法继续添加子分类");
return ;
}
}
//拼接url上需要的参数
var urlParams = '?parentId='+parentId+'&parentName='+parentName
+'&depth='+depth+"¤tPage="+$("#currentPage").val();
var index = layer.open({
type: 2,
title: '添加分类管理',
area: ['800px', '420px'], //宽高
fix: false, //不固定
maxmin: true,
content: Feng.ctxPath + '/category/category_add'+urlParams
});
this.layerIndex = index;
};
/**
* 打开查看分类管理详情
*/
Category.openCategoryDetail = function () {
if(!this.check()){
return ;
}
//获取浏览器的当前时间偏移
var zoneHour = moment(new Date()).format('Z');
var id = Category.seItem.id.trim();
if(!id){
Feng.error("没有获取到id!");
}
//拼接需要的参数
var urlParams = "?id="+id+"&timeZone="+zoneHour
+"¤tPage="+$("#currentPage").val();
var index = layer.open({
type: 2,
title: '分类管理详情',
area: ['953px', '533px'], //宽高
fix: false, //不固定
maxmin: true,
content: Feng.ctxPath + '/category/category_update' + urlParams
});
this.layerIndex = index;
};
/**
* 重置查询条件条件
*/
Category.reset = function () {
$("#byStatus").find("option[text='正常']").attr("selected",true);
$("#byStatus").find("option[text!='正常']").attr("selected",false);
$("#searchChild").find("option[text='是']").attr("selected",true);
$("#searchChild").find("option[text!='是']").attr("selected",false);
$("#currentPage").val("1");//当前页
$("#limit").val("5");//每页查询条数
$("#byName").val("");//分类名称
$("#byDepth").val("");//层级
$("#searchChild").attr("disabled",false);
}
/**
* 修改分类的状态,停用或启用
*/
Category.changeStatus = function () {
if(!this.check()){
return;
}
var item =this.seItem;
var id = item.id;
var version = item.version;
var statusName = item.statusName;
var status ;
if(statusName =='正常'){//如果当前是正常,那么接下来的操作就是要停用
status =2;
statusName = "停用";
}else{//如果当前的状态是已停用了,那么接下来的操作就是要启用
status =1 ;
statusName = "启用";
}
var operation =function () {
var ajax = new $ax(Feng.ctxPath + "/category/status", function () {
Feng.success("修改成功!");
//删除成功之后,刷新当前页
var queryParams = Category.formParams();
queryParams['currentPage'] = $("#currentPage").val();
Category.table.refresh({query: queryParams});
}, function (data) {
Feng.error("操作失败:" + data.responseJSON.message + "!");
});
ajax.set("id",id);
ajax.set("version",version);
ajax.set("status",status);
ajax.start();
}
Feng.confirm("是否【"+statusName+"】分类【"+item.name+"】及其下的所有子分类?", operation);
}
/**
* 删除分类管理
*/
Category.delete = function () {
//检查是否有选中要删除的分类
if(!this.check()){
return;
}
var id = this.seItem.id;//获取列表页面中记录的id的值
var version = this.seItem.version;//获取列表页面隐藏的version字段的值
var operation =function () {
var ajax = new $ax(Feng.ctxPath + "/category/delete", function () {
Feng.success("删除成功!");
//删除成功之后,刷新当前页
var queryParams = Category.formParams();
queryParams['currentPage'] = $("#currentPage").val();
Category.table.refresh({query: queryParams});
}, function (data) {
Feng.error("操作失败:" + data.responseJSON.message + "!");
});
ajax.set("id",id);
ajax.set("version",version);
ajax.start();
}
Feng.confirm("是否刪除分类【"+this.seItem.name+"】及其所有子分类?", operation);
};
/**
* 条件查询分类管理列表
*/
Category.search = function () {
//当前页面刷新
var queryParams = Category.formParams();
queryParams['currentPage'] = $("#currentPage").val();
Category.table.refresh({query: queryParams});
};
$(function () {
var defaultColunms = Category.initColumn();
var table = new BSTreeTable(Category.id, "/category/list", defaultColunms);
table.setExpandColumn(1);//设置第一列展示下拉列表
table.setIdField("id");//分类编号
table.setCodeField("id");//分类父编号,用于设置父子关系
table.setParentCodeField("parentId");//分类父编号,用于设置父子关系
table.setExpandAll(true);
//设置请求时的参数
var queryData = Category.formParams();
queryData['limit'] = 5;//
table.setData(queryData);
table.init();
Category.table = table;
$("#limit").val("5");//设置每页的查询的默认条数
//设置当前对象的名称,分页时需要使用
PageTool.callerName="Category";
});
/**
* 查询表单提交参数对象
* @returns {{}}
*/
Category.formParams = function() {
var queryData = {};
queryData['name'] = $("#byName").val().trim();//名称条件
queryData['depth'] = $("#byDepth").val();//层级条件
queryData['status'] = $("#byStatus").val();//状态条件
queryData['searchChild'] = $("#searchChild").val();//是否查询子菜单
queryData['limit'] = $("#limit").val();//设置每页查询条数
return queryData;
}
/**
* 每页查询的页码数修改之后触发失去焦点事件,
* 将当前页码重置为 1 .
* 主要是为了解决以下情况:
* 假设总共10条记录,每页查询3条,那么总共就有4页,当用户在第三页的时候,
* 修改成每页查询10条,修改后点击查询,会出现没有数据显示的情况。
* 原因是,用户的当前页码 currentPage 的值依旧是3,
* 而每页查询10条后,总共只有1页,查询第三页时肯定没有数据啦
*/
$("#limit").on('blur',function(){
$("#currentPage").val(1);
});
/**
* 【分类名称】输入框失去焦点事件
*/
$("#byName").on('blur',function(){
Category.setSearchChildSelected();
});
/**
*【深度】输入框失去焦点事件
*/
$("#byDepth").on('blur',function(){
Category.setSearchChildSelected();
});
/**
* 设置【是否查询子菜单】选择框是否可用
*/
Category.setSearchChildSelected = function () {
var byName = $("#byName").val().trim();
var byDepth = $("#byDepth").val().trim();
if(byName && !byDepth){
//当选择分类名称查询,不选择层级查询时,默认无法查询子类菜单,这样是为了防止查重和查出不必要的数据
$("#searchChild").val("false");
$("#searchChild").attr("disabled",true);
}else{
//其它情况,都可以自主觉得是否查询子菜单
$("#searchChild").val("true");
$("#searchChild").attr("disabled",false);
}
}
(6)category.html
@layout("/common/_container.html"){
分类管理
<#NameCon id="byName" name="分类名称" />
<#NameCon id="byDepth" name="层级" />
<#SelectCon id="byStatus" name="状态" >
#SelectCon>
<#SelectCon id="searchChild" name="是否查询子菜单" >
#SelectCon>
<#NameCon id="limit" name="每页查询条数"/>
<#button name="重置" icon="fa-repeat" clickFun="Category.reset()" space="true"/>
<#button id='searchBtn' name="搜索" icon="fa-search" clickFun="Category.search()"/>
@if(shiro.hasPermission("/category/add")){
<#button name="添加" icon="fa-plus" clickFun="Category.openAddCategory()"/>
@}
@if(shiro.hasPermission("/category/update")){
<#button name="修改" icon="fa-edit" clickFun="Category.openCategoryDetail()" space="true"/>
@}
@if(shiro.hasPermission("/category/delete")){
<#button name="删除" icon="fa-remove" clickFun="Category.delete()" space="true"/>
@}
@if(shiro.hasPermission("/category/status")){
<#button name="启用/停用" icon="fa-hourglass-start" clickFun="Category.changeStatus()" space="true"/>
@}
<#table id="CategoryTable"/>
@}
该系列更多文章请前往 Guns二次开发目录