關于部落格中使用的Guns版本問題請先閱讀 Guns二次開發目錄
在上一篇部落格中,我們實作了背景管理系統的商品分類管理子產品的添加功能,這篇部落格我們開始來實作修改的功能。
1、預期實作的效果
這是預期實作的效果圖:
此時能夠修改的字段其實隻有兩個,而分類狀态的修改則被我獨立出來,一方面是為了提高體驗,更主要的其實隻是為了簡化後端接口的邏輯,将複雜的邏輯拆分為兩個獨立的事件,并交由兩個接口去實作。分類狀态的值有兩個,一個是啟用,一個是停用。啟用的時候需要同時啟用被停用的父級分類;停用則要判斷目前分類下是否有商品,隻有在沒有上架商品的情況下才能停用目前分類(當然,這個邏輯不是現在考慮的,隻是預留出一個位置,等待商品子產品開發出來後再做補充)。
2、修改分類功能的實作
(1)首先是修改按鈕的點選事件
當點選【修改】按鈕時 ,會調用下面的這個 Category.openCategoryDetail()函數,因為我資料庫儲存的時間都是13位的unix時間戳,是以需要格式化時間,要格式化時間,就要擷取目前浏覽器的時區偏移量。而下面的這個moment()函數在前面的部落格中有介紹過,引用的是moment.js中函數。
(2)查詢分類詳情的後端接口
上面的函數執行完成之後,會跳轉到分類詳情的接口,這個接口的主要作用是根據分類id查詢到目前分類的詳情資訊,為beetl的頁面渲染(springboot,确切的說是由springmvc自動完成模闆渲染)提供資料。
擷取資料的操作,有兩個地方要說明,一個是 DBUtil.selectById()方法,這個工具方法主要是用來判斷前端傳來的id字段是否合法,也就是能否在資料庫中查到記錄,因為後續很多子產品的接口都要執行此操作,是以我将他單獨抽取出來作為一個工具類,其核心功能是使用反射實作的,同時也涉及到一個非spring管理的類調用spring管理的類的實作方法。TimeUtil.unixTimestampToLocalDate()方法則是我自定義的時間格式化函數。工具類具體的實作邏輯,可以檢視代碼。
(3)分類詳情頁面
分類詳情頁面,需要對字段進行填充,使用的是beetl模闆的文法。除此之外,我還加了一些input隐藏标簽,用來存儲一些重要的資料,這些資料都是在送出修改分類接口的時候後端需要的。
(4)修改頁面送出按鈕調用的js函數
送出修改的js邏輯和添加時一樣,不過需要注意修改成功之後,關閉模态框和重新整理清單的操作。 因為category_info.js 是在 category_edit.html頁面引入的,而 category.js則沒有在 category_edit.html頁面引入,在category_edit.html頁面要想調用 category.js的函數,需要加一個 window.parent 字首:
(5)商品分類修改接口的實作
修改商品分類接口有使用到樂觀鎖,其邏輯是:假設我們在分類詳情頁面看到的資料是version=3的版本,此時修改的時候,也要攜帶這個version=3這個參數,意思是我要修改的是version=3這個版本的資料。在編寫修改的 sql語句的時候,要同步更新version(即 sql的set 子句部分 要加上 version=version+1),同時其 where子句也要添加一個 version=3 的條件。并發情況下,當你點選送出按鈕之前,假設這條記錄被另一個人修改過了,此時記錄的version就必然會大于3,那麼version=3這個條件就不成立,是以修改就不能成功,抛出運作時異常後,前面已經執行的sql都将會被事務復原。此時前端再提示使用者重新打開詳情一面,一方面是為了檢視最新的詳情資訊,另一方面也是為了讓頁面擷取最新的version字段的值,隻有攜帶最新的version才能修改成功。關于樂觀鎖的使用,Mybatis-plus提供了更簡便的方法,可以參考這篇部落格 mybatis-plus中使用樂觀鎖 。我在項目中使用的方法可能顯得有些笨拙,但這也是由實際需求決定的。
3、啟用/停用功能的實作
(1)在頁面添加【啟用/停用】
首先,這個這個按鈕的實作不是簡單的在頁面加一行顯示按鈕的代碼就可以了,而應該遵守【修改】和【删除】按鈕的規範,如下圖:
具體實作的步驟是:
最後退出目前賬号後重新登入,就能看到這個 【啟用/停用】按鈕了。
(2)編寫【啟用/停用】按鈕的點選事件
點選【啟用/停用】按鈕後觸發的js函數的内部邏輯,要通過目前記錄的狀态,如果目前狀态是“停用”,那麼此時的請求就是“啟用”,反之,如果目前記錄的狀态已經是“啟用”,那這個點選事件觸發的就是“停用”請求。啟用與停用的操作,也需要攜帶樂觀鎖字段(本例的字段名是 version),樂觀鎖的使用前面已經提過,此處不再贅述。
啟用/停用 按鈕觸發的點選事件:
這裡有一點要說明,這裡的version字段其實是隐藏值,這個需要修改bootstrap源碼才能實作,修改的bootstrap源碼檔案是下面這個,此處我先分享出來,具體修改的過程留到下一篇部落格說明,因為這一篇的篇幅有點大。
(3)啟用/停用的後端接口
啟用和停用接口的邏輯會有些複雜,前面已經分析過,此處不再贅述,大家直接看後面的代碼即可。
4、實作修改時的更新時間自動填充
首先請看下面這段代碼,這是我執行修改商品分類時拼接的語句:
然後看具體執行的sql:
可以發現,我雖然沒有設定updateTime這個字段的值,但是執行sql的時候卻自動加上了。
這是通過 mybatis-plus的公共字段自動填充 功能實作的。下面是具體的實作代碼:
(1)首先是實體類中對要自動填充的自動添加注解
(2)自定義實作類 MyMetaObjectHandler
5、源碼
注:此處分享的代碼,隻跟本篇部落格的主題 ,即修改商品分類的實作有關,是以,即使你全部照搬這篇部落格裡提供的代碼,也可能會出現因為缺少某些類而導緻項目無法啟動,因為有些代碼要在前面幾遍部落格中才會出現。建議你看的時候,重點關注修改點相對于的代碼,這才是本篇部落格的靈魂啊。
(1)StatusConstant.java
package cn.stylefeng.guns.elephish.constants;
/**
* 資料庫中的表格的 status字段狀态的值的常量
*/
public interface StatusConstant {
/**
* 商品分類表 mall_product_category 中的status字段:
* 1,啟用中;2,停用中;3,已删除
*/
int PRODUCT_CATEGORY_STATUS_START = 1;//表示 啟用中 含義的值。
int PRODUCT_CATEGORY_STATUS_STOP = 2;//表示 停用中 含義的值
int PRODUCT_CATEGORY_STATUS_DELETE = 3;//表示 已删除 含義的值
}
(2)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
* @Date 2020-04-13 11:02:46
*/
@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<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")
@ResponseBody
public 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")
@ResponseBody
public 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")
@ResponseBody
public Object add(@Valid CategoryForm categoryForm) {
/**
* 1、修改接收資料的實體類,因為如果直接使用DAO層的實體類來接收,
* 會導緻一些不需要的資料被寫進資料庫
* 2、對必傳資料要判斷是否為空
* 3、隻接收需要的資料,比如這個CategoryForm實體類,id這個字段我是不需要的,但是隻是
* 添加這個接口不需要,我修改接口是需要的,此時不能在CategoryForm這個類中不定義id這個屬性。
* 是以,正确的做法是,在添加接口的具體邏輯裡,我不在乎你是否傳了id,因為我壓根不會操作這個字段
*/
categoryService.addCategory(categoryForm);
return SUCCESS_TIP;
}
/**
* 删除分類管理
*/
@RequestMapping(value = "/delete")
@ResponseBody
public Object delete(@RequestParam Integer categoryId) {
categoryService.deleteById(categoryId);
return SUCCESS_TIP;
}
/**
* 修改分類管理
*
* 邏輯:
* (1)已廢棄的商品分類不能修改
* (2)允許修改的地方:分類名稱,排序數字
* (3)如果修改的分類名稱已經存在,修改失敗
* (4)其它情況修改成功
*/
@RequestMapping(value = "/update")
@ResponseBody
public Object update(@Valid CategoryForm categoryForm) {
//修改流水暫時不處理,後面專門使用單獨的篇幅示範
categoryService.updateCategory(categoryForm);
return SUCCESS_TIP;
}
/**
* 分類管理詳情
*/
// @RequestMapping(value = "/detail/{categoryId}")
// @ResponseBody
// public Object detail(@PathVariable("categoryId") Integer categoryId) {
// return categoryService.selectById(categoryId);
// }
/**
* 擷取菜單清單(選擇父級菜單用)
*/
@RequestMapping(value = "/selectCategoryTreeList")
@ResponseBody
public List<ZTreeNode> selectMenuTreeList() {
List<ZTreeNode> roleTreeList = categoryService.categoryTreeList();
roleTreeList.add(ZTreeNode.createParent());
return roleTreeList;
}
}
(3)Category.java
package cn.stylefeng.guns.elephish.model;
import com.baomidou.mybatisplus.annotations.Version;
import com.baomidou.mybatisplus.enums.FieldFill;
import com.baomidou.mybatisplus.enums.IdType;
import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.annotations.TableField;
import com.baomidou.mybatisplus.activerecord.Model;
import com.baomidou.mybatisplus.annotations.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author hqq
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("mall_category")
public class Category extends Model<Category> {
private static final long serialVersionUID = 1L;
/**
* 類别id
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 父類别id,當id=0時,說明是根節點,一級類别
*/
@TableField("parent_id")
private Integer parentId;
/**
* 目前類别的所有父類别id,包含多層級别的父類别id,友善查找
*/
@TableField("parent_ids")
private String parentIds;
/**
* 類别名稱
*/
private String name;
/**
* 類别狀态。1,正常;2,已廢棄
*/
private Integer status;
/**
* 排序編号,同類展示順序,數值相等則自然排序
*/
@TableField("sort")
private Integer sort;
/**
* 深度,也就是菜單層級,1表示第一級,2表示第二級,以此類推
*/
private Integer depth;
/**
* 版本(樂觀鎖保留字段)
*/
@Version
private Integer version;
/**
* 建立時間,機關是毫秒
*/
@TableField(value = "create_time",fill = FieldFill.INSERT)
private Long createTime;
/**
* 更新時間,機關是毫秒
*/
@TableField(value="update_time",fill = FieldFill.UPDATE)
private Long updateTime;
@Override
protected Serializable pkVal() {
return this.id;
}
}
(4)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.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.form.CategoryForm;
import cn.stylefeng.guns.elephish.model.Category;
import cn.stylefeng.guns.elephish.dao.CategoryMapper;
import cn.stylefeng.guns.elephish.service.ICategoryService;
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 {
@Autowired
private CategoryMapper categoryMapper;
@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<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 form
*/
@Transactional
@Override
public void updateCategory(CategoryForm form) {
/**
* 做到給商品分類添加屬性組的時候,發現如果修改分類所屬的父類時,
* 後期的業務邏輯會變得很複雜,為簡化業務邏輯的複雜度,
* 修改商品分類的時候,不允許使用者更換父類菜單。
*/
//校驗參數合法性,其實可以省略,但是後面因為要校驗同名記錄時需要用到父類id
Category 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);
}
//封裝要修改的内容,預設不更換父類菜單,隻是修改名稱或修改排序編号
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);
}
}
/**
* 擷取分類管理的詳情資訊
*/
@Override
public 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;
}
@Override
public List<ZTreeNode> 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<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);
}
}
@Override
public 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為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<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;
}
}
(5)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 version
* @param status
*/
void updateStatus(int id, int version, int status);
}
(6)DBUtil.java
package cn.stylefeng.guns.elephish.utils;
import cn.hutool.core.util.ReflectUtil;
import cn.stylefeng.roses.core.util.SpringContextHolder;
import cn.stylefeng.roses.kernel.model.exception.ServiceException;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
/**
* 公共的db對象操作類
* Created by hqq
*/
public class DBUtil {
/**
* 通過id查詢去資料庫查詢對應的記錄。
* 建議隻是校驗參數是否合法時使用
* @param id 記錄的id
* @param fieldName 出錯字段的名稱
* @param clazz 對應的mapper實作類的類型
* @return 如果擷取的對象為null,則抛出異常,否則傳回可用的對象
*/
public static <T,V>T selectById(int id,String fieldName,Class<V> clazz){
//擷取對應的BaseMapper實作類
Object obj = SpringContextHolder.getBean(clazz);
//擷取baseMapper的 selectById 方法對象
// Method method = ReflectionUtils.findMethod(obj.getClass(), "selectById", Integer.class);
Method method = ReflectUtil.getMethod(obj.getClass(), "selectById");
//Object invokeMethod(Method method, Object target, Object... args)
//在指定對象(target)上,使用指定參數(args),執行方法(method);
Object o = ReflectionUtils.invokeMethod(method, obj, id);
if(o==null){
//ILLEGAL_PARAM(400,"非法參數"),
throw new ServiceException(400,"參數【"+fieldName+"】不合法");
}
return (T)o;
}
}
(7)StringUtil.java
package cn.stylefeng.guns.elephish.utils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.velocity.runtime.directive.Foreach;
import java.util.ArrayList;
import java.util.List;
/**
* 自定義的字元串工具類
* Created by hqq
*/
public class StringUtil {
/**
* 自定義字元串split方法,并去除每一個元素的首尾空格
* 如果傳入的參數是null或空串,容量為0的字元串集合
* 如果分隔後的結果為null或空數組,傳回容量為0的集合,防止調用者空指針異常
*
* 示例如:split("aa, bb ,c c," , ",");
* 得到結果: ["aa","bb","c c"]
*
* @param mutiString 需要進行分割的字元串
* @param separatorChars 分割标志
* @return
*/
public static List<String> split(String mutiString,final String separatorChars){
//定義一個list封裝最後的結果
List<String> result = new ArrayList<>();
if(StringUtils.isBlank(mutiString)){
return result;
}
//使用性能更好的StringUtils.split方法
String[] split = StringUtils.split(mutiString,separatorChars);
if(split==null || split.length<1){
return result;
}
//去除首尾空格
for (int i = 0; i<split.length; i++) {
if(StringUtils.isBlank(split[i])){
continue;
}
result.add(split[i].trim());
}
return result;
}
/**
* 将字元串"[1],[2],[3]," 轉換為集合封裝 {1,2,3}
* 如果數組中有不能轉換成int類型的元素,則會被忽略。如:"[1],[abc],[3],"得到的結果是:[1,3]
* 如果最終沒有符合條件的數,傳回容量為0的空集合,防止調用者空指針異常
* @param mutiString 需要進行分割的字元串
* @param separatorChars 分割标志
* @return
*/
public static List<Integer> chunkSplitInt(String mutiString,final String separatorChars){
//定義一個list封裝最後的結果
List<Integer> result = new ArrayList<>();
//分割成數組
List<String> list = StringUtil.split(mutiString, separatorChars);
if(list.isEmpty()){
return result;
}
//操作每一個元素
String temp = null;
int length = 0;
for (int i = 0 ; i<list.size(); i++) {
temp = list.get(i);
length = temp.length();
//既然每個元素都是[]包圍的,那麼長度肯定大于2
if(length<3){
continue;
}
//去除首尾的"[]"
temp = temp.substring(1,length-1);
//判斷字元串中是否全為數字,示例如下:
//NumberUtils.isDigits("0000000000.596");//false
//NumberUtils.isDigits("0000000000596");//true
if(!NumberUtils.isDigits(temp)){
continue;
}
result.add(new Integer(temp));
}
return result;
}
}
(8)TimeUtil.java
package cn.stylefeng.guns.elephish.utils;
import org.apache.commons.lang3.StringUtils;
import java.time.*;
import java.time.format.DateTimeFormatter;
/**
* Created by hqq
*/
public class TimeUtil {
private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 将 "+08:00"類型的字元串,轉換成對應的數字 8
* 如果轉換失敗,會傳回 -20 ,因為-20并不是時間偏移數,
* 如果運用到時間轉換中,會報錯
* @param zoneStr 要轉化的參數
* @return
*/
public static int formatZoneHour(String zoneStr){
int errorNum = -20;
if(StringUtils.isBlank(zoneStr)){
return errorNum;
}
try{
return new Integer(zoneStr.replaceAll(":00", "").trim());
}catch (Exception e){
}
return errorNum;
}
/**
* 擷取目前時間戳,注意,建立的項目,
* 背景統一使用這個方法生成的時間戳
* @return
*/
public static long currentTimeMillis(){
return Instant.now().toEpochMilli();
// return CurrentTimeMillisClock.getInstance().now();
}
public static void main(String[] args) {
System.out.println(currentTimeMillis());
System.out.println(System.currentTimeMillis());
System.out.println(Instant.now().toEpochMilli());
}
/**
* 将unix時間戳格式化為 yyyy-MM-dd HH:mm:ss 格式的時間
* 如果轉換失敗,原樣傳回時間戳
* @param unixTimestamp unix時間戳,機關是毫秒
* @param zoneHour 時區偏移的小時數,傳回是 -12 到 +12
* @return
*/
public static String unixTimestampToLocalDate(long unixTimestamp , int zoneHour){
try {
//傳入的時區範圍錯誤
if(!(zoneHour >=-12 && zoneHour<=12)){
return ""+unixTimestamp;
}
Instant instant = Instant.ofEpochMilli(unixTimestamp);
DateTimeFormatter ftf = DateTimeFormatter
.ofPattern(DEFAULT_DATE_PATTERN).withZone(ZoneOffset.ofHours(zoneHour));
return ftf.format(instant);
} catch (Exception e) {
e.printStackTrace();
}
//如果失敗,傳回原時間戳
return ""+unixTimestamp;
}
}
(9)CategoryWrapper.java
package cn.stylefeng.guns.elephish.wrapper;
import cn.stylefeng.guns.core.common.constant.factory.ConstantFactory;
import cn.stylefeng.guns.elephish.constants.WrapperDictNameConstant;
import cn.stylefeng.roses.core.base.warpper.BaseControllerWrapper;
import java.util.List;
import java.util.Map;
/**
* 商品分類管理的包裝類
*
* Created by hqq
*/
public class CategoryWrapper extends BaseControllerWrapper {
public CategoryWrapper(List<Map<String, Object>> multi) {
super(multi);
}
/**
* 此處自定義添加需要包裝的字段的字典資訊
*/
@Override
protected void wrapTheMap(Map<String, Object> map) {
//狀态名稱
map.put("statusName", ConstantFactory.me().getDictById(
WrapperDictNameConstant.MALL_CATEGORY_STATUS_ID, map.get("status").toString()));
}
}
(10)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 = $("<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);
// 添加加載loading
var _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的寫法,統一傳回數組,這裡隻傳回了指定的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);
(11)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;
var version = this.seItem.version;
var operation =function () {
var ajax = new $ax(Feng.ctxPath + "/category/delete", function (data) {
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);
}
}
(12)category_info.js
/**
* 初始化分類管理詳情對話框
*/
var CategoryInfoDlg = {
ztreeInstance: null
};
/**
* 關閉此對話框
*/
CategoryInfoDlg.close = function() {
parent.layer.close(window.parent.Category.layerIndex);
}
/**
* 收集資料
*/
CategoryInfoDlg.collectData = function(type) {
var fieldArr;//定義一個數組,封裝所有需要收集的資料的字段
var needArr;//定義一個數組,封裝字段是否是必傳字段,注意必須與 fieldArr數組中的容量保持一緻
var fieldNameArr;//定義一個數組,封裝對應字段的字段名稱,用于收集資訊出錯時定位錯誤資訊
var typeArr;//定義數組,封裝需要收集的字段的值的類型
var strMaxLengthArr;//定義數組,指定字元串類型的字段的值的最大長度。-1表示該字段不用比較
if(type === 'add'){
//設定添加需要的字段
fieldArr = ['parentId','name','sort','status','parentName'];
needArr = [true,true,true,true,false];
fieldNameArr = ['父類編号','分類名稱','排序編号','狀态設定',''];
typeArr = ['number','','number','number',''];
strMaxLengthArr = [11,10,11,2,-1];
}else if(type==='edit'){
//設定修改需要的字段
fieldArr = ['id','parentId','name','sort','version'];
needArr = [true,true,true,true,true];
fieldNameArr = ['分類編号','父類編号','分類名稱','排序編号','版本資訊'];
typeArr = ['number','number','','number','number'];
strMaxLengthArr = [11,11,10,11,11];
}else{
return "收集資訊出錯啦";
}
return CommonTool.collectData(fieldArr,needArr,fieldNameArr,typeArr,strMaxLengthArr);
}
/**
* 送出添加
*/
CategoryInfoDlg.addSubmit = function() {
//重新采集資料
var result = this.collectData("add");
//如果傳回的字段是字元串類型的,說明出錯了,直接傳回出錯資訊
if(typeof(result)=='string'){
Feng.error(result);
return ;
}
//送出資訊
var ajax = new $ax(Feng.ctxPath + "/category/add", function(data){
Feng.success("添加成功!");
//修改成功之後,重新整理資料,依舊跳轉到原來的頁面
var queryParams = window.parent.Category.formParams();
queryParams['currentPage'] = $("#currentPage").val()
window.parent.Category.table.refresh({query:queryParams });
CategoryInfoDlg.close();
},function(data){
Feng.error("添加失敗:" + data.responseJSON.message + "!");
});
ajax.set(result);//設定請求參數
ajax.start();
}
/**
* 送出修改
*/
CategoryInfoDlg.editSubmit = function() {
var result = this.collectData("edit");
//如果傳回的字段是字元串類型的,說明出錯了,直接傳回出錯資訊
if(typeof(result)=='string'){
Feng.error(result);
return ;
}
// if(result["id"]===result["parentId"]){
// Feng.error("子分類與父分類不能相同");
// return ;
// }
//送出資訊
var ajax = new $ax(Feng.ctxPath + "/category/update", function(data){
Feng.success("修改成功!");
//修改成功之後,重新整理資料,依舊跳轉到原來的頁面
var queryParams = window.parent.Category.formParams();
queryParams['currentPage'] = $("#currentPage").val();
window.parent.Category.table.refresh({query: queryParams});
CategoryInfoDlg.close();
},function(data){
Feng.error("修改失敗!" + data.responseJSON.message + "!");
});
ajax.set(result);//設定請求參數
ajax.start();
}
$(function () {
});
(13)category_edit.html
@layout("/common/_container.html"){
<div class="ibox float-e-margins">
<div class="ibox-content">
<div class="form-horizontal">
<div class="row">
<div class="col-sm-6 b-r">
<h3 style="color:green;text-align:center;">父類資訊</h3>
<#input id="parentName" disabled="disabled" value="${nvl(item.parentName,'無')}"
name="父類名稱" underline="false"/>
<br/>
<#input id="parentId" disabled="disabled" value="${nvl(item.parentId,'無')}"
name="父類編号" underline="false"/>
<br/>
<#input id="parentDepth" disabled="disabled" value="${nvl(item.parentDepth,'無')}"
name="父類層級" underline="false"/>
<br/><br><br><br><br><br>
</div>
<div class="col-sm-6">
<h3 style="color:green;text-align:center;">目前分類資訊</h3>
<input id="id" type="hidden" value="${item.id}"/>
<input id="currentPage" type="hidden" value="${item.currentPage}"/>
<input id="version" type="hidden" value="${item.version}">
<#input id="name" disabled="${item.status==1?'':'disabled'}"
name="分類名稱" value="${nvl(item.name,'無')}" />
<#input id="sort" disabled="${item.status==1?'':'disabled'}"
name="排序編号" value="${nvl(item.sort,'無')}" />
<#input id="status" disabled="disabled" name="分類狀态"
value="${nvl(item.statusName,'無')}" />
<#input id="depth" disabled="disabled" name="分類層級"
value="${nvl(item.depth,'無')}" />
<#input id="parentNames" disabled="disabled" name="層級詳情"
value="${nvl(item.parentNames,'無')}" />
<#input id="createTime" disabled="disabled" name="建立時間"
value="${nvl(item.createTime,'無')}" />
<#input id="updateTime" disabled="disabled" name="更新時間"
value="${nvl(item.updateTime,'無')}" />
</div>
</div>
<div class="row btn-group-m-t">
<div class="col-sm-10">
@if(item.status ==1){
<#button btnCss="info" name="送出" id="ensure" icon="fa-check"
clickFun="CategoryInfoDlg.editSubmit()"/>
@}
<#button btnCss="danger" name="取消" id="cancel" icon="fa-eraser"
clickFun="CategoryInfoDlg.close()"/>
</div>
</div>
</div>
</div>
</div>
<script src="${ctxPath}/static/modular/elephish/category/category_info.js?j=${date().time}"></script>
@}
(14)BeanConfig.java
package cn.stylefeng.guns.elephish.config;
import cn.stylefeng.guns.elephish.utils.TimeUtil;
import com.baomidou.mybatisplus.mapper.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Instant;
/**
*
* Created by hqq
*/
@Configuration
public class BeanConfig {
/**
* mybatis-plus自動填充新增是修改時間的時間字段
* @return
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MetaObjectHandler() {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", TimeUtil.currentTimeMillis(), metaObject);
// this.setFieldValByName("update_time", LocalDateTime.now(), metaObject);
}
/**
* 設定自動填充updateTime字段的值,為13位的unix時間戳。
* 這個方法必須設定,因為我的所有表中,時間字段存的是時間戳,
* 而mybatis-plus(也有可能是guns)這個字段預設填充的datetime類型的,
* 是以調用mybatis-plus自帶的删除、更新等方法,會報錯,為了解決這個問題,
* 是以使用了自己的配置覆寫前面的配置
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
//定義所有的updateTime字段的值為時間戳類型
this.setFieldValByName("updateTime", TimeUtil.currentTimeMillis(), metaObject);
}
};
}
}
(15)LimitationConstant.java
package cn.stylefeng.guns.elephish.constants;
/**
* Created by hqq
*/
public interface LimitationConstant {
/**
* 商品分類菜單樹形結構的最大層級,也就是葉子節點所在的層級
*/
int MALL_CATEGORY_TREE_MAX_DEPTH = 3;
}
(16)WrapperDictNameConstant.java
package cn.stylefeng.guns.elephish.constants;
/**
*
* 字典包裝類的字典名稱常量
*
* Created by hqq
*/
public interface WrapperDictNameConstant {
/**
* sys_user 表中 sex 字段對應的字典名稱 的ID
*/
int SYS_USER_SEX_ID = 127;
/**
* mall_product_category 表中status字段對應的 字典ID
*/
int MALL_CATEGORY_STATUS_ID = 130;
}
(17)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二次開發目錄