关于博客中使用的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)对于数据拥有者(也就是公司)来说,用户的所有数据都是有价值的,因为大数据时代来了,所以不管是前台系统的用户,还是后台系统的管理员,都不应该具备拥有直接删除数据库数据的权限,而删除功能又必须提供给用户,于是使用逻辑删除就显得合情合理了。
当然了,以上纯属本人自己的观点,大家见仁见智。

继续我们今日的主题。

1、分类管理列表添加隐藏字段

上一篇博客中有讲到使用乐观锁字段(verision)来做修改,删除的时候也是需要的,但是前面只是直接给出了代码,并没有具体的解释如何实现的,这里我来做补充说明。

(1)预期实现的效果图

如下图,这是实现成功之后的效果图。从图中可以看出,我每条记录只显示8个字段,但实际上每行记录携带了9个字段,我们的目的,就是可以随心所欲的在页面中携带一些不需要展示出来的隐藏字段。

如果按照原有的方法,要想在每条记录中展示某个值,只需要在 category.js中 添加相应的字段就可以,但是这样会有一个问题,那就是被添加的字段也会同时显示在列表页面中,而我的需求是:我只是需要这个字段,而并不需要展示这个字段。

(2)实现步骤

为了实现这个效果,我可是死磕了几个夜晚,读了N遍bootstrap-treeable.js的源码啊。我的实现方式如下:

①首先在初始化表格的时候,添加一个自定义的属性 hiddenField ,这个hiddenField 的值是所有需要隐藏的字段,注意它的格式是:每个字段都由中括号[]括起来,如果有多个值,则用逗号分开。举例:假设要隐藏的字段有两个,是 version 和 name ,那么最终拼接好的 hiddenField : "[version],[name],"。同时不要忘记,下图中的B步骤也是必不可少的,否则会没有数据。

②然后是对boostrap-treeable.js源码的修改,主要有三处修改,请看截图:

到这里,功能就已经实现了。

2、实现删除功能

(1)前端【删除】按钮的点击事件

(2)后端删除接口的实现

注意,此处我删除业务的逻辑是这样定义的:删除当前分类的时候,需要递归删除当前分类下的所有子分类,同时因为我的商品是挂载在叶子节点分类下的,所以如果当前分类是叶子节点,或者当前分类的子分类是叶子节点,再删除他们之前都要额外判断这些叶子节点是否挂载有上架商品。如果有,则不允许删除,整个事务都应该回滚。

具体的实现逻辑,请直接读源码。

3、源码

同样的,有些文件可能没有粘贴出来,主要还是前面的博客中都已经粘贴出来了,此处不想重复无用的操作,而且贴出来的代码只是作为一种参考,为了帮助理解本篇博客的主题。真正完整的代码,会在这个系列的博客结束之时贴出来,可能还有一段很长的时间。

(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/";@Autowiredprivate 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<String,Object> 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")@ResponseBodypublic Object list(QueryParam queryParam,PageInfo pageInfo) {List<Map<String, Object>> list = categoryService.listCategory(queryParam,pageInfo);//因为是自定义分页,所以返回的数据格式需要做特殊封装,主要是两个属性名的定义要固定JSONObject jo=new JSONObject();//也可以使用 Map<String,Object>//属性名必须是【data】,对应的值是List<Map<String, Object>>格式jo.put("data",new CategoryWrapper(list).wrap());jo.put("pageInfo",pageInfo);//属性名必须是 pageInfo,return jo;}/*** 停用或启用商品分类及其所有子类* @param id* @param version* @param status* @return*/@RequestMapping(value = "/status")@ResponseBodypublic Object updateStatus(@RequestParam("id")int id,@RequestParam("version")int version,@RequestParam("status")int status) {categoryService.updateStatus(id,version,status);return SUCCESS_TIP;}/*** 新增分类管理*/@RequestMapping(value = "/add")@ResponseBodypublic Object add(@Valid CategoryForm categoryForm) {/*** 1、修改接收数据的实体类,因为如果直接使用DAO层的实体类来接收,* 会导致一些不需要的数据被写进数据库* 2、对必传数据要判断是否为空* 3、只接收需要的数据,比如这个CategoryForm实体类,id这个字段我是不需要的,但是只是* 添加这个接口不需要,我修改接口是需要的,此时不能在CategoryForm这个类中不定义id这个属性。* 所以,正确的做法是,在添加接口的具体逻辑里,我不在乎你是否传了id,因为我压根不会操作这个字段*/categoryService.addCategory(categoryForm);return SUCCESS_TIP;}/*** 删除分类管理*/@RequestMapping(value = "/delete")@ResponseBodypublic Object delete(@RequestParam("id") int id,@RequestParam("version")int version) {/*** 删除商品分类的逻辑:* (1)只能做逻辑删除,不能做物理删除,因为有可能商品管理中用到了这个分类,* 如果做了物理删除,以后映射查询商品的时候可能会出错* (2)删除的时候不能只删除当前分类,还要将当前分类下的所有子类做递归逻辑删除,* 为了保证数据安全,前端要做二次确认的提示,防止用户误操作*///操作流水和授权暂时不实现,后面篇幅介绍categoryService.deleteCategoryById(id,version);return SUCCESS_TIP;}/*** 修改分类管理** 逻辑:* (1)已废弃的商品分类不能修改* (2)允许修改的地方:分类名称,排序数字* (3)如果修改的分类名称已经存在,修改失败* (4)其它情况修改成功*/@RequestMapping(value = "/update")@ResponseBodypublic Object update(@Valid CategoryForm categoryForm) {//修改流水暂时不处理,后面专门使用单独的篇幅演示categoryService.updateCategory(categoryForm);return SUCCESS_TIP;}/*** 获取菜单列表(选择父级菜单用)*/@RequestMapping(value = "/selectCategoryTreeList")@ResponseBodypublic List<ZTreeNode> selectMenuTreeList() {List<ZTreeNode> roleTreeList = categoryService.categoryTreeList();roleTreeList.add(ZTreeNode.createParent());return roleTreeList;}
}

(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.*;/*** <p>*  服务实现类* </p>** @author hqq */
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category>implements ICategoryService,StatusConstant {@Autowiredprivate CategoryMapper categoryMapper;@Autowiredprivate IProductAttributeGroupService productAttributeGroupService;@Autowiredprivate ProductAttributeGroupMapper productAttributeGroupMapper;@Autowiredprivate ProductAttachMapper productAttachMapper;@Transactional@Overridepublic 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<Integer> ids = StringUtil.chunkSplitInt(category.getParentIds(), ",");ids.remove(0);//去掉顶级菜单标识Integer[] idArr = ids.toArray(new Integer[ids.size()]);Wrapper<Category> 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<Category> 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<Category> list = categoryMapper.selectList(wrapper);Category temp = null;for(int i = 0 ;i<list.size() ; i++){temp = list.get(i);recursionUpdateStatus(temp.getId(),temp.getVersion(),status);}}/*** (逻辑)删除商品分类及其所有子类* 注意要控制在同一个事务中* @param categoryId* @param version*/@Transactional@Overridepublic void deleteCategoryById(int categoryId, int version) {//检验数据是否合法Category category = DBUtil.selectById(categoryId,"分类id",CategoryMapper.class);//判断版本是否冲突,如果冲突,则直接返回if(version != category.getVersion()){
//            FAIL_DELETE_CONCURRENT(500,"并发删除异常,请稍后重试"),throw new ServiceException(BizExceptionEnum.FAIL_DELETE_CONCURRENT);}//判断是否已经删除的,如果已经删除了,那么也不做错误提示,直接返回成功if(category.getStatus() == PRODUCT_CATEGORY_STATUS_DELETE){// ERROR_REPEAT_DELETE(500,"当前记录已删除,请勿重复操作"),throw new ServiceException(BizExceptionEnum.ERROR_REPEAT_DELETE);}recursionDelete(categoryId,version);}/*** 递归删除所有的子节点*/private void recursionDelete(int id, int version){//判断当前分类下是否有上架中的商品,//......//对当前分类做逻辑删除,只要有一个删除失败了,就全部失败Category param = new Category();param.setId(id);param.setVersion(version);param.setStatus(PRODUCT_CATEGORY_STATUS_DELETE);count = categoryMapper.updateById(param);if(count==0){//FAIL_DELETE_CONCURRENT(500,"并发删除异常,请稍后重试"),throw new ServiceException(BizExceptionEnum.FAIL_DELETE_CONCURRENT);}//查询所有未被删除的子类,递归删除所有的子类菜单Wrapper<Category> wrapper = new EntityWrapper<>();wrapper.eq("parent_id",param.getId()).ne("status",PRODUCT_CATEGORY_STATUS_DELETE).setSqlSelect("id","version");//只查询id和version字段,提高封装效率List<Category> list = categoryMapper.selectList(wrapper);Category temp = null;for(int i = 0 ;i<list.size() ; i++){temp = list.get(i);recursionDelete(temp.getId(),temp.getVersion());}}/*** 更新分类管理菜单的信息* @param form*/@Transactional@Overridepublic void updateCategory(CategoryForm form) {/*** 做到给商品分类添加属性组的时候,发现如果修改分类所属的父类时,* 后期的业务逻辑会变得很复杂,为简化业务逻辑的复杂度,* 修改商品分类的时候,不允许用户更换父类菜单。*///校验参数合法性,其实可以省略,但是后面因为要校验同名记录时需要用到父类idCategory category = DBUtil.selectById(form.getId(),"分类id",CategoryMapper.class);if(category.getStatus() == PRODUCT_CATEGORY_STATUS_DELETE){//CATEGORY_ERROR_CATEGORY_DELETED(500,"不能修改已废弃的商品份分类"),throw new ServiceException(BizExceptionEnum.CATEGORY_ERROR_CATEGORY_DELETED);}//日志需要
//        LogObjectHolder.me().set(category);//因为我前面接收id的时候,用的是int类型的,所以如果程序能走到这里,//那么说明,此时至少不会造成空指针异常。//封装要修改的内容,默认不更换父类菜单,只是修改名称或修改排序编号Category cg = new Category();cg.setName(form.getName());cg.setSort(form.getSort());cg.setVersion(form.getVersion()+1);Wrapper<Category> 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);}}/*** 获取分类管理的详情信息*/@Overridepublic Map<String, Object> 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<Integer> ids = StringUtil.chunkSplitInt(category.getParentIds(), ",");String name = null;int num = 0;for(int i=0;i<ids.size();i++){num = ids.get(i);//顶级菜单栏不用查if(num==0){continue;}name = categoryMapper.findCategoryNameById(num);if(StringUtils.isBlank(name)){continue;}parentNames.append("->[").append(name).append("]");}}//再加上自己parentNames.append("->[").append(category.getName()).append("]");jo.put("parentName",parentName);//保存父类名称jo.put("parentDepth",parentDepth);//保存父类深度jo.put("parentNames",parentNames.toString());//保存所有的父类名称return jo;}@Overridepublic List<ZTreeNode> categoryTreeList() {return categoryMapper.categoryTreeList(LimitationConstant.MALL_CATEGORY_TREE_MAX_DEPTH);}/*** 自定义逻辑的添加商品分类实现* @param form*/@Transactional@Overridepublic 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,默认值是0String 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<Category> 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);}}@Overridepublic List<Map<String, Object>> listCategory(QueryParam queryParam, PageInfo pageInfo) {//设置排序String sortField = "sort";//排序字段boolean isAsc = true;//是否正序排序//构建查询条件Wrapper<Category> wrapper = buildWrapper(queryParam, sortField,isAsc);Page<Map<String,Object>> page=new Page<>(pageInfo.getCurrentPage(),pageInfo.getLimit());List<Map<String, Object>> 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<Map<String, Object>> list = new ArrayList<>();for(int i = 0; i<maps.size();i++){findChildCategory(list, maps.get(i),sortField,isAsc,queryParam.getStatus());}return list;}/*** 封装 category 的查询条件,* 注意:这些查询条件是针对顶级菜单的,* 子级菜单的查询条件只有一个parent_id和排序方式* @param queryParam* @return*/private Wrapper<Category> buildWrapper(QueryParam queryParam,String sortField,boolean isAsc){int status = queryParam.getStatus();Wrapper<Category> 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为0if(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<Map<String, Object>> findChildCategory(List<Map<String, Object>> result,Map<String, Object> category,String sortField, boolean isAsc, int status){result.add(category);//封装子级菜单的查询条件,// 子级菜单的查询条件只有一个parent_id和排序方式Wrapper<Category> 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<Map<String, Object>> childList = categoryMapper.selectMaps(wrapper);for (Map<String, Object> 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;/*** <p>*  服务类* </p>** @author hqq*/
public interface ICategoryService extends IService<Category> {/*** 获取分类管理列表*/List<Map<String,Object>> listCategory(QueryParam queryParam, PageInfo pageInfo);/*** 自定义逻辑的添加商品分类实现*/void addCategory(CategoryForm categoryForm);/*** 获取分类管理的详情信息*/Map<String,Object> getCategoryDetails(Integer categoryId, String timeZone);/*** 获取菜单列表树*/List<ZTreeNode> 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或checkboxvar hasSelectItem = false;var target = $(this);// 在外层包装一下div,样式用的bootstrap-table的var _main_div = $("<div class='bootstrap-tree-table fixed-table-container'></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 = $("<div class='fixed-table-toolbar'></div>");var _tool_left_div = $("<div class='bs-bars pull-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 = $('<tr class="tg-' + item[options.parentCode] + '"></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 = $('<td style="text-align:center;width:36px"></td>');if (column.radio) {var _ipt = $('<input name="select_item" type="radio" value="' + item[options.id] + '"></input>');td.append(_ipt);}if (column.checkbox) {var _ipt = $('<input name="select_item" type="checkbox" value="' + item[options.id] + '"></input>');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){//拼装自定义的<td>标签,这个标签和其它的一样,不同点是多了一个隐藏属性 style="display: none;",// 也正是通过这个属性达到隐藏效果,但这还不够,表头的列也必须加上隐藏属性,否则排版会出问题var td=$('<td title="' + item[column.field] + '" name="' + column.field+ '" style="display: none;">'+item[column.field]+'</td>');tr.append(td);return true;//结束本次循环,进入下一个循环}//###var td = $('<td title="' + item[column.field] + '" name="' + column.field + '" style="'+ ((column.width) ? ('width:' + column.width) : '') + '"></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('<span class="treetable-expander"></span>')} else {td.prepend('<span class="treetable-expander ' + _icon + '"></span>')}for (var int = 0; int < (lv - 1); int++) {td.prepend('<span class="treetable-indent"></span>')}}tr.append(td);}});return tr;}// 加载数据target.load = function (parms) {// 加载数据前先清空allData = {};// 加载数据前先清空target.html("");// 构造表头var thr = $('<tr></tr>');$.each(options.columns, function (i, item) {var th = null;// 判断有没有选择列if (i == 0 && item.field == 'selectItem') {hasSelectItem = true;th = $('<th style="width:36px"></th>');} else {th = $('<th style="' + ((item.width) ? ('width:' + item.width) : '') + '"></th>');}//### 添加隐藏参数修改点3(共3处修改):修改表头的列为隐藏//为了保证排版不出问题,表头的列也必须有,这个这个列的属性也是隐藏的if(hiddenFields.indexOf("["+item.field+"]")!= -1){th = $('<th style="display: none;"></th>');}//###th.text(item.title);thr.append(th);});var thead = $('<thead class="treetable-thead"></thead>');thead.append(thr);target.append(thead);// 构造表体var tbody = $('<tbody class="treetable-tbody"></tbody>');target.append(tbody);// 添加加载loadingvar _loading = '<tr><td colspan="' + options.columns.length + '"><div style="display: block;text-align: center;">正在努力地加载数据中,请稍候……</div></td></tr>'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 = '<tr><td colspan="' + options.columns.length + '"><div style="display: block;text-align: center;">没有找到匹配的记录</div></td></tr>'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 = '<tr><td colspan="' + options.columns.length + '"><div style="display: block;text-align: center;">' + xhr.responseText + '</div></td></tr>'tbody.html(_errorMsg);debugger;},});}if (options.url) {target.load();} else {// 也可以通过defaults里面的data属性通过传递一个数据集合进来对组件进行初始化....有兴趣可以自己实现,思路和上述类似}return target;};// 组件方法封装........$.fn.bootstrapTreeTable.methods = {// 返回选中记录的id(返回的id由配置中的id属性指定)// 为了兼容bootstrap-table的写法,统一返回数组,这里只返回了指定的idgetSelections: function (target, data) {// 所有被选中的记录inputvar _ipt = target.find("tbody").find("tr").find("input[name='select_item']:checked");var chk_value = [];// 如果是radioif (_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的urlajaxParams: {}, // 请求数据的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", //表格idseItem: 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;//分类iddepth = item.depth;//分类的深度//我项目设计了分类管理项目的最大层级是3级,如果超过3级就不能添加if(depth >= Category.maxDepth){Feng.error("当前节点已是叶子节点,无法继续添加子分类");return ;}}//拼接url上需要的参数var urlParams = '?parentId='+parentId+'&parentName='+parentName+'&depth='+depth+"&currentPage="+$("#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+"&currentPage="+$("#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"){
<div class="row"><div class="col-sm-12"><div class="ibox float-e-margins"><div class="ibox-title"><h5 id="pageInfo">分类管理</h5></div><div class="ibox-content"><div class="row row-lg"><div class="col-sm-12"><div class="row"><div class="col-sm-2"><#NameCon id="byName" name="分类名称" /></div><div class="col-sm-2"><#NameCon id="byDepth" name="层级" /></div><div class="col-sm-2"><!-- 默认查询正常的,所以将<option value="1">正常</option>放在最前 --><#SelectCon id="byStatus" name="状态" ><option value="1">正常</option><option value="0">全部</option><option value="2">停用中</option></#SelectCon></div></div><div class="row"><div class="col-sm-2"><#SelectCon id="searchChild" name="是否查询子菜单" ><option value="true">是</option><option value="false">否</option></#SelectCon></div><div class="col-sm-2"><#NameCon id="limit" name="每页查询条数"/></div><div class="col-sm-2"></div><div class="col-sm-3"><#button name="重置" icon="fa-repeat" clickFun="Category.reset()" space="true"/><#button id='searchBtn' name="搜索" icon="fa-search" clickFun="Category.search()"/></div></div><br/><div class="hidden-xs" id="CategoryTableToolbar" role="group">@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"/>@}</div><#table id="CategoryTable"/></div></div><!--定义一个空的div标签,用于分页内容的位置--><br><div  id="pageDiv"></div></div></div></div>
</div><!-- 自定义分页引入 -->
<script src="${ctxPath}/static/modular/elephish/common/paging.js?j=${date().time}"></script>
<script src="${ctxPath}/static/modular/elephish/category/category.js?j=${date().time}"/>@}

该系列更多文章请前往 Guns二次开发目录

Guns二次开发(十):商品分类管理之【删】字诀相关推荐

  1. Guns二次开发(四):重构字典管理模块

    关于博客中使用的Guns版本问题请先阅读   Guns二次开发目录 Guns中的字段管理是一个很不错的功能,我其实也是第一次发现还能这么玩,因为以前一直使用的方法是:后端直接将数据库保存的状态值返回给 ...

  2. shopxo二次开发:商品管理添加、商品导入库存、设置库存量(教程)

    shopxo二次开发:商品添加与库存设置(教程如下) 商品添加与库存设置的一系列的操作说明: 步骤 · 如下: 商品添加与库存设置的一系列的操作说明: 商品管理.商品添加.商品类型设置.商品导入库存. ...

  3. Guns二次开发(五):解决Guns v5.1-final 【代码生成】页面数据表列表没有数据的问题

    关于博客中使用的Guns版本问题请先阅读   Guns二次开发目录 前面的博客中说过,选择Guns做我们的后台管理系统,其中很大一个原因是看中了他的代码生成功能,建议大家先看一下这个演示视频 guns ...

  4. ecshop二次开发 给商品添加自定义字段【包含我自己进一步的开发实例详解】

    本文包含商品自定义添加教程及进一步的开发实例: 教程: 说起自定义字段,我想很多的朋友像我一样会想起一些开源的CMS(比如Dedecms.Phpcms.帝国)等,他们是可以在后台直接添加自定义字段的. ...

  5. ecshopnbsp;二次开发nbsp;全集

    原文地址:ecshop 二次开发 全集作者:phper老腰 添加商品属性: ecshop二次开发,增加商品属性的方法,添加商品属性的方法 默认分类   2009-09-04 23:22   阅读740 ...

  6. 趣谈网络协议笔记-二(第十三讲)

    趣谈网络协议笔记-二(第十三讲) 套接字Socket:Talk is cheap, show me the code 前言 这只是笔记,是为了整理刘超大神的极客时间专栏的只是而存在的! 经常会在网络上 ...

  7. 专题开发十二:JEECG微云快速开发平台-基础用户权限

      专题开发十二:JEECG微云快速开发平台-基础用户权限 11.3.4自定义按钮权限 Jeecg中,目前按钮权限设置,是通过对平台自己封装的按钮标签(<t:dgFunOpt等)进行设置.而在开 ...

  8. 质量管理14条原则、敏捷开发宣言、敏捷开发十二原则

    一.质量管理大师爱德华·戴明博士经典的质量管理14条原则 1. Create constancy of purpose toward improvement of product and servic ...

  9. 第十四课 k8s源码学习和二次开发原理篇-调度器原理

    第十四课 k8s源码学习和二次开发原理篇-调度器原理 tags: k8s 源码学习 categories: 源码学习 二次开发 文章目录 第十四课 k8s源码学习和二次开发原理篇-调度器原理 第一节 ...

最新文章

  1. centos7中nfs文件系统的使用
  2. oracle 关于日期格式转换与使用
  3. 国际会议排名zz(通信、网络类)
  4. 13.5.虚拟化工具--jhat详解、13.6.虚拟化工具--jstack详解
  5. 46. Permutations 排列数
  6. python垃圾回收价格表_深度解析Python垃圾回收机制(超级详细)
  7. hibernate reverse engineering 中没有java src folder
  8. HTTP Get POST方式请求数据
  9. Linux系统 nginx伪静态配置及nginx重启
  10. 集成测试之自顶向下、自底向上、三明治集成
  11. 《一课经济学》六、政府价格管制
  12. 2017美团网易360部分笔试题
  13. 人工智能数学基础---定积分5:使用分部积分法计算定积分
  14. 数据库可视化查询工具
  15. 可靠性测试主要注意点
  16. MySQL数据库-设置数据完整性
  17. 迫在眉睫的企业内控与跃跃欲试的IT
  18. 关于自学HTML+CSS+JS的日子
  19. 【shell脚本创建编辑文件,读取文件】
  20. xss和csrf其实就是一个过滤器和一个拦截器而已:放屁

热门文章

  1. 为啥移动oa办公软件提倡无纸化是未来的大势所趋?
  2. tracker_kcf环境配置
  3. 精密空调故障有哪些?精密空调常见故障维修
  4. 计算机算法设计与分析(第4版) 王晓东 著 2012.2 笔记(这本书还不错,偏实用、有难度)
  5. Qt调用海康威视SDK二次开发抓图,录像,停止录像
  6. 最简单 便捷的开发工具 IDEA 下载包,视频安装 插件 破解码
  7. SpringBoot2.0.3+Mybatis+Mysql+druid实现读写分离+事务+切换数据源失败
  8. 回归算法以及源码分享
  9. 商务风团队建设与入职培训PPT模板
  10. 发明三国杀游戏的计算机天才楼天城回母校杭十四中(楼教主orz)