Parcourir la source

菜单列表查询,新增及修改功能

xusl il y a 2 ans
Parent
commit
67ac1d81f5
27 fichiers modifiés avec 3198 ajouts et 352 suppressions
  1. 18 0
      backend/pom.xml
  2. 142 0
      backend/src/main/java/com/jiayue/ssi/constant/Constants.java
  3. 78 0
      backend/src/main/java/com/jiayue/ssi/constant/UserConstants.java
  4. 129 0
      backend/src/main/java/com/jiayue/ssi/controller/SysMenuController.java
  5. 17 0
      backend/src/main/java/com/jiayue/ssi/controller/UserLoginController.java
  6. 281 0
      backend/src/main/java/com/jiayue/ssi/entity/SysMenu.java
  7. 2 4
      backend/src/main/java/com/jiayue/ssi/entity/SysParameter.java
  8. 106 0
      backend/src/main/java/com/jiayue/ssi/entity/vo/MetaVo.java
  9. 149 0
      backend/src/main/java/com/jiayue/ssi/entity/vo/RouterVo.java
  10. 131 0
      backend/src/main/java/com/jiayue/ssi/mapper/SysMenuMapper.java
  11. 65 0
      backend/src/main/java/com/jiayue/ssi/service/SysMenuService.java
  12. 324 0
      backend/src/main/java/com/jiayue/ssi/service/impl/SysMenuServiceImpl.java
  13. 610 0
      backend/src/main/java/com/jiayue/ssi/util/StringUtils.java
  14. 14 12
      backend/src/main/resources/application.yml
  15. 202 0
      backend/src/main/resources/mapper/system/SysMenuMapper.xml
  16. 1 0
      ui/package.json
  17. 23 0
      ui/src/directive/index.js
  18. 28 0
      ui/src/directive/permission/hasPermi.js
  19. 4 3
      ui/src/main.js
  20. 7 0
      ui/src/router/index.js
  21. 5 1
      ui/src/store/index.js
  22. 133 0
      ui/src/store/modules/permission.js
  23. 239 0
      ui/src/utils/ruoyi.js
  24. 488 0
      ui/src/views/sysManager/sysMenu/index.vue
  25. 1 1
      ui/src/views/sysManager/userManager/index.vue
  26. 0 331
      ui/src/views/sysmenu/index.vue
  27. 1 0
      ui/vue.config.js

+ 18 - 0
backend/pom.xml

@@ -201,6 +201,24 @@
                 <version>2.2.1.RELEASE</version>
             </plugin>
         </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/java</directory>
+                <includes>
+                    <include>**/*.properties</include>
+                    <include>**/*.xml</include>
+                </includes>
+                <filtering>false</filtering>
+            </resource>
+            <resource>
+                <directory>src/main/resources</directory>
+                <includes>
+                    <include>**/*.properties</include>
+                    <include>**/*.xml</include>
+                </includes>
+                <filtering>false</filtering>
+            </resource>
+        </resources>
     </build>
     <!--上传配置 必须 -->
     <distributionManagement>

+ 142 - 0
backend/src/main/java/com/jiayue/ssi/constant/Constants.java

@@ -0,0 +1,142 @@
+package com.jiayue.ssi.constant;
+
+import io.jsonwebtoken.Claims;
+
+/**
+ * 通用常量信息
+ *
+ * @author ruoyi
+ */
+public class Constants
+{
+    /**
+     * UTF-8 字符集
+     */
+    public static final String UTF8 = "UTF-8";
+
+    /**
+     * GBK 字符集
+     */
+    public static final String GBK = "GBK";
+
+    /**
+     * www主域
+     */
+    public static final String WWW = "www.";
+
+    /**
+     * http请求
+     */
+    public static final String HTTP = "http://";
+
+    /**
+     * https请求
+     */
+    public static final String HTTPS = "https://";
+
+    /**
+     * 通用成功标识
+     */
+    public static final String SUCCESS = "0";
+
+    /**
+     * 通用失败标识
+     */
+    public static final String FAIL = "1";
+
+    /**
+     * 登录成功
+     */
+    public static final String LOGIN_SUCCESS = "Success";
+
+    /**
+     * 注销
+     */
+    public static final String LOGOUT = "Logout";
+
+    /**
+     * 注册
+     */
+    public static final String REGISTER = "Register";
+
+    /**
+     * 登录失败
+     */
+    public static final String LOGIN_FAIL = "Error";
+
+    /**
+     * 验证码有效期(分钟)
+     */
+    public static final Integer CAPTCHA_EXPIRATION = 2;
+
+    /**
+     * 令牌
+     */
+    public static final String TOKEN = "token";
+
+    /**
+     * 令牌前缀
+     */
+    public static final String TOKEN_PREFIX = "Bearer ";
+
+    /**
+     * 令牌前缀
+     */
+    public static final String LOGIN_USER_KEY = "login_user_key";
+
+    /**
+     * 用户ID
+     */
+    public static final String JWT_USERID = "userid";
+
+    /**
+     * 用户名称
+     */
+    public static final String JWT_USERNAME = Claims.SUBJECT;
+
+    /**
+     * 用户头像
+     */
+    public static final String JWT_AVATAR = "avatar";
+
+    /**
+     * 创建时间
+     */
+    public static final String JWT_CREATED = "created";
+
+    /**
+     * 用户权限
+     */
+    public static final String JWT_AUTHORITIES = "authorities";
+
+    /**
+     * 资源映射路径 前缀
+     */
+    public static final String RESOURCE_PREFIX = "/profile";
+
+    /**
+     * RMI 远程方法调用
+     */
+    public static final String LOOKUP_RMI = "rmi:";
+
+    /**
+     * LDAP 远程方法调用
+     */
+    public static final String LOOKUP_LDAP = "ldap:";
+
+    /**
+     * LDAPS 远程方法调用
+     */
+    public static final String LOOKUP_LDAPS = "ldaps:";
+
+    /**
+     * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
+     */
+    public static final String[] JOB_WHITELIST_STR = { "com.ruoyi" };
+
+    /**
+     * 定时任务违规的字符
+     */
+    public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
+            "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config" };
+}

+ 78 - 0
backend/src/main/java/com/jiayue/ssi/constant/UserConstants.java

@@ -0,0 +1,78 @@
+package com.jiayue.ssi.constant;
+
+/**
+ * 用户常量信息
+ *
+ * @author ruoyi
+ */
+public class UserConstants
+{
+    /**
+     * 平台内系统用户的唯一标志
+     */
+    public static final String SYS_USER = "SYS_USER";
+
+    /** 正常状态 */
+    public static final String NORMAL = "0";
+
+    /** 异常状态 */
+    public static final String EXCEPTION = "1";
+
+    /** 用户封禁状态 */
+    public static final String USER_DISABLE = "1";
+
+    /** 角色封禁状态 */
+    public static final String ROLE_DISABLE = "1";
+
+    /** 部门正常状态 */
+    public static final String DEPT_NORMAL = "0";
+
+    /** 部门停用状态 */
+    public static final String DEPT_DISABLE = "1";
+
+    /** 字典正常状态 */
+    public static final String DICT_NORMAL = "0";
+
+    /** 是否为系统默认(是) */
+    public static final String YES = "Y";
+
+    /** 是否菜单外链(是) */
+    public static final String YES_FRAME = "0";
+
+    /** 是否菜单外链(否) */
+    public static final String NO_FRAME = "1";
+
+    /** 菜单类型(目录) */
+    public static final String TYPE_DIR = "M";
+
+    /** 菜单类型(菜单) */
+    public static final String TYPE_MENU = "C";
+
+    /** 菜单类型(按钮) */
+    public static final String TYPE_BUTTON = "F";
+
+    /** Layout组件标识 */
+    public final static String LAYOUT = "Layout";
+
+    /** ParentView组件标识 */
+    public final static String PARENT_VIEW = "ParentView";
+
+    /** InnerLink组件标识 */
+    public final static String INNER_LINK = "InnerLink";
+
+    /** 校验返回结果码 */
+    public final static String UNIQUE = "0";
+    public final static String NOT_UNIQUE = "1";
+
+    /**
+     * 用户名长度限制
+     */
+    public static final int USERNAME_MIN_LENGTH = 2;
+    public static final int USERNAME_MAX_LENGTH = 20;
+
+    /**
+     * 密码长度限制
+     */
+    public static final int PASSWORD_MIN_LENGTH = 5;
+    public static final int PASSWORD_MAX_LENGTH = 20;
+}

+ 129 - 0
backend/src/main/java/com/jiayue/ssi/controller/SysMenuController.java

@@ -0,0 +1,129 @@
+package com.jiayue.ssi.controller;
+
+import java.util.Date;
+import java.util.List;
+
+import com.jiayue.ssi.constant.UserConstants;
+import com.jiayue.ssi.entity.SysMenu;
+import com.jiayue.ssi.entity.SysParameter;
+import com.jiayue.ssi.service.SysMenuService;
+import com.jiayue.ssi.util.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+import com.jiayue.ssi.annotation.InterfaceLimit;
+import com.jiayue.ssi.util.ResponseVO;
+import com.jiayue.ssi.util.SecurityContextUtil;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 菜单接口
+ *
+ * @author xsl
+ * @since 2023/03/21
+ */
+@RestController
+@RequestMapping("/sysMenuController")
+@Slf4j
+public class SysMenuController {
+
+    @Autowired
+    private SysMenuService sysMenuService;
+
+    /**
+     * 获取菜单列表
+     */
+    // @PreAuthorize("@ss.hasPermi('system:menu:list')")
+    @GetMapping("/list")
+    public ResponseVO list(SysMenu menu) {
+        try {
+
+            List<SysMenu> menus = sysMenuService.selectMenuList(menu, SecurityContextUtil.getSysUser().getId());
+            return ResponseVO.success(menus);
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("获取菜单列表异常");
+            return ResponseVO.error(null);
+        }
+    }
+
+    /**
+     * 新增菜单
+     */
+    @PostMapping
+    @InterfaceLimit
+    public ResponseVO add(@RequestBody SysMenu menu) {
+        if (UserConstants.NOT_UNIQUE.equals(sysMenuService.checkMenuNameUnique(menu))) {
+            return ResponseVO.fail(menu.getMenuName() + "'失败,菜单名称已存在!");
+        } else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) {
+            return ResponseVO.fail(menu.getMenuName() + "'失败,地址必须以http(s)://开头!");
+        }
+
+        try {
+            int bo = sysMenuService.insertMenu(menu);
+            if (bo == 1) {
+                return ResponseVO.success("添加菜单成功");
+            } else {
+                log.error("添加菜单失败");
+                return ResponseVO.fail("添加菜单失败");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("添加菜单异常");
+            return ResponseVO.error(e);
+        }
+    }
+
+    /**
+     * 更新系统参数
+     *
+     * @param menu 参数
+     * @return 执行结果
+     */
+    @PutMapping
+    public ResponseVO update(@RequestBody SysMenu menu) {
+        if (UserConstants.NOT_UNIQUE.equals(sysMenuService.checkMenuNameUnique(menu)))
+        {
+            return ResponseVO.fail("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在!");
+
+        }
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
+        {
+            return ResponseVO.fail("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
+        }
+        else if (menu.getMenuId().equals(menu.getParentId()))
+        {
+            return ResponseVO.fail("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
+        }
+
+        try {
+            int bo = sysMenuService.updateMenu(menu);
+            if (bo == 1) {
+                return ResponseVO.success("修改菜单成功");
+            } else {
+                log.error("修改菜单失败");
+                return ResponseVO.fail("修改菜单失败");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("修改菜单异常");
+            return ResponseVO.error(e);
+        }
+    }
+
+    /**
+     * 根据菜单编号获取详细信息
+     */
+    @GetMapping(value = "/{getDetailInfo}")
+    @InterfaceLimit
+    public ResponseVO getDetailInfo(Long menuId) {
+        try {
+            SysMenu sysMenu = sysMenuService.selectMenuById(menuId);
+            return ResponseVO.success(sysMenu);
+        } catch (Exception e) {
+            e.printStackTrace();
+            log.error("获取菜单明细异常");
+            return ResponseVO.error(null);
+        }
+    }
+}

+ 17 - 0
backend/src/main/java/com/jiayue/ssi/controller/UserLoginController.java

@@ -5,7 +5,9 @@ import cn.hutool.captcha.CircleCaptcha;
 import com.jiayue.ssi.annotation.InterfaceLimit;
 import com.jiayue.ssi.config.SendMailUtil;
 import com.jiayue.ssi.constant.CacheConstants;
+import com.jiayue.ssi.entity.SysMenu;
 import com.jiayue.ssi.entity.SysUser;
+import com.jiayue.ssi.service.SysMenuService;
 import com.jiayue.ssi.service.SysUserService;
 import com.jiayue.ssi.util.*;
 import com.wf.captcha.SpecCaptcha;
@@ -23,6 +25,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -39,6 +42,8 @@ public class UserLoginController {
     JwtTokenUtil jwtTokenUtil;
     @Autowired
     SendMailUtil sendMailUtil;
+    @Autowired
+    SysMenuService sysMenuService;
 
     /**
      * 生成验证码
@@ -148,4 +153,16 @@ public class UserLoginController {
         String newToken = jwtTokenUtil.refreshToken(oldToken);
         return ResponseVO.success(newToken);
     }
+
+    /**
+     * 获取路由菜单信息
+     *
+     * @return 路由菜单信息
+     */
+    @GetMapping("getRouters")
+    public ResponseVO getRouters() {
+        Integer userId = SecurityContextUtil.getSysUser().getId();
+        List<SysMenu> menus = sysMenuService.selectMenuTreeByUserId(userId);
+        return ResponseVO.success(sysMenuService.buildMenus(menus));
+    }
 }

+ 281 - 0
backend/src/main/java/com/jiayue/ssi/entity/SysMenu.java

@@ -0,0 +1,281 @@
+package com.jiayue.ssi.entity;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * 菜单权限表
+ *
+ * @author xsl
+ * @version 3.0
+ */
+public class SysMenu extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /** 菜单ID */
+    private Integer menuId;
+
+    /** 菜单名称 */
+    private String menuName;
+
+    /** 父菜单名称 */
+    private String parentName;
+
+    /** 父菜单ID */
+    private Long parentId;
+
+    /** 显示顺序 */
+    private Integer orderNum;
+
+    /** 路由地址 */
+    private String path;
+
+    /** 组件路径 */
+    private String component;
+
+    /** 路由参数 */
+    private String query;
+
+    /** 是否为外链(0是 1否) */
+    private String isFrame;
+
+    /** 是否缓存(0缓存 1不缓存) */
+    private String isCache;
+
+    /** 类型(M目录 C菜单 F按钮) */
+    private String menuType;
+
+    /** 显示状态(0显示 1隐藏) */
+    private String visible;
+
+    /** 菜单状态(0正常 1停用) */
+    private String status;
+
+    /** 权限字符串 */
+    private String perms;
+
+    /** 菜单图标 */
+    private String icon;
+
+    /** 请求参数 */
+    @JsonInclude(JsonInclude.Include.NON_EMPTY)
+    private Map<String, Object> params;
+
+    /** 子菜单 */
+    private List<SysMenu> children = new ArrayList<SysMenu>();
+
+    public Integer getMenuId()
+    {
+        return menuId;
+    }
+
+    public void setMenuId(Integer menuId)
+    {
+        this.menuId = menuId;
+    }
+
+    @NotBlank(message = "菜单名称不能为空")
+    @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符")
+    public String getMenuName()
+    {
+        return menuName;
+    }
+
+    public void setMenuName(String menuName)
+    {
+        this.menuName = menuName;
+    }
+
+    public String getParentName()
+    {
+        return parentName;
+    }
+
+    public void setParentName(String parentName)
+    {
+        this.parentName = parentName;
+    }
+
+    public Long getParentId()
+    {
+        return parentId;
+    }
+
+    public void setParentId(Long parentId)
+    {
+        this.parentId = parentId;
+    }
+
+    @NotNull(message = "显示顺序不能为空")
+    public Integer getOrderNum()
+    {
+        return orderNum;
+    }
+
+    public void setOrderNum(Integer orderNum)
+    {
+        this.orderNum = orderNum;
+    }
+
+    @Size(min = 0, max = 200, message = "路由地址不能超过200个字符")
+    public String getPath()
+    {
+        return path;
+    }
+
+    public void setPath(String path)
+    {
+        this.path = path;
+    }
+
+    @Size(min = 0, max = 200, message = "组件路径不能超过255个字符")
+    public String getComponent()
+    {
+        return component;
+    }
+
+    public void setComponent(String component)
+    {
+        this.component = component;
+    }
+
+    public String getQuery()
+    {
+        return query;
+    }
+
+    public void setQuery(String query)
+    {
+        this.query = query;
+    }
+
+    public String getIsFrame()
+    {
+        return isFrame;
+    }
+
+    public void setIsFrame(String isFrame)
+    {
+        this.isFrame = isFrame;
+    }
+
+    public String getIsCache()
+    {
+        return isCache;
+    }
+
+    public void setIsCache(String isCache)
+    {
+        this.isCache = isCache;
+    }
+
+    @NotBlank(message = "菜单类型不能为空")
+    public String getMenuType()
+    {
+        return menuType;
+    }
+
+    public void setMenuType(String menuType)
+    {
+        this.menuType = menuType;
+    }
+
+    public String getVisible()
+    {
+        return visible;
+    }
+
+    public void setVisible(String visible)
+    {
+        this.visible = visible;
+    }
+
+    public String getStatus()
+    {
+        return status;
+    }
+
+    public void setStatus(String status)
+    {
+        this.status = status;
+    }
+
+    @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符")
+    public String getPerms()
+    {
+        return perms;
+    }
+
+    public void setPerms(String perms)
+    {
+        this.perms = perms;
+    }
+
+    public String getIcon()
+    {
+        return icon;
+    }
+
+    public void setIcon(String icon)
+    {
+        this.icon = icon;
+    }
+
+    public List<SysMenu> getChildren()
+    {
+        return children;
+    }
+
+    public void setChildren(List<SysMenu> children)
+    {
+        this.children = children;
+    }
+
+    public Map<String, Object> getParams()
+    {
+        if (params == null)
+        {
+            params = new HashMap<>();
+        }
+        return params;
+    }
+
+    public void setParams(Map<String, Object> params)
+    {
+        this.params = params;
+    }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
+            .append("menuId", getMenuId())
+            .append("menuName", getMenuName())
+            .append("parentId", getParentId())
+            .append("orderNum", getOrderNum())
+            .append("path", getPath())
+            .append("component", getComponent())
+            .append("isFrame", getIsFrame())
+            .append("IsCache", getIsCache())
+            .append("menuType", getMenuType())
+            .append("visible", getVisible())
+            .append("status ", getStatus())
+            .append("perms", getPerms())
+            .append("icon", getIcon())
+            .append("createBy", getCreateBy())
+            .append("createTime", getCreateTime())
+            .append("updateBy", getUpdateBy())
+            .append("updateTime", getUpdateTime())
+            .append("remark", getRemark())
+            .toString();
+    }
+}

+ 2 - 4
backend/src/main/java/com/jiayue/ssi/entity/SysParameter.java

@@ -1,6 +1,5 @@
 package com.jiayue.ssi.entity;
 
-
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -10,9 +9,8 @@ import lombok.EqualsAndHashCode;
 /**
  * 参数实体
  *
- * @author bizy
- * @version 1.0
- * @since 2018/10/22 11:29
+ * @author xsl
+ * @version 3.0
  */
 @EqualsAndHashCode(callSuper = true)
 @Data

+ 106 - 0
backend/src/main/java/com/jiayue/ssi/entity/vo/MetaVo.java

@@ -0,0 +1,106 @@
+package com.jiayue.ssi.entity.vo;
+
+import com.jiayue.ssi.util.StringUtils;
+
+/**
+ * 路由显示信息
+ *
+ * @author ruoyi
+ */
+public class MetaVo
+{
+    /**
+     * 设置该路由在侧边栏和面包屑中展示的名字
+     */
+    private String title;
+
+    /**
+     * 设置该路由的图标,对应路径src/assets/icons/svg
+     */
+    private String icon;
+
+    /**
+     * 设置为true,则不会被 <keep-alive>缓存
+     */
+    private boolean noCache;
+
+    /**
+     * 内链地址(http(s)://开头)
+     */
+    private String link;
+
+    public MetaVo()
+    {
+    }
+
+    public MetaVo(String title, String icon)
+    {
+        this.title = title;
+        this.icon = icon;
+    }
+
+    public MetaVo(String title, String icon, boolean noCache)
+    {
+        this.title = title;
+        this.icon = icon;
+        this.noCache = noCache;
+    }
+
+    public MetaVo(String title, String icon, String link)
+    {
+        this.title = title;
+        this.icon = icon;
+        this.link = link;
+    }
+
+    public MetaVo(String title, String icon, boolean noCache, String link)
+    {
+        this.title = title;
+        this.icon = icon;
+        this.noCache = noCache;
+        if (StringUtils.ishttp(link))
+        {
+            this.link = link;
+        }
+    }
+
+    public boolean isNoCache()
+    {
+        return noCache;
+    }
+
+    public void setNoCache(boolean noCache)
+    {
+        this.noCache = noCache;
+    }
+
+    public String getTitle()
+    {
+        return title;
+    }
+
+    public void setTitle(String title)
+    {
+        this.title = title;
+    }
+
+    public String getIcon()
+    {
+        return icon;
+    }
+
+    public void setIcon(String icon)
+    {
+        this.icon = icon;
+    }
+
+    public String getLink()
+    {
+        return link;
+    }
+
+    public void setLink(String link)
+    {
+        this.link = link;
+    }
+}

+ 149 - 0
backend/src/main/java/com/jiayue/ssi/entity/vo/RouterVo.java

@@ -0,0 +1,149 @@
+package com.jiayue.ssi.entity.vo;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+/**
+ * 路由配置信息
+ *
+ * @author ruoyi
+ */
+@JsonInclude(JsonInclude.Include.NON_EMPTY)
+public class RouterVo
+{
+    /**
+     * 路由名字
+     */
+    private String name;
+
+    /**
+     * 路由地址
+     */
+    private String path;
+
+    /**
+     * 是否隐藏路由,当设置 true 的时候该路由不会再侧边栏出现
+     */
+    private boolean hidden;
+
+    /**
+     * 重定向地址,当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
+     */
+    private String redirect;
+
+    /**
+     * 组件地址
+     */
+    private String component;
+
+    /**
+     * 路由参数:如 {"id": 1, "name": "ry"}
+     */
+    private String query;
+
+    /**
+     * 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
+     */
+    private Boolean alwaysShow;
+
+    /**
+     * 其他元素
+     */
+    private MetaVo meta;
+
+    /**
+     * 子路由
+     */
+    private List<RouterVo> children;
+
+    public String getName()
+    {
+        return name;
+    }
+
+    public void setName(String name)
+    {
+        this.name = name;
+    }
+
+    public String getPath()
+    {
+        return path;
+    }
+
+    public void setPath(String path)
+    {
+        this.path = path;
+    }
+
+    public boolean getHidden()
+    {
+        return hidden;
+    }
+
+    public void setHidden(boolean hidden)
+    {
+        this.hidden = hidden;
+    }
+
+    public String getRedirect()
+    {
+        return redirect;
+    }
+
+    public void setRedirect(String redirect)
+    {
+        this.redirect = redirect;
+    }
+
+    public String getComponent()
+    {
+        return component;
+    }
+
+    public void setComponent(String component)
+    {
+        this.component = component;
+    }
+
+    public String getQuery()
+    {
+        return query;
+    }
+
+    public void setQuery(String query)
+    {
+        this.query = query;
+    }
+
+    public Boolean getAlwaysShow()
+    {
+        return alwaysShow;
+    }
+
+    public void setAlwaysShow(Boolean alwaysShow)
+    {
+        this.alwaysShow = alwaysShow;
+    }
+
+    public MetaVo getMeta()
+    {
+        return meta;
+    }
+
+    public void setMeta(MetaVo meta)
+    {
+        this.meta = meta;
+    }
+
+    public List<RouterVo> getChildren()
+    {
+        return children;
+    }
+
+    public void setChildren(List<RouterVo> children)
+    {
+        this.children = children;
+    }
+}

+ 131 - 0
backend/src/main/java/com/jiayue/ssi/mapper/SysMenuMapper.java

@@ -0,0 +1,131 @@
+package com.jiayue.ssi.mapper;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jiayue.ssi.entity.SysMenu;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * <p>
+ *  菜单管理Mapper
+ * </p>
+ *
+ * @author xsl
+ * @since 2023-03-17
+ */
+@Mapper
+public interface SysMenuMapper  extends BaseMapper<SysMenu> {
+    /**
+     * 查询系统菜单列表
+     *
+     * @param menu 菜单信息
+     * @return 菜单列表
+     */
+    public List<SysMenu> selectMenuList(SysMenu menu);
+
+    /**
+     * 根据用户所有权限
+     *
+     * @return 权限列表
+     */
+    public List<String> selectMenuPerms();
+
+    /**
+     * 根据用户查询系统菜单列表
+     *
+     * @param menu 菜单信息
+     * @return 菜单列表
+     */
+    public List<SysMenu> selectMenuListByUserId(SysMenu menu);
+
+    /**
+     * 根据角色ID查询权限
+     *
+     * @param roleId 角色ID
+     * @return 权限列表
+     */
+    public List<String> selectMenuPermsByRoleId(Long roleId);
+
+    /**
+     * 根据用户ID查询权限
+     *
+     * @param userId 用户ID
+     * @return 权限列表
+     */
+    public List<String> selectMenuPermsByUserId(Long userId);
+
+    /**
+     * 根据用户ID查询菜单
+     *
+     * @return 菜单列表
+     */
+    public List<SysMenu> selectMenuTreeAll();
+
+    /**
+     * 根据用户ID查询菜单
+     *
+     * @param userId 用户ID
+     * @return 菜单列表
+     */
+    public List<SysMenu> selectMenuTreeByUserId(Integer userId);
+
+    /**
+     * 根据角色ID查询菜单树信息
+     *
+     * @param roleId 角色ID
+     * @param menuCheckStrictly 菜单树选择项是否关联显示
+     * @return 选中菜单列表
+     */
+    public List<Long> selectMenuListByRoleId(@Param("roleId") Long roleId, @Param("menuCheckStrictly") boolean menuCheckStrictly);
+
+    /**
+     * 根据菜单ID查询信息
+     *
+     * @param menuId 菜单ID
+     * @return 菜单信息
+     */
+    public SysMenu selectMenuById(Long menuId);
+
+    /**
+     * 是否存在菜单子节点
+     *
+     * @param menuId 菜单ID
+     * @return 结果
+     */
+    public int hasChildByMenuId(Long menuId);
+
+    /**
+     * 新增菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public int insertMenu(SysMenu menu);
+
+    /**
+     * 修改菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public int updateMenu(SysMenu menu);
+
+    /**
+     * 删除菜单管理信息
+     *
+     * @param menuId 菜单ID
+     * @return 结果
+     */
+    public int deleteMenuById(Long menuId);
+
+    /**
+     * 校验菜单名称是否唯一
+     *
+     * @param menuName 菜单名称
+     * @param parentId 父菜单ID
+     * @return 结果
+     */
+    public SysMenu checkMenuNameUnique(@Param("menuName") String menuName, @Param("parentId") Long parentId);
+}

+ 65 - 0
backend/src/main/java/com/jiayue/ssi/service/SysMenuService.java

@@ -0,0 +1,65 @@
+package com.jiayue.ssi.service;
+
+import java.util.List;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.jiayue.ssi.entity.SysMenu;
+import com.jiayue.ssi.entity.SysParameter;
+import com.jiayue.ssi.entity.vo.RouterVo;
+
+/**
+ * 菜单服务类
+ * @author xsl
+ * @date 2023/2/16
+ */
+public interface SysMenuService  extends IService<SysMenu> {
+    /**
+     * 根据用户ID查询菜单树信息
+     *
+     * @param userId 用户ID
+     * @return 菜单列表
+     */
+    List<SysMenu> selectMenuTreeByUserId(Integer userId);
+    /**
+     * 构建前端路由所需要的菜单
+     *
+     * @param menus 菜单列表
+     * @return 路由列表
+     */
+    List<RouterVo> buildMenus(List<SysMenu> menus);
+    /**
+     * 查询系统菜单列表
+     *
+     * @param menu 菜单信息
+     * @return 菜单列表
+     */
+    List<SysMenu> selectMenuList(SysMenu menu, Integer userId);
+    /**
+     * 校验菜单名称是否唯一
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    String checkMenuNameUnique(SysMenu menu);
+    /**
+     * 新增保存菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    int insertMenu(SysMenu menu);
+    /**
+     * 根据菜单ID查询信息
+     *
+     * @param menuId 菜单ID
+     * @return 菜单信息
+     */
+    SysMenu selectMenuById(Long menuId);
+    /**
+     * 修改保存菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    int updateMenu(SysMenu menu);
+}

+ 324 - 0
backend/src/main/java/com/jiayue/ssi/service/impl/SysMenuServiceImpl.java

@@ -0,0 +1,324 @@
+package com.jiayue.ssi.service.impl;
+
+import java.util.*;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.jiayue.ssi.constant.Constants;
+import com.jiayue.ssi.constant.UserConstants;
+import com.jiayue.ssi.entity.SysMenu;
+import com.jiayue.ssi.entity.SysParameter;
+import com.jiayue.ssi.entity.vo.MetaVo;
+import com.jiayue.ssi.entity.vo.RouterVo;
+import com.jiayue.ssi.mapper.SysMenuMapper;
+import com.jiayue.ssi.mapper.SysParameterMapper;
+import com.jiayue.ssi.service.SysMenuService;
+import com.jiayue.ssi.service.SysParameterService;
+import com.jiayue.ssi.util.SecurityContextUtil;
+import com.jiayue.ssi.util.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 菜单服务类
+ *
+ * @author xsl
+ * @date 2023/2/16
+ */
+@Service
+public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> implements SysMenuService {
+
+    @Autowired
+    SysMenuMapper menuMapper;
+
+    /**
+     * 构建前端路由所需要的菜单
+     *
+     * @param menus 菜单列表
+     * @return 路由列表
+     */
+    @Override
+    public List<RouterVo> buildMenus(List<SysMenu> menus) {
+        List<RouterVo> routers = new LinkedList<RouterVo>();
+        for (SysMenu menu : menus) {
+            RouterVo router = new RouterVo();
+            router.setHidden("1".equals(menu.getVisible()));
+            router.setName(getRouteName(menu));
+            router.setPath(getRouterPath(menu));
+            router.setComponent(getComponent(menu));
+            router.setQuery(menu.getQuery());
+            router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()),
+                menu.getPath()));
+            List<SysMenu> cMenus = menu.getChildren();
+            if (!cMenus.isEmpty() && cMenus.size() > 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType())) {
+                router.setAlwaysShow(true);
+                router.setRedirect("noRedirect");
+                router.setChildren(buildMenus(cMenus));
+            } else if (isMenuFrame(menu)) {
+                router.setMeta(null);
+                List<RouterVo> childrenList = new ArrayList<RouterVo>();
+                RouterVo children = new RouterVo();
+                children.setPath(menu.getPath());
+                children.setComponent(menu.getComponent());
+                children.setName(StringUtils.capitalize(menu.getPath()));
+                children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(),
+                    StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
+                children.setQuery(menu.getQuery());
+                childrenList.add(children);
+                router.setChildren(childrenList);
+            } else if (menu.getParentId().intValue() == 0 && isInnerLink(menu)) {
+                router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
+                router.setPath("/");
+                List<RouterVo> childrenList = new ArrayList<RouterVo>();
+                RouterVo children = new RouterVo();
+                String routerPath = innerLinkReplaceEach(menu.getPath());
+                children.setPath(routerPath);
+                children.setComponent(UserConstants.INNER_LINK);
+                children.setName(StringUtils.capitalize(routerPath));
+                children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
+                childrenList.add(children);
+                router.setChildren(childrenList);
+            }
+            routers.add(router);
+        }
+        return routers;
+    }
+
+    /**
+     * 获取路由地址
+     *
+     * @param menu 菜单信息
+     * @return 路由地址
+     */
+    public String getRouterPath(SysMenu menu) {
+        String routerPath = menu.getPath();
+        // 内链打开外网方式
+        if (menu.getParentId().intValue() != 0 && isInnerLink(menu)) {
+            routerPath = innerLinkReplaceEach(routerPath);
+        }
+        // 非外链并且是一级目录(类型为目录)
+        if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
+            && UserConstants.NO_FRAME.equals(menu.getIsFrame())) {
+            routerPath = "/" + menu.getPath();
+        }
+        // 非外链并且是一级目录(类型为菜单)
+        else if (isMenuFrame(menu)) {
+            routerPath = "/";
+        }
+        return routerPath;
+    }
+
+    /**
+     * 根据用户ID查询菜单
+     *
+     * @param userId 用户名称
+     * @return 菜单列表
+     */
+    @Override
+    public List<SysMenu> selectMenuTreeByUserId(Integer userId) {
+        List<SysMenu> menus = null;
+        if (userId == 1) {
+            menus = menuMapper.selectMenuTreeAll();
+        } else {
+            menus = menuMapper.selectMenuTreeByUserId(userId);
+        }
+        return getChildPerms(menus, 0);
+    }
+
+    /**
+     * 查询系统菜单列表
+     *
+     * @param menu 菜单信息
+     * @return 菜单列表
+     */
+    @Override
+    public List<SysMenu> selectMenuList(SysMenu menu, Integer userId) {
+        // 管理员显示所有菜单信息
+        return menuMapper.selectMenuList(menu);
+    }
+
+    /**
+     * 获取组件信息
+     *
+     * @param menu 菜单信息
+     * @return 组件信息
+     */
+    public String getComponent(SysMenu menu) {
+        String component = UserConstants.LAYOUT;
+        if (StringUtils.isNotEmpty(menu.getComponent()) && !isMenuFrame(menu)) {
+            component = menu.getComponent();
+        } else if (StringUtils.isEmpty(menu.getComponent()) && menu.getParentId().intValue() != 0
+            && isInnerLink(menu)) {
+            component = UserConstants.INNER_LINK;
+        } else if (StringUtils.isEmpty(menu.getComponent()) && isParentView(menu)) {
+            component = UserConstants.PARENT_VIEW;
+        }
+        return component;
+    }
+
+    /**
+     * 是否为parent_view组件
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public boolean isParentView(SysMenu menu) {
+        return menu.getParentId().intValue() != 0 && UserConstants.TYPE_DIR.equals(menu.getMenuType());
+    }
+
+    /**
+     * 是否为内链组件
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public boolean isInnerLink(SysMenu menu) {
+        return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath());
+    }
+
+    /**
+     * 是否为菜单内部跳转
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public boolean isMenuFrame(SysMenu menu) {
+        return menu.getParentId().intValue() == 0 && UserConstants.TYPE_MENU.equals(menu.getMenuType())
+            && menu.getIsFrame().equals(UserConstants.NO_FRAME);
+    }
+
+    /**
+     * 获取路由名称
+     *
+     * @param menu 菜单信息
+     * @return 路由名称
+     */
+    public String getRouteName(SysMenu menu) {
+        String routerName = StringUtils.capitalize(menu.getPath());
+        // 非外链并且是一级目录(类型为目录)
+        if (isMenuFrame(menu)) {
+            routerName = StringUtils.EMPTY;
+        }
+        return routerName;
+    }
+
+    /**
+     * 根据父节点的ID获取所有子节点
+     *
+     * @param list 分类表
+     * @param parentId 传入的父节点ID
+     * @return String
+     */
+    public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId) {
+        List<SysMenu> returnList = new ArrayList<SysMenu>();
+        for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();) {
+            SysMenu t = (SysMenu)iterator.next();
+            // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
+            if (t.getParentId() == parentId) {
+                recursionFn(list, t);
+                returnList.add(t);
+            }
+        }
+        return returnList;
+    }
+
+    /**
+     * 递归列表
+     *
+     * @param list 分类表
+     * @param t 子节点
+     */
+    private void recursionFn(List<SysMenu> list, SysMenu t) {
+        // 得到子节点列表
+        List<SysMenu> childList = getChildList(list, t);
+        t.setChildren(childList);
+        for (SysMenu tChild : childList) {
+            if (hasChild(list, tChild)) {
+                recursionFn(list, tChild);
+            }
+        }
+    }
+
+    /**
+     * 得到子节点列表
+     */
+    private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t) {
+        List<SysMenu> tlist = new ArrayList<SysMenu>();
+        Iterator<SysMenu> it = list.iterator();
+        while (it.hasNext()) {
+            SysMenu n = (SysMenu)it.next();
+            if (n.getParentId().longValue() == t.getMenuId().longValue()) {
+                tlist.add(n);
+            }
+        }
+        return tlist;
+    }
+
+    /**
+     * 判断是否有子节点
+     */
+    private boolean hasChild(List<SysMenu> list, SysMenu t) {
+        return getChildList(list, t).size() > 0;
+    }
+
+    /**
+     * 内链域名特殊字符替换
+     *
+     * @return 替换后的内链域名
+     */
+    public String innerLinkReplaceEach(String path) {
+        return StringUtils.replaceEach(path, new String[] {Constants.HTTP, Constants.HTTPS, Constants.WWW, "."},
+            new String[] {"", "", "", "/"});
+    }
+
+    /**
+     * 校验菜单名称是否唯一
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    @Override
+    public String checkMenuNameUnique(SysMenu menu) {
+        Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId();
+        SysMenu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId());
+        if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) {
+            return UserConstants.NOT_UNIQUE;
+        }
+        return UserConstants.UNIQUE;
+    }
+
+    /**
+     * 新增保存菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    @Override
+    public int insertMenu(SysMenu menu) {
+        menu.setCreateBy(SecurityContextUtil.getSysUser().getCreateBy());
+        return menuMapper.insertMenu(menu);
+    }
+
+    /**
+     * 修改保存菜单信息
+     *
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    @Override
+    public int updateMenu(SysMenu menu) {
+        menu.setUpdateBy(SecurityContextUtil.getSysUser().getCreateBy());
+        return menuMapper.updateMenu(menu);
+    }
+
+    /**
+     * 根据菜单ID查询信息
+     *
+     * @param menuId 菜单ID
+     * @return 菜单信息
+     */
+    @Override
+    public SysMenu selectMenuById(Long menuId) {
+
+        return menuMapper.selectMenuById(menuId);
+    }
+}

+ 610 - 0
backend/src/main/java/com/jiayue/ssi/util/StringUtils.java

@@ -0,0 +1,610 @@
+package com.jiayue.ssi.util;
+
+import java.util.*;
+
+import cn.hutool.core.text.StrFormatter;
+import com.jiayue.ssi.constant.Constants;
+import org.springframework.util.AntPathMatcher;
+
+
+/**
+ * 字符串工具类
+ *
+ * @author ruoyi
+ */
+public class StringUtils extends org.apache.commons.lang3.StringUtils
+{
+    /** 空字符串 */
+    private static final String NULLSTR = "";
+
+    /** 下划线 */
+    private static final char SEPARATOR = '_';
+
+    /**
+     * 获取参数不为空值
+     *
+     * @param value defaultValue 要判断的value
+     * @return value 返回值
+     */
+    public static <T> T nvl(T value, T defaultValue)
+    {
+        return value != null ? value : defaultValue;
+    }
+
+    /**
+     * * 判断一个Collection是否为空, 包含List,Set,Queue
+     *
+     * @param coll 要判断的Collection
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Collection<?> coll)
+    {
+        return isNull(coll) || coll.isEmpty();
+    }
+
+    /**
+     * * 判断一个Collection是否非空,包含List,Set,Queue
+     *
+     * @param coll 要判断的Collection
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Collection<?> coll)
+    {
+        return !isEmpty(coll);
+    }
+
+    /**
+     * * 判断一个对象数组是否为空
+     *
+     * @param objects 要判断的对象数组
+     ** @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Object[] objects)
+    {
+        return isNull(objects) || (objects.length == 0);
+    }
+
+    /**
+     * * 判断一个对象数组是否非空
+     *
+     * @param objects 要判断的对象数组
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Object[] objects)
+    {
+        return !isEmpty(objects);
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     *
+     * @param map 要判断的Map
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(Map<?, ?> map)
+    {
+        return isNull(map) || map.isEmpty();
+    }
+
+    /**
+     * * 判断一个Map是否为空
+     *
+     * @param map 要判断的Map
+     * @return true:非空 false:空
+     */
+    public static boolean isNotEmpty(Map<?, ?> map)
+    {
+        return !isEmpty(map);
+    }
+
+    /**
+     * * 判断一个字符串是否为空串
+     *
+     * @param str String
+     * @return true:为空 false:非空
+     */
+    public static boolean isEmpty(String str)
+    {
+        return isNull(str) || NULLSTR.equals(str.trim());
+    }
+
+    /**
+     * * 判断一个字符串是否为非空串
+     *
+     * @param str String
+     * @return true:非空串 false:空串
+     */
+    public static boolean isNotEmpty(String str)
+    {
+        return !isEmpty(str);
+    }
+
+    /**
+     * * 判断一个对象是否为空
+     *
+     * @param object Object
+     * @return true:为空 false:非空
+     */
+    public static boolean isNull(Object object)
+    {
+        return object == null;
+    }
+
+    /**
+     * * 判断一个对象是否非空
+     *
+     * @param object Object
+     * @return true:非空 false:空
+     */
+    public static boolean isNotNull(Object object)
+    {
+        return !isNull(object);
+    }
+
+    /**
+     * * 判断一个对象是否是数组类型(Java基本型别的数组)
+     *
+     * @param object 对象
+     * @return true:是数组 false:不是数组
+     */
+    public static boolean isArray(Object object)
+    {
+        return isNotNull(object) && object.getClass().isArray();
+    }
+
+    /**
+     * 去空格
+     */
+    public static String trim(String str)
+    {
+        return (str == null ? "" : str.trim());
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str 字符串
+     * @param start 开始
+     * @return 结果
+     */
+    public static String substring(final String str, int start)
+    {
+        if (str == null)
+        {
+            return NULLSTR;
+        }
+
+        if (start < 0)
+        {
+            start = str.length() + start;
+        }
+
+        if (start < 0)
+        {
+            start = 0;
+        }
+        if (start > str.length())
+        {
+            return NULLSTR;
+        }
+
+        return str.substring(start);
+    }
+
+    /**
+     * 截取字符串
+     *
+     * @param str 字符串
+     * @param start 开始
+     * @param end 结束
+     * @return 结果
+     */
+    public static String substring(final String str, int start, int end)
+    {
+        if (str == null)
+        {
+            return NULLSTR;
+        }
+
+        if (end < 0)
+        {
+            end = str.length() + end;
+        }
+        if (start < 0)
+        {
+            start = str.length() + start;
+        }
+
+        if (end > str.length())
+        {
+            end = str.length();
+        }
+
+        if (start > end)
+        {
+            return NULLSTR;
+        }
+
+        if (start < 0)
+        {
+            start = 0;
+        }
+        if (end < 0)
+        {
+            end = 0;
+        }
+
+        return str.substring(start, end);
+    }
+
+    /**
+     * 格式化文本, {} 表示占位符<br>
+     * 此方法只是简单将占位符 {} 按照顺序替换为参数<br>
+     * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可<br>
+     * 例:<br>
+     * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
+     * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
+     * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
+     *
+     * @param template 文本模板,被替换的部分用 {} 表示
+     * @param params 参数值
+     * @return 格式化后的文本
+     */
+    public static String format(String template, Object... params)
+    {
+        if (isEmpty(params) || isEmpty(template))
+        {
+            return template;
+        }
+        return StrFormatter.format(template, params);
+    }
+
+    /**
+     * 是否为http(s)://开头
+     *
+     * @param link 链接
+     * @return 结果
+     */
+    public static boolean ishttp(String link)
+    {
+        return StringUtils.startsWithAny(link, Constants.HTTP, Constants.HTTPS);
+    }
+
+    /**
+     * 字符串转set
+     *
+     * @param str 字符串
+     * @param sep 分隔符
+     * @return set集合
+     */
+    public static final Set<String> str2Set(String str, String sep)
+    {
+        return new HashSet<String>(str2List(str, sep, true, false));
+    }
+
+    /**
+     * 字符串转list
+     *
+     * @param str 字符串
+     * @param sep 分隔符
+     * @param filterBlank 过滤纯空白
+     * @param trim 去掉首尾空白
+     * @return list集合
+     */
+    public static final List<String> str2List(String str, String sep, boolean filterBlank, boolean trim)
+    {
+        List<String> list = new ArrayList<String>();
+        if (StringUtils.isEmpty(str))
+        {
+            return list;
+        }
+
+        // 过滤空白字符串
+        if (filterBlank && StringUtils.isBlank(str))
+        {
+            return list;
+        }
+        String[] split = str.split(sep);
+        for (String string : split)
+        {
+            if (filterBlank && StringUtils.isBlank(string))
+            {
+                continue;
+            }
+            if (trim)
+            {
+                string = string.trim();
+            }
+            list.add(string);
+        }
+
+        return list;
+    }
+
+    /**
+     * 判断给定的set列表中是否包含数组array 判断给定的数组array中是否包含给定的元素value
+     *
+     * @param array 给定的数组
+     * @return boolean 结果
+     */
+    public static boolean containsAny(Collection<String> collection, String... array)
+    {
+        if (isEmpty(collection) || isEmpty(array))
+        {
+            return false;
+        }
+        else
+        {
+            for (String str : array)
+            {
+                if (collection.contains(str))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    /**
+     * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写
+     *
+     * @param cs 指定字符串
+     * @param searchCharSequences 需要检查的字符串数组
+     * @return 是否包含任意一个字符串
+     */
+    public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences)
+    {
+        if (isEmpty(cs) || isEmpty(searchCharSequences))
+        {
+            return false;
+        }
+        for (CharSequence testStr : searchCharSequences)
+        {
+            if (containsIgnoreCase(cs, testStr))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 驼峰转下划线命名
+     */
+    public static String toUnderScoreCase(String str)
+    {
+        if (str == null)
+        {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder();
+        // 前置字符是否大写
+        boolean preCharIsUpperCase = true;
+        // 当前字符是否大写
+        boolean curreCharIsUpperCase = true;
+        // 下一字符是否大写
+        boolean nexteCharIsUpperCase = true;
+        for (int i = 0; i < str.length(); i++)
+        {
+            char c = str.charAt(i);
+            if (i > 0)
+            {
+                preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1));
+            }
+            else
+            {
+                preCharIsUpperCase = false;
+            }
+
+            curreCharIsUpperCase = Character.isUpperCase(c);
+
+            if (i < (str.length() - 1))
+            {
+                nexteCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1));
+            }
+
+            if (preCharIsUpperCase && curreCharIsUpperCase && !nexteCharIsUpperCase)
+            {
+                sb.append(SEPARATOR);
+            }
+            else if ((i != 0 && !preCharIsUpperCase) && curreCharIsUpperCase)
+            {
+                sb.append(SEPARATOR);
+            }
+            sb.append(Character.toLowerCase(c));
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * 是否包含字符串
+     *
+     * @param str 验证字符串
+     * @param strs 字符串组
+     * @return 包含返回true
+     */
+    public static boolean inStringIgnoreCase(String str, String... strs)
+    {
+        if (str != null && strs != null)
+        {
+            for (String s : strs)
+            {
+                if (str.equalsIgnoreCase(trim(s)))
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串。 例如:HELLO_WORLD->HelloWorld
+     *
+     * @param name 转换前的下划线大写方式命名的字符串
+     * @return 转换后的驼峰式命名的字符串
+     */
+    public static String convertToCamelCase(String name)
+    {
+        StringBuilder result = new StringBuilder();
+        // 快速检查
+        if (name == null || name.isEmpty())
+        {
+            // 没必要转换
+            return "";
+        }
+        else if (!name.contains("_"))
+        {
+            // 不含下划线,仅将首字母大写
+            return name.substring(0, 1).toUpperCase() + name.substring(1);
+        }
+        // 用下划线将原始字符串分割
+        String[] camels = name.split("_");
+        for (String camel : camels)
+        {
+            // 跳过原始字符串中开头、结尾的下换线或双重下划线
+            if (camel.isEmpty())
+            {
+                continue;
+            }
+            // 首字母大写
+            result.append(camel.substring(0, 1).toUpperCase());
+            result.append(camel.substring(1).toLowerCase());
+        }
+        return result.toString();
+    }
+
+    /**
+     * 驼峰式命名法
+     * 例如:user_name->userName
+     */
+    public static String toCamelCase(String s)
+    {
+        if (s == null)
+        {
+            return null;
+        }
+        if (s.indexOf(SEPARATOR) == -1)
+        {
+            return s;
+        }
+        s = s.toLowerCase();
+        StringBuilder sb = new StringBuilder(s.length());
+        boolean upperCase = false;
+        for (int i = 0; i < s.length(); i++)
+        {
+            char c = s.charAt(i);
+
+            if (c == SEPARATOR)
+            {
+                upperCase = true;
+            }
+            else if (upperCase)
+            {
+                sb.append(Character.toUpperCase(c));
+                upperCase = false;
+            }
+            else
+            {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串
+     *
+     * @param str 指定字符串
+     * @param strs 需要检查的字符串数组
+     * @return 是否匹配
+     */
+    public static boolean matches(String str, List<String> strs)
+    {
+        if (isEmpty(str) || isEmpty(strs))
+        {
+            return false;
+        }
+        for (String pattern : strs)
+        {
+            if (isMatch(pattern, str))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 判断url是否与规则配置:
+     * ? 表示单个字符;
+     * * 表示一层路径内的任意字符串,不可跨层级;
+     * ** 表示任意层路径;
+     *
+     * @param pattern 匹配规则
+     * @param url 需要匹配的url
+     * @return
+     */
+    public static boolean isMatch(String pattern, String url)
+    {
+        AntPathMatcher matcher = new AntPathMatcher();
+        return matcher.match(pattern, url);
+    }
+
+    @SuppressWarnings("unchecked")
+    public static <T> T cast(Object obj)
+    {
+        return (T) obj;
+    }
+
+    /**
+     * 数字左边补齐0,使之达到指定长度。注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。
+     *
+     * @param num 数字对象
+     * @param size 字符串指定长度
+     * @return 返回数字的字符串格式,该字符串为指定长度。
+     */
+    public static final String padl(final Number num, final int size)
+    {
+        return padl(num.toString(), size, '0');
+    }
+
+    /**
+     * 字符串左补齐。如果原始字符串s长度大于size,则只保留最后size个字符。
+     *
+     * @param s 原始字符串
+     * @param size 字符串指定长度
+     * @param c 用于补齐的字符
+     * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。
+     */
+    public static final String padl(final String s, final int size, final char c)
+    {
+        final StringBuilder sb = new StringBuilder(size);
+        if (s != null)
+        {
+            final int len = s.length();
+            if (s.length() <= size)
+            {
+                for (int i = size - len; i > 0; i--)
+                {
+                    sb.append(c);
+                }
+                sb.append(s);
+            }
+            else
+            {
+                return s.substring(len - size, len);
+            }
+        }
+        else
+        {
+            for (int i = size; i > 0; i--)
+            {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+}

+ 14 - 12
backend/src/main/resources/application.yml

@@ -60,21 +60,23 @@ spring:
 #    #数据库大写命名下划线转换
 #    #capital-mode: true
 
-mybatis:
-  table:
-    auto: update
-    #create	    系统启动后,会将所有的表删除掉,然后根据model中配置的结构重新建表,该操作会破坏原有数据。
-    #update	    系统会自动判断哪些表是新建的,哪些字段要修改类型等,哪些字段要删除,哪些字段要新增,该操作不会破坏原有数据。
-    #none 		系统不做任何处理。
-    #add		新增表/新增字段/新增索引/新增唯一约束的功能,不做做修改和删除 (只在版本1.0.9.RELEASE及以上支持)。
-  model:
-    pack: com.ssqmybatis.entity #扫描用于创建表的对象的包名,多个包用“,”隔开
-  database:
-    type: mysql #数据库类型 目前只支持mysql
+#mybatis:
+#  table:
+#    auto: update
+#    #create	    系统启动后,会将所有的表删除掉,然后根据model中配置的结构重新建表,该操作会破坏原有数据。
+#    #update	    系统会自动判断哪些表是新建的,哪些字段要修改类型等,哪些字段要删除,哪些字段要新增,该操作不会破坏原有数据。
+#    #none 		系统不做任何处理。
+#    #add		新增表/新增字段/新增索引/新增唯一约束的功能,不做做修改和删除 (只在版本1.0.9.RELEASE及以上支持)。
+#  model:
+#    pack: com.ssqmybatis.entity #扫描用于创建表的对象的包名,多个包用“,”隔开
+#  database:
+#    type: mysql #数据库类型 目前只支持mysql
+#  mapper-locations: classpath:mapper/*.xml
 mybatis-plus:
+  typeAliasesPackage: com.jiayue.ssi.entity
   configuration:
     log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
-  mapper-locations: classpath:/mapper/*Mapper.xml
+  mapper-locations: classpath:mapper/system/*.xml
   global-config:
     db-column-underline: true
     logic-delete-field: del_flag

+ 202 - 0
backend/src/main/resources/mapper/system/SysMenuMapper.xml

@@ -0,0 +1,202 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+		PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+		"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jiayue.ssi.mapper.SysMenuMapper">
+
+	<resultMap type="SysMenu" id="SysMenuResult">
+		<id     property="menuId"         column="menu_id"        />
+		<result property="menuName"       column="menu_name"      />
+		<result property="parentName"     column="parent_name"    />
+		<result property="parentId"       column="parent_id"      />
+		<result property="orderNum"       column="order_num"      />
+		<result property="path"           column="path"           />
+		<result property="component"      column="component"      />
+		<result property="query"          column="query"          />
+		<result property="isFrame"        column="is_frame"       />
+		<result property="isCache"        column="is_cache"       />
+		<result property="menuType"       column="menu_type"      />
+		<result property="visible"        column="visible"        />
+		<result property="status"         column="status"         />
+		<result property="perms"          column="perms"          />
+		<result property="icon"           column="icon"           />
+		<result property="createBy"       column="create_by"      />
+		<result property="createTime"     column="create_time"    />
+		<result property="updateTime"     column="update_time"    />
+		<result property="updateBy"       column="update_by"      />
+		<result property="remark"         column="remark"         />
+	</resultMap>
+
+	<sql id="selectMenuVo">
+        select menu_id, menu_name, parent_id, order_num, path, component, `query`, is_frame, is_cache, menu_type, visible, status, ifnull(perms,'') as perms, icon, create_time
+		from sys_menu
+    </sql>
+
+    <select id="selectMenuList" parameterType="SysMenu" resultMap="SysMenuResult">
+		<include refid="selectMenuVo"/>
+		<where>
+			<if test="menuName != null and menuName != ''">
+				AND menu_name like concat('%', #{menuName}, '%')
+			</if>
+			<if test="visible != null and visible != ''">
+				AND visible = #{visible}
+			</if>
+			<if test="status != null and status != ''">
+				AND status = #{status}
+			</if>
+		</where>
+		order by parent_id, order_num
+	</select>
+
+	<select id="selectMenuTreeAll" resultMap="SysMenuResult">
+		select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+		from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0
+		order by m.parent_id, m.order_num
+	</select>
+
+	<select id="selectMenuListByUserId" parameterType="SysMenu" resultMap="SysMenuResult">
+		select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+		from sys_menu m
+		left join sys_role_menu rm on m.menu_id = rm.menu_id
+		left join sys_user_role ur on rm.role_id = ur.role_id
+		left join sys_role ro on ur.role_id = ro.role_id
+		where ur.user_id = #{params.userId}
+		<if test="menuName != null and menuName != ''">
+            AND m.menu_name like concat('%', #{menuName}, '%')
+		</if>
+		<if test="visible != null and visible != ''">
+            AND m.visible = #{visible}
+		</if>
+		<if test="status != null and status != ''">
+            AND m.status = #{status}
+		</if>
+		order by m.parent_id, m.order_num
+	</select>
+
+    <select id="selectMenuTreeByUserId" parameterType="Long" resultMap="SysMenuResult">
+		select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+		from sys_menu m
+			 left join sys_role_menu rm on m.menu_id = rm.menu_id
+			 left join sys_user_role ur on rm.role_id = ur.role_id
+			 left join sys_role ro on ur.role_id = ro.role_id
+			 left join sys_user u on ur.user_id = u.user_id
+		where u.user_id = #{userId} and m.menu_type in ('M', 'C') and m.status = 0  AND ro.status = 0
+		order by m.parent_id, m.order_num
+	</select>
+
+	<select id="selectMenuListByRoleId" resultType="Long">
+		select m.menu_id
+		from sys_menu m
+            left join sys_role_menu rm on m.menu_id = rm.menu_id
+        where rm.role_id = #{roleId}
+            <if test="menuCheckStrictly">
+              and m.menu_id not in (select m.parent_id from sys_menu m inner join sys_role_menu rm on m.menu_id = rm.menu_id and rm.role_id = #{roleId})
+            </if>
+		order by m.parent_id, m.order_num
+	</select>
+
+	<select id="selectMenuPerms" resultType="String">
+		select distinct m.perms
+		from sys_menu m
+			 left join sys_role_menu rm on m.menu_id = rm.menu_id
+			 left join sys_user_role ur on rm.role_id = ur.role_id
+	</select>
+
+	<select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">
+		select distinct m.perms
+		from sys_menu m
+			 left join sys_role_menu rm on m.menu_id = rm.menu_id
+			 left join sys_user_role ur on rm.role_id = ur.role_id
+			 left join sys_role r on r.role_id = ur.role_id
+		where m.status = '0' and r.status = '0' and ur.user_id = #{userId}
+	</select>
+
+	<select id="selectMenuPermsByRoleId" parameterType="Long" resultType="String">
+		select distinct m.perms
+		from sys_menu m
+			 left join sys_role_menu rm on m.menu_id = rm.menu_id
+		where m.status = '0' and rm.role_id = #{roleId}
+	</select>
+
+	<select id="selectMenuById" parameterType="Long" resultMap="SysMenuResult">
+		<include refid="selectMenuVo"/>
+		where menu_id = #{menuId}
+	</select>
+
+	<select id="hasChildByMenuId" resultType="Long">
+	    select count(1) from sys_menu where parent_id = #{menuId}
+	</select>
+
+	<select id="checkMenuNameUnique" parameterType="SysMenu" resultMap="SysMenuResult">
+		<include refid="selectMenuVo"/>
+		where menu_name=#{menuName} and parent_id = #{parentId} limit 1
+	</select>
+
+	<update id="updateMenu" parameterType="SysMenu">
+		update sys_menu
+		<set>
+			<if test="menuName != null and menuName != ''">menu_name = #{menuName},</if>
+			<if test="parentId != null">parent_id = #{parentId},</if>
+			<if test="orderNum != null">order_num = #{orderNum},</if>
+			<if test="path != null and path != ''">path = #{path},</if>
+			<if test="component != null">component = #{component},</if>
+			<if test="query != null">`query` = #{query},</if>
+			<if test="isFrame != null and isFrame != ''">is_frame = #{isFrame},</if>
+			<if test="isCache != null and isCache != ''">is_cache = #{isCache},</if>
+			<if test="menuType != null and menuType != ''">menu_type = #{menuType},</if>
+			<if test="visible != null">visible = #{visible},</if>
+			<if test="status != null">status = #{status},</if>
+			<if test="perms !=null">perms = #{perms},</if>
+			<if test="icon !=null and icon != ''">icon = #{icon},</if>
+			<if test="remark != null and remark != ''">remark = #{remark},</if>
+			<if test="updateBy != null and updateBy != ''">update_by = #{updateBy},</if>
+			update_time = sysdate()
+		</set>
+		where menu_id = #{menuId}
+	</update>
+
+	<insert id="insertMenu" parameterType="SysMenu">
+		insert into sys_menu(
+		<if test="menuId != null and menuId != 0">menu_id,</if>
+		<if test="parentId != null and parentId != 0">parent_id,</if>
+		<if test="menuName != null and menuName != ''">menu_name,</if>
+		<if test="orderNum != null">order_num,</if>
+		<if test="path != null and path != ''">path,</if>
+		<if test="component != null and component != ''">component,</if>
+		<if test="query != null and query != ''">`query`,</if>
+		<if test="isFrame != null and isFrame != ''">is_frame,</if>
+		<if test="isCache != null and isCache != ''">is_cache,</if>
+		<if test="menuType != null and menuType != ''">menu_type,</if>
+		<if test="visible != null">visible,</if>
+		<if test="status != null">status,</if>
+		<if test="perms !=null and perms != ''">perms,</if>
+		<if test="icon != null and icon != ''">icon,</if>
+		<if test="remark != null and remark != ''">remark,</if>
+		<if test="createBy != null and createBy != ''">create_by,</if>
+		create_time
+		)values(
+		<if test="menuId != null and menuId != 0">#{menuId},</if>
+		<if test="parentId != null and parentId != 0">#{parentId},</if>
+		<if test="menuName != null and menuName != ''">#{menuName},</if>
+		<if test="orderNum != null">#{orderNum},</if>
+		<if test="path != null and path != ''">#{path},</if>
+		<if test="component != null and component != ''">#{component},</if>
+		<if test="query != null and query != ''">#{query},</if>
+		<if test="isFrame != null and isFrame != ''">#{isFrame},</if>
+		<if test="isCache != null and isCache != ''">#{isCache},</if>
+		<if test="menuType != null and menuType != ''">#{menuType},</if>
+		<if test="visible != null">#{visible},</if>
+		<if test="status != null">#{status},</if>
+		<if test="perms !=null and perms != ''">#{perms},</if>
+		<if test="icon != null and icon != ''">#{icon},</if>
+		<if test="remark != null and remark != ''">#{remark},</if>
+		<if test="createBy != null and createBy != ''">#{createBy},</if>
+		sysdate()
+		)
+	</insert>
+
+	<delete id="deleteMenuById" parameterType="Long">
+	    delete from sys_menu where menu_id = #{menuId}
+	</delete>
+
+</mapper>

+ 1 - 0
ui/package.json

@@ -16,6 +16,7 @@
     "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml"
   },
   "dependencies": {
+    "@riophae/vue-treeselect": "0.4.0",
     "axios": "0.18.1",
     "echarts": "^4.9.0",
     "element-ui": "2.13.0",

+ 23 - 0
ui/src/directive/index.js

@@ -0,0 +1,23 @@
+// import hasRole from './permission/hasRole'
+import hasPermi from './permission/hasPermi'
+// import dialogDrag from './dialog/drag'
+// import dialogDragWidth from './dialog/dragWidth'
+// import dialogDragHeight from './dialog/dragHeight'
+// import clipboard from './module/clipboard'
+
+const install = function(Vue) {
+  // Vue.directive('hasRole', hasRole)
+  Vue.directive('hasPermi', hasPermi)
+  // Vue.directive('clipboard', clipboard)
+  // Vue.directive('dialogDrag', dialogDrag)
+  // Vue.directive('dialogDragWidth', dialogDragWidth)
+  // Vue.directive('dialogDragHeight', dialogDragHeight)
+}
+
+if (window.Vue) {
+  // window['hasRole'] = hasRole
+  window['hasPermi'] = hasPermi
+  Vue.use(install); // eslint-disable-line
+}
+
+export default install

+ 28 - 0
ui/src/directive/permission/hasPermi.js

@@ -0,0 +1,28 @@
+ /**
+ * v-hasPermi 操作权限处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+import store from '@/store'
+
+export default {
+  inserted(el, binding, vnode) {
+    const { value } = binding
+    const all_permission = "*:*:*";
+    // const permissions = store.getters && store.getters.permissions
+    const permissions = ["0","1","3","4"]
+    if (value && value instanceof Array && value.length > 0) {
+      const permissionFlag = value
+
+      const hasPermissions = permissions.some(permission => {
+        return all_permission === permission || permissionFlag.includes(permission)
+      })
+
+      if (!hasPermissions) {
+        el.parentNode && el.parentNode.removeChild(el)
+      }
+    } else {
+      throw new Error(`请设置操作权限标签值`)
+    }
+  }
+}

+ 4 - 3
ui/src/main.js

@@ -21,13 +21,14 @@ import VXETable from 'vxe-table'
 import 'vxe-table/lib/index.css'
 
 import {removeToken} from './utils/auth'
-import { resetForm} from "@/utils/index"
-
+// import { resetForm} from "@/utils/index"
+import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi"
 import {sm2 as sm5, sm2 as sm4, sm2 as sm3, sm2} from "sm-crypto"
 Vue.prototype.$moment = moment
 Vue.prototype.$echarts = echarts
 Vue.use(VXETable)
 Vue.use(plugins)
+Vue.prototype.handleTree = handleTree
 /**
  * If you don't want to use mock-server
  * you want to use MockJs for mock api
@@ -57,7 +58,7 @@ VXETable.setup({
 })
 
 Vue.prototype.resetForm = resetForm
-
+Vue.prototype.parseTime = parseTime
 // 公钥Q
 let publicKey2 = '041967638ca43d4577d8dba166bff4437fde944270101f398a95b846ec2f8177d09f8abc5d62b6cd2c7216274d7abe0c8e04b0bb691207a32dd2e12d6bd2798672'
 // 私钥D

+ 7 - 0
ui/src/router/index.js

@@ -72,6 +72,13 @@ export const constantRoutes = [
         component: () => import('@/views/sysManager/sysParameter/index'),
         meta: {title: '参数管理'}
       }
+      ,
+      {
+        path: 'sysMenu',
+        name: '菜单管理',
+        component: () => import('@/views/sysManager/sysMenu/index'),
+        meta: {title: '菜单管理'}
+      }
     ]
   },
   {

+ 5 - 1
ui/src/store/index.js

@@ -6,6 +6,9 @@ import settings from './modules/settings'
 import user from './modules/user'
 import tagsView from './modules/tagsView'
 import createPersistedState from 'vuex-persistedstate'
+// import permission from './modules/permission'
+
+
 Vue.use(Vuex)
 
 const store = new Vuex.Store({
@@ -13,7 +16,8 @@ const store = new Vuex.Store({
     app,
     settings,
     user,
-    tagsView
+    tagsView,
+    // permission
   },
   getters,
   plugins:[

+ 133 - 0
ui/src/store/modules/permission.js

@@ -0,0 +1,133 @@
+// import auth from '@/plugins/auth'
+// import router, { constantRoutes, dynamicRoutes } from '@/router'
+// import { getRouters } from '@/api/menu'
+// import Layout from '@/layout/index'
+// import ParentView from '@/components/ParentView'
+// import InnerLink from '@/layout/components/InnerLink'
+//
+// const permission = {
+//   state: {
+//     routes: [],
+//     addRoutes: [],
+//     defaultRoutes: [],
+//     topbarRouters: [],
+//     sidebarRouters: []
+//   },
+//   mutations: {
+//     SET_ROUTES: (state, routes) => {
+//       state.addRoutes = routes
+//       state.routes = constantRoutes.concat(routes)
+//     },
+//     SET_DEFAULT_ROUTES: (state, routes) => {
+//       state.defaultRoutes = constantRoutes.concat(routes)
+//     },
+//     SET_TOPBAR_ROUTES: (state, routes) => {
+//       state.topbarRouters = routes
+//     },
+//     SET_SIDEBAR_ROUTERS: (state, routes) => {
+//       state.sidebarRouters = routes
+//     },
+//   },
+//   actions: {
+//     // 生成路由
+//     GenerateRoutes({ commit }) {
+//       return new Promise(resolve => {
+//         // 向后端请求路由数据
+//         getRouters().then(res => {
+//           const sdata = JSON.parse(JSON.stringify(res.data))
+//           const rdata = JSON.parse(JSON.stringify(res.data))
+//           const sidebarRoutes = filterAsyncRouter(sdata)
+//           const rewriteRoutes = filterAsyncRouter(rdata, false, true)
+//           const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
+//           rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
+//           router.addRoutes(asyncRoutes);
+//           commit('SET_ROUTES', rewriteRoutes)
+//           commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
+//           commit('SET_DEFAULT_ROUTES', sidebarRoutes)
+//           commit('SET_TOPBAR_ROUTES', sidebarRoutes)
+//           resolve(rewriteRoutes)
+//         })
+//       })
+//     }
+//   }
+// }
+//
+// // 遍历后台传来的路由字符串,转换为组件对象
+// function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
+//   return asyncRouterMap.filter(route => {
+//     if (type && route.children) {
+//       route.children = filterChildren(route.children)
+//     }
+//     if (route.component) {
+//       // Layout ParentView 组件特殊处理
+//       if (route.component === 'Layout') {
+//         route.component = Layout
+//       } else if (route.component === 'ParentView') {
+//         route.component = ParentView
+//       } else if (route.component === 'InnerLink') {
+//         route.component = InnerLink
+//       } else {
+//         route.component = loadView(route.component)
+//       }
+//     }
+//     if (route.children != null && route.children && route.children.length) {
+//       route.children = filterAsyncRouter(route.children, route, type)
+//     } else {
+//       delete route['children']
+//       delete route['redirect']
+//     }
+//     return true
+//   })
+// }
+//
+// function filterChildren(childrenMap, lastRouter = false) {
+//   var children = []
+//   childrenMap.forEach((el, index) => {
+//     if (el.children && el.children.length) {
+//       if (el.component === 'ParentView' && !lastRouter) {
+//         el.children.forEach(c => {
+//           c.path = el.path + '/' + c.path
+//           if (c.children && c.children.length) {
+//             children = children.concat(filterChildren(c.children, c))
+//             return
+//           }
+//           children.push(c)
+//         })
+//         return
+//       }
+//     }
+//     if (lastRouter) {
+//       el.path = lastRouter.path + '/' + el.path
+//     }
+//     children = children.concat(el)
+//   })
+//   return children
+// }
+//
+// // 动态路由遍历,验证是否具备权限
+// export function filterDynamicRoutes(routes) {
+//   const res = []
+//   routes.forEach(route => {
+//     if (route.permissions) {
+//       if (auth.hasPermiOr(route.permissions)) {
+//         res.push(route)
+//       }
+//     } else if (route.roles) {
+//       if (auth.hasRoleOr(route.roles)) {
+//         res.push(route)
+//       }
+//     }
+//   })
+//   return res
+// }
+//
+// // export const loadView = (view) => {
+// //   if (process.env.NODE_ENV === 'development') {
+// //     return (resolve) => require([`@/views/${view}`], resolve)
+// //   } else {
+// //     // 使用 import 实现生产环境的路由懒加载
+// //     return () => import(`@/views/${view}`)
+// //   }
+// // }
+//
+// export default permission

+ 239 - 0
ui/src/utils/ruoyi.js

@@ -0,0 +1,239 @@
+
+
+/**
+ * 通用js方法封装处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+// 日期格式化
+export function parseTime(time, pattern) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+      time = parseInt(time)
+    } else if (typeof time === 'string') {
+      time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
+    }
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+    let value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+    if (result.length > 0 && value < 10) {
+      value = '0' + value
+    }
+    return value || 0
+  })
+  return time_str
+}
+
+// 表单重置
+export function resetForm(refName) {
+  if (this.$refs[refName]) {
+    this.$refs[refName].resetFields();
+  }
+}
+
+// 添加日期范围
+export function addDateRange(params, dateRange, propName) {
+  let search = params;
+  search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
+  dateRange = Array.isArray(dateRange) ? dateRange : [];
+  if (typeof (propName) === 'undefined') {
+    search.params['beginTime'] = dateRange[0];
+    search.params['endTime'] = dateRange[1];
+  } else {
+    search.params['begin' + propName] = dateRange[0];
+    search.params['end' + propName] = dateRange[1];
+  }
+  return search;
+}
+
+// 回显数据字典
+export function selectDictLabel(datas, value) {
+  if (value === undefined) {
+    return "";
+  }
+  var actions = [];
+  Object.keys(datas).some((key) => {
+    if (datas[key].value == ('' + value)) {
+      actions.push(datas[key].label);
+      return true;
+    }
+  })
+  if (actions.length === 0) {
+    actions.push(value);
+  }
+  return actions.join('');
+}
+
+// 回显数据字典(字符串、数组)
+export function selectDictLabels(datas, value, separator) {
+  if (value === undefined || value.length ===0) {
+    return "";
+  }
+  if (Array.isArray(value)) {
+    value = value.join(",");
+  }
+  var actions = [];
+  var currentSeparator = undefined === separator ? "," : separator;
+  var temp = value.split(currentSeparator);
+  Object.keys(value.split(currentSeparator)).some((val) => {
+    var match = false;
+    Object.keys(datas).some((key) => {
+      if (datas[key].value == ('' + temp[val])) {
+        actions.push(datas[key].label + currentSeparator);
+        match = true;
+      }
+    })
+    if (!match) {
+      actions.push(temp[val] + currentSeparator);
+    }
+  })
+  return actions.join('').substring(0, actions.join('').length - 1);
+}
+
+// 字符串格式化(%s )
+export function sprintf(str) {
+  var args = arguments, flag = true, i = 1;
+  str = str.replace(/%s/g, function () {
+    var arg = args[i++];
+    if (typeof arg === 'undefined') {
+      flag = false;
+      return '';
+    }
+    return arg;
+  });
+  return flag ? str : '';
+}
+
+// 转换字符串,undefined,null等转化为""
+export function parseStrEmpty(str) {
+  if (!str || str == "undefined" || str == "null") {
+    return "";
+  }
+  return str;
+}
+
+// 数据合并
+export function mergeRecursive(source, target) {
+  for (var p in target) {
+    try {
+      if (target[p].constructor == Object) {
+        source[p] = mergeRecursive(source[p], target[p]);
+      } else {
+        source[p] = target[p];
+      }
+    } catch (e) {
+      source[p] = target[p];
+    }
+  }
+  return source;
+};
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export function handleTree(data, id, parentId, children) {
+  let config = {
+    id: id || 'id',
+    parentId: parentId || 'parentId',
+    childrenList: children || 'children'
+  };
+
+  var childrenListMap = {};
+  var nodeIds = {};
+  var tree = [];
+
+  for (let d of data) {
+    let parentId = d[config.parentId];
+    if (childrenListMap[parentId] == null) {
+      childrenListMap[parentId] = [];
+    }
+    nodeIds[d[config.id]] = d;
+    childrenListMap[parentId].push(d);
+  }
+
+  for (let d of data) {
+    let parentId = d[config.parentId];
+    if (nodeIds[parentId] == null) {
+      tree.push(d);
+    }
+  }
+
+  for (let t of tree) {
+    adaptToChildrenList(t);
+  }
+
+  function adaptToChildrenList(o) {
+    if (childrenListMap[o[config.id]] !== null) {
+      o[config.childrenList] = childrenListMap[o[config.id]];
+    }
+    if (o[config.childrenList]) {
+      for (let c of o[config.childrenList]) {
+        adaptToChildrenList(c);
+      }
+    }
+  }
+  return tree;
+}
+
+/**
+* 参数处理
+* @param {*} params  参数
+*/
+export function tansParams(params) {
+  let result = ''
+  for (const propName of Object.keys(params)) {
+    const value = params[propName];
+    var part = encodeURIComponent(propName) + "=";
+    if (value !== null && value !== "" && typeof (value) !== "undefined") {
+      if (typeof value === 'object') {
+        for (const key of Object.keys(value)) {
+          if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
+            let params = propName + '[' + key + ']';
+            var subPart = encodeURIComponent(params) + "=";
+            result += subPart + encodeURIComponent(value[key]) + "&";
+          }
+        }
+      } else {
+        result += part + encodeURIComponent(value) + "&";
+      }
+    }
+  }
+  return result
+}
+
+// 验证是否为blob格式
+export async function blobValidate(data) {
+  try {
+    const text = await data.text();
+    JSON.parse(text);
+    return false;
+  } catch (error) {
+    return true;
+  }
+}

+ 488 - 0
ui/src/views/sysManager/sysMenu/index.vue

@@ -0,0 +1,488 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
+      <el-form-item label="菜单名称" prop="menuName">
+        <el-input
+          v-model="queryParams.menuName"
+          placeholder="请输入菜单名称"
+          clearable
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+<!--      <el-form-item label="状态" prop="status">-->
+<!--        <el-select v-model="queryParams.status" placeholder="菜单状态" clearable>-->
+<!--          <el-option-->
+<!--            v-for="dict in dict.type.sys_normal_disable"-->
+<!--            :key="dict.value"-->
+<!--            :label="dict.label"-->
+<!--            :value="dict.value"-->
+<!--          />-->
+<!--        </el-select>-->
+<!--      </el-form-item>-->
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          icon="el-icon-sort"
+          size="mini"
+          @click="toggleExpandAll"
+        >展开/折叠</el-button>
+      </el-col>
+    </el-row>
+
+    <el-table
+      v-if="refreshTable"
+      v-loading="loading"
+      :data="menuList"
+      row-key="menuId"
+      :default-expand-all="isExpandAll"
+      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+    >
+      <el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
+      <el-table-column prop="icon" label="图标" align="center" width="100">
+        <template slot-scope="scope">
+          <svg-icon :icon-class="scope.row.icon" />
+        </template>
+      </el-table-column>
+      <el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
+      <el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
+      <el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
+      <el-table-column prop="status" label="状态" width="80" :formatter="statusFormat"/>
+      <el-table-column label="创建时间" align="center" prop="createTime">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-plus"
+            @click="handleAdd(scope.row)"
+
+          >新增</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 添加或修改菜单对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="680px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="上级菜单" prop="parentId">
+              <treeselect
+                v-model="form.parentId"
+                :options="menuOptions"
+                :normalizer="normalizer"
+                :show-count="true"
+                placeholder="选择上级菜单"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="菜单类型" prop="menuType">
+              <el-radio-group v-model="form.menuType">
+                <el-radio label="M">目录</el-radio>
+                <el-radio label="C">菜单</el-radio>
+                <el-radio label="F">按钮</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24" v-if="form.menuType != 'F'">
+            <el-form-item label="菜单图标" prop="icon">
+              <el-popover
+                placement="bottom-start"
+                width="460"
+                trigger="click"
+                @show="$refs['iconSelect'].reset()"
+              >
+                <IconSelect ref="iconSelect" @selected="selected" />
+                <el-input slot="reference" v-model="form.icon" placeholder="点击选择图标" readonly>
+                  <svg-icon
+                    v-if="form.icon"
+                    slot="prefix"
+                    :icon-class="form.icon"
+                    class="el-input__icon"
+                    style="height: 32px;width: 16px;"
+                  />
+                  <i v-else slot="prefix" class="el-icon-search el-input__icon" />
+                </el-input>
+              </el-popover>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="菜单名称" prop="menuName">
+              <el-input v-model="form.menuName" placeholder="请输入菜单名称" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="显示排序" prop="orderNum">
+              <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType != 'F'">
+            <el-form-item prop="isFrame">
+              <span slot="label">
+                <el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                是否外链
+              </span>
+              <el-radio-group v-model="form.isFrame">
+                <el-radio label="0">是</el-radio>
+                <el-radio label="1">否</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType != 'F'">
+            <el-form-item prop="path">
+              <span slot="label">
+                <el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                路由地址
+              </span>
+              <el-input v-model="form.path" placeholder="请输入路由地址" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType == 'C'">
+            <el-form-item prop="component">
+              <span slot="label">
+                <el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                组件路径
+              </span>
+              <el-input v-model="form.component" placeholder="请输入组件路径" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType != 'M'">
+            <el-form-item prop="perms">
+              <el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
+              <span slot="label">
+                <el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                权限字符
+              </span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType == 'C'">
+            <el-form-item prop="query">
+              <el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
+              <span slot="label">
+                <el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                路由参数
+              </span>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType == 'C'">
+            <el-form-item prop="isCache">
+              <span slot="label">
+                <el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                是否缓存
+              </span>
+              <el-radio-group v-model="form.isCache">
+                <el-radio label="0">缓存</el-radio>
+                <el-radio label="1">不缓存</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType != 'F'">
+            <el-form-item prop="visible">
+              <span slot="label">
+                <el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                显示状态
+              </span>
+              <el-radio-group v-model="form.visible">
+                <el-radio
+                  v-for="dict in sysShow"
+                  :key="dict.value"
+                  :label="dict.value"
+                >{{dict.label}}</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.menuType != 'F'">
+            <el-form-item prop="status">
+              <span slot="label">
+                <el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
+                <i class="el-icon-question"></i>
+                </el-tooltip>
+                菜单状态
+              </span>
+              <el-radio-group v-model="form.status">
+                <el-radio
+                  v-for="dict in menuStatus"
+                  :key="dict.value"
+                  :label="dict.value"
+                >{{dict.label}}</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+// import { listMenu, getMenu, delMenu, addMenu, updateMenu } from "@/api/system/menu";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import IconSelect from "@/components/IconSelect";
+
+export default {
+  name: "Menu",
+  dicts: ['sys_show_hide', 'sys_normal_disable'],
+  components: { Treeselect, IconSelect },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 菜单表格树数据
+      menuList: [],
+      sysShow: [
+        {value: '0', label: '显示菜单'},
+        {value: '1', label: '隐藏菜单'}
+      ],
+      menuStatus: [
+        {value: '0', label: '正常'},
+        {value: '1', label: '停用'}
+      ],
+      // 菜单树选项
+      menuOptions: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 是否展开,默认全部折叠
+      isExpandAll: false,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 查询参数
+      queryParams: {
+        menuName: undefined,
+        visible: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        menuName: [
+          { required: true, message: "菜单名称不能为空", trigger: "blur" }
+        ],
+        orderNum: [
+          { required: true, message: "菜单顺序不能为空", trigger: "blur" }
+        ],
+        path: [
+          { required: true, message: "路由地址不能为空", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    // 列表状态格式化
+    statusFormat(row, column) {
+      let belongTo = '未知的类型'
+      for (let i = 0; i < this.menuStatus.length; i++) {
+        if (row.status == "0") {
+          belongTo = "正常"
+        } else if (row.status == "1") {
+          belongTo = "停用"
+        }
+      }
+      return belongTo
+    },
+    // 选择图标
+    selected(name) {
+      this.form.icon = name
+    },
+    /** 查询菜单列表 */
+    getList() {
+      this.loading = true;
+      this.$axios.get('/sysMenuController/list').then((res) => {
+        this.menuList = this.handleTree(res.data, "menuId")
+        this.loading = false
+      })
+    },
+    /** 转换菜单数据结构 */
+    normalizer(node) {
+      if (node.children && !node.children.length) {
+        delete node.children
+      }
+      return {
+        id: node.menuId,
+        label: node.menuName,
+        children: node.children
+      };
+    },
+    /** 查询菜单下拉树结构 */
+    getTreeselect() {
+      this.$axios.get('/sysMenuController/list').then((res) => {
+        this.menuOptions = []
+        const menu = { menuId: 0, menuName: '主类目', children: [] }
+        menu.children = this.handleTree(res.data, "menuId")
+        this.menuOptions.push(menu)
+      })
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        menuId: undefined,
+        parentId: 0,
+        menuName: undefined,
+        icon: undefined,
+        menuType: "M",
+        orderNum: undefined,
+        isFrame: "1",
+        isCache: "0",
+        visible: "0",
+        status: "0"
+      };
+      this.resetForm("form")
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm")
+      this.handleQuery()
+    },
+    /** 新增按钮操作 */
+    handleAdd(row) {
+      this.reset()
+      this.getTreeselect();
+      if (row != null && row.menuId) {
+        this.form.parentId = row.menuId
+      } else {
+        this.form.parentId = 0
+      }
+      this.open = true
+      this.title = "添加菜单"
+    },
+    /** 展开/折叠操作 */
+    toggleExpandAll() {
+      this.refreshTable = false
+      this.isExpandAll = !this.isExpandAll
+      this.$nextTick(() => {
+        this.refreshTable = true
+      });
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset()
+      this.getTreeselect()
+      var searchParams = {
+        menuId: row.menuId
+      }
+      this.$axios.get('/sysMenuController/getDetailInfo',
+        {params: searchParams}).then((res) => {
+        this.form = res.data
+        this.open = true
+        this.title = "修改菜单"
+      }).catch((error) => {
+        // this.$message.error('获取数据出错' + error)
+      })
+    },
+    /** 提交按钮 */
+    submitForm: function() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.menuId != undefined) {
+            this.$axios.put('/sysMenuController', this.form).then((res) => {
+              if (res.code == 0) {
+                this.$message.success('修改成功')
+              }
+              if (res.code == 1) {
+                this.$message.error(res.data)
+              }
+              this.open = false;
+              this.getList();
+            }).catch((error) => {
+              // this.$message.error(error)
+            })
+          } else {
+            this.$axios.post('/sysMenuController', this.form).then((res) => {
+              if (res.code == 0) {
+                this.$message.success('新增成功')
+              }
+              if (res.code == 1) {
+                this.$message.error(res.data)
+              }
+              this.open = false;
+              this.getList();
+            }).catch((error) => {
+              // this.$message.error(error)
+            })
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      this.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {
+        return delMenu(row.menuId)
+      }).then(() => {
+        this.getList()
+        this.$modal.msgSuccess("删除成功");
+      }).catch(() => {})
+    }
+  }
+}
+</script>

+ 1 - 1
ui/src/views/sysManager/userManager/index.vue

@@ -269,7 +269,7 @@ export default {
       this.pageSize = pageSize
       this.getList()
     },
-    // 时间格式化
+    // 列表状态格式化
     statusFormat({cellValue}) {
       let belongTo = '未知的类型'
       for (let i = 0; i < this.statusOptions.length; i++) {

+ 0 - 331
ui/src/views/sysmenu/index.vue

@@ -1,331 +0,0 @@
-<template>
-  <div class="app-container">
-    <el-card class="box-carde">
-      <div slot="header" class="clearfix">
-        <span>系统参数</span>
-        <el-button
-          style="float: right;padding:3px 10px 3px 3px;"
-          type="text"
-          :loading="btnLonding"
-          @click="exportDataEvent"
-        >导出数据
-        </el-button>
-      </div>
-      <el-button
-        type="primary"
-        size="small"
-        style="round-clip: 10px"
-        :loading="btnLonding"
-        @click="insertEvent"
-      >新增</el-button>
-      <el-input
-        v-model="keywords"
-        placeholder="通过描述搜索参数,记得回车哦..."
-        clearable
-        style="width: 300px;margin: 0px;padding: 0px;"
-        size="mini"
-        prefix-icon="el-icon-search"
-        @keyup.enter.native="searchEmp"
-      />
-      <el-button
-        type="primary"
-        size="small"
-        style="round-clip: 10px"
-        :loading="btnLonding"
-        @click="searchEmp"
-      >查询</el-button>
-      <div style="padding-top: 10px">
-        <vxe-table
-          ref="xTable"
-          highlight-current-row
-          v-show="showTable"
-          :keep-source="true"
-          align="center"
-          export-config
-          :loading="loading"
-          auto-resize
-          border
-          resizable
-          show-overflow
-          :edit-rules="rules"
-          :data="tableData"
-          :edit-config="{trigger: 'manual', mode: 'row',autoClear: false,icon:'none',activeMethod: activeCellMethod}"
-        >
-          <vxe-table-column title="参数配置">
-            <vxe-table-column
-              field="sysKey"
-              title="参数"
-              width="25%"
-              :edit-render="{name: '$input', props: {type: 'text', readonly: !add}}"
-            />
-            <vxe-table-column
-              field="sysValue"
-              title="参数值"
-              :edit-render="{name: '$input', attrs: {type: 'text'}}"
-            />
-            <vxe-table-column
-              field="describe"
-              title="参数描述"
-              width="30%"
-              :edit-render="{name: '$input', attrs: {type: 'text'}}"
-            />
-            <vxe-table-column field="creator" title="创建人" width="8%" />
-            <vxe-table-column :formatter="dateFormat" field="createTime" width="20%" title="创建时间" />
-            <vxe-table-column width="10%" align="center" title="操作">
-              <template v-slot="{ row }">
-                <template v-if="$refs.xTable.isActiveByRow(row)">
-                  <el-button
-                    style="padding: 3px 4px 3px 4px;margin: 2px"
-                    size="mini"
-                    icon="el-icon-edit"
-                    :loading="saveLoding"
-                    @click="saveRowEvent(row)"
-                  >保存
-                  </el-button>
-                  <el-button
-                    style="padding: 3px 4px 3px 4px;margin: 2px"
-                    size="mini"
-                    icon="el-icon-edit"
-                    @click="cancelRowEvent(row)"
-                  >取消
-                  </el-button>
-                </template>
-                <template v-else>
-                  <el-button
-                    style="padding: 3px 4px 3px 4px;margin: 2px"
-                    size="mini"
-                    icon="el-icon-edit"
-                    :loading="btnLonding"
-                    @click="editRowEvent(row)"
-                  >编辑
-                  </el-button>
-                  <!--<el-button-->
-                  <!--type="danger"-->
-                  <!--style="padding: 3px 4px 3px 4px;margin: 2px"-->
-                  <!--size="mini"-->
-                  <!--icon="el-icon-delete"-->
-                  <!--@click="removeEvent(row)"-->
-                  <!--:loading=btnLonding>删除-->
-                  <!--</el-button>-->
-                </template>
-              </template>
-            </vxe-table-column>
-          </vxe-table-column>
-        </vxe-table>
-        <vxe-pager
-          v-show="showTable"
-          perfect
-          :current-page.sync="currentPage"
-          :page-size.sync="pageSize"
-          :total="total"
-          :page-sizes="[10,50,100]"
-          :layouts="['PrevJump', 'PrevPage','JumpNumber', 'NextPage', 'NextJump', 'Sizes', 'FullJump', 'Total']"
-          @page-change="handlePageChange"
-        ><!--v-show="!btnLonding"-->
-        </vxe-pager>
-      </div>
-
-    </el-card>
-  </div>
-</template>
-<script>
-import request from '@/utils/request'
-
-export default {
-  data() {
-    return {
-      keywords: '',
-      add: false,
-      addType: false,
-      loading: false,
-      saveLoding: false,
-      showTable: true,
-      btnLonding: false,
-      tableData: [],
-      currentPage: 1,
-      pageSize: 10,
-      total: 0,
-      rules: {
-        sysKey: [
-          { required: true, message: '参数标识不能为空' }
-        ],
-        sysValue: [
-          { required: true, message: '参数不能为空' }
-        ],
-        describe: [
-          { required: true, message: '参数描述不能为空' }
-        ]
-      }
-    }
-  },
-  created() {
-    this.getAll()
-  },
-  methods: {
-    activeCellMethod({ column, columnIndex }) {
-      if (columnIndex === 1) {
-        return false
-      }
-      return true
-    },
-    searchEmp() {
-      this.loading = true
-      this.saveLoding = false
-      this.$axios.get('/sysParameter/' + this.currentPage + '/' + this.pageSize + '/' + this.keywords).then((res) => {
-        this.tableData = res.data.content
-        this.total = res.data.count
-        console.log('获取系统参数信息成功')
-        if (res.data.content == '') {
-          this.showTable = false
-        } else {
-          this.showTable = true
-        }
-        this.tableData = res.data.content
-        this.total = res.data.totalElements
-        this.loading = false
-      }).catch((error) => {
-        this.$message.error('获取系统参数信息出错' + error)
-      })
-    },
-    getAll() {
-      this.loading = true
-      this.saveLoding = false
-      this.$axios.get('/sysParameter/' + this.currentPage + '/' + this.pageSize,).then((res) => {
-        this.tableData = res.data.content
-        this.total = res.data.count
-        console.log('获取系统参数信息成功')
-        if (res.data.content == '') {
-          this.showTable = false
-        } else {
-          this.showTable = true
-        }
-        this.tableData = res.data.content
-        this.total = res.data.totalElements
-        this.loading = false
-      }).catch((error) => {
-        this.$message.error('获取系统参数信息出错' + error)
-      })
-    },
-    editRowEvent(row) {
-      this.btnLonding = true
-      this.$refs.xTable.setActiveRow(row)
-    },
-    saveRowEvent(row) {
-      this.saveLoding = true
-      this.$refs.xTable.validate(valid => {
-        if (valid) {
-          if (this.addType) {
-            this.$axios.post('/sysParameter/', row).then(response => {
-              this.getAll()
-              this.btnLonding = false
-              this.addType = false
-              this.$XModal.message({ status: 'warning', message: response.message })
-              this.add = false
-            }).catch((error) => {
-              this.loading = false
-              this.$emit('sendLoading', this.loading)
-              this.$message.error('添加出错' + error)
-              this.add = true
-            })
-          } else {
-            this.$axios.put('/sysParameter/', row).then(response => {
-              this.getAll()
-              this.$XModal.message({ status: 'warning', message: response.message })
-              this.saveLoding = false
-              this.btnLonding = false
-            })
-          }
-        } else {
-          this.$XModal.message({ status: 'error', message: '校验不通过!' })
-          this.saveLoding = false
-        }
-      })
-      this.saveLoding = false
-    },
-    cancelRowEvent(row) {
-      this.addType = false
-      this.saveLoding = false
-      this.btnLonding = false
-      const xTable = this.$refs.xTable
-      if (this.add) {
-        xTable.clearActived().then(() => {
-          xTable.remove(row)
-          this.add = false
-        })
-      } else {
-        xTable.clearActived().then(() => {
-          // 还原行数据
-          xTable.revertData(row)
-        })
-      }
-    },
-    // removeEvent(row) {
-    //   window.console.log(row)
-    //   this.$XModal.confirm('您确定要删除该数据?').then(type => {
-    //     if (type === 'confirm') {
-    //       this.$axios.delete('/sysParameter/',{data:row}).then(response => {
-    //         this.$XModal.message({status: 'warning', message: response.message})
-    //         this.$refs.xTable.remove(row)
-    //       })
-    //     }
-    //   })
-    // },
-    dateFormat(row, column) {
-      var date = row.cellValue
-
-      if (date == undefined || date == null) {
-        return ''
-      }
-      return this.$moment(date).format('YYYY-MM-DD HH:mm:ss')// 使用moment插件进行日期格式化
-    },
-    insertEvent() {
-      this.add = true
-      this.addType = true
-      this.showTable = true
-      this.btnLonding = true
-
-      this.$refs.xTable.insert()
-        .then(({ row }) => this.$refs.xTable.setActiveRow(row))
-    },
-    handlePageChange({ currentPage, pageSize }) {
-      this.currentPage = currentPage
-      this.pageSize = pageSize
-      this.getAll()
-    },
-    exportDataEvent() {
-      this.loading = true
-      this.$axios.get('/sysParameter/').then(res => {
-        const data = res.data.content
-        this.$refs.xTable.exportData({
-          filename: '系统参数信息',
-          type: 'csv',
-          isHeader: true,
-          isFooter: true,
-          data
-        })
-        this.loading = false
-      }).catch(e => {
-        this.loading = false
-      })
-    },
-    importDataEvent() {
-      this.$refs.xTable.importData({ types: ['csv'] })
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-  .app-container {
-    /*left: 0;*/
-    /*width: 100%;*/
-    /*min-height: 100%;*/
-    /*height: auto;*/
-    /*background-image: linear-gradient(25deg, #05362d, #145d44, #24875d, #35b477)*/
-  }
-
-  .pagination {
-    margin: 20px 0;
-    text-align: right;
-  }
-</style>

+ 1 - 0
ui/vue.config.js

@@ -27,6 +27,7 @@ module.exports = {
   publicPath: './',
   outputDir: 'dist',
   assetsDir: 'assets',
+  // lintOnSave: process.env.NODE_ENV === 'development',
   lintOnSave: process.env.NODE_ENV === 'development',
   productionSourceMap: false,
   pages: {