Explorar o código

Merge remote-tracking branch 'upstream/master'

“zyj” %!s(int64=4) %!d(string=hai) anos
pai
achega
fd72dec6a9
Modificáronse 36 ficheiros con 503 adicións e 188 borrados
  1. 2 2
      pom.xml
  2. 1 1
      ruoyi-admin/pom.xml
  3. 29 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java
  4. 2 5
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java
  5. 2 0
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
  6. 1 1
      ruoyi-admin/src/main/resources/application.yml
  7. 1 1
      ruoyi-common/pom.xml
  8. 3 0
      ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
  9. 12 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java
  10. 1 1
      ruoyi-framework/pom.xml
  11. 3 2
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
  12. 1 1
      ruoyi-generator/pom.xml
  13. 0 36
      ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm
  14. 0 38
      ruoyi-generator/src/main/resources/vm/vue/index.vue.vm
  15. 1 1
      ruoyi-quartz/pom.xml
  16. 1 1
      ruoyi-system/pom.xml
  17. 35 0
      ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java
  18. 37 2
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java
  19. 1 0
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java
  20. 1 1
      ruoyi-ui/package.json
  21. 7 6
      ruoyi-ui/src/components/Editor/index.vue
  22. 48 29
      ruoyi-ui/src/components/FileUpload/index.vue
  23. 3 1
      ruoyi-ui/src/components/HeaderSearch/index.vue
  24. 155 45
      ruoyi-ui/src/components/ImageUpload/index.vue
  25. 2 2
      ruoyi-ui/src/components/TopNav/index.vue
  26. 64 0
      ruoyi-ui/src/directive/dialog/drag.js
  27. 5 2
      ruoyi-ui/src/directive/index.js
  28. 1 1
      ruoyi-ui/src/directive/permission/hasPermi.js
  29. 1 1
      ruoyi-ui/src/directive/permission/hasRole.js
  30. 1 1
      ruoyi-ui/src/layout/components/AppMain.vue
  31. 27 0
      ruoyi-ui/src/layout/components/InnerLink/index.vue
  32. 11 2
      ruoyi-ui/src/main.js
  33. 1 0
      ruoyi-ui/src/router/index.js
  34. 3 0
      ruoyi-ui/src/store/modules/permission.js
  35. 40 1
      ruoyi-ui/src/views/index.vue
  36. 0 4
      ruoyi-ui/src/views/system/notice/index.vue

+ 2 - 2
pom.xml

@@ -6,14 +6,14 @@
 	
     <groupId>com.ruoyi</groupId>
     <artifactId>ruoyi</artifactId>
-    <version>3.5.0</version>
+    <version>3.6.0</version>
 
     <name>ruoyi</name>
     <url>http://www.ruoyi.vip</url>
     <description>若依管理系统</description>
     
     <properties>
-        <ruoyi.version>3.5.0</ruoyi.version>
+        <ruoyi.version>3.6.0</ruoyi.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>1.8</java.version>

+ 1 - 1
ruoyi-admin/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>3.5.0</version>
+        <version>3.6.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
     <packaging>jar</packaging>

+ 29 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysIndexController.java

@@ -0,0 +1,29 @@
+package com.ruoyi.web.controller.system;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 首页
+ *
+ * @author ruoyi
+ */
+@RestController
+public class SysIndexController
+{
+    /** 系统基础配置 */
+    @Autowired
+    private RuoYiConfig ruoyiConfig;
+
+    /**
+     * 访问首页,提示语
+     */
+    @RequestMapping("/")
+    public String index()
+    {
+        return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
+    }
+}

+ 2 - 5
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysMenuController.java

@@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import com.ruoyi.common.annotation.Log;
-import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.controller.BaseController;
 import com.ruoyi.common.core.domain.AjaxResult;
@@ -102,8 +101,7 @@ public class SysMenuController extends BaseController
         {
             return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
         }
-        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame())
-                && !StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS))
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
         {
             return AjaxResult.error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
         }
@@ -123,8 +121,7 @@ public class SysMenuController extends BaseController
         {
             return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
         }
-        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame())
-                && !StringUtils.startsWithAny(menu.getPath(), Constants.HTTP, Constants.HTTPS))
+        else if (UserConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath()))
         {
             return AjaxResult.error("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
         }

+ 2 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java

@@ -220,6 +220,7 @@ public class SysRoleController extends BaseController
     /**
      * 批量取消授权用户
      */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
     @Log(title = "角色管理", businessType = BusinessType.GRANT)
     @PutMapping("/authUser/cancelAll")
     public AjaxResult cancelAuthUserAll(Long roleId, Long[] userIds)
@@ -230,6 +231,7 @@ public class SysRoleController extends BaseController
     /**
      * 批量选择用户授权
      */
+    @PreAuthorize("@ss.hasPermi('system:role:edit')")
     @Log(title = "角色管理", businessType = BusinessType.GRANT)
     @PutMapping("/authUser/selectAll")
     public AjaxResult selectAuthUserAll(Long roleId, Long[] userIds)

+ 1 - 1
ruoyi-admin/src/main/resources/application.yml

@@ -3,7 +3,7 @@ ruoyi:
   # 名称
   name: RuoYi
   # 版本
-  version: 3.5.0
+  version: 3.6.0
   # 版权年份
   copyrightYear: 2021
   # 实例演示开关

+ 1 - 1
ruoyi-common/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>3.5.0</version>
+        <version>3.6.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 3 - 0
ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java

@@ -57,6 +57,9 @@ public class UserConstants
     /** 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";

+ 12 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/StringUtils.java

@@ -6,6 +6,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.core.text.StrFormatter;
 
 /**
@@ -260,6 +261,17 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils
         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
      * 

+ 1 - 1
ruoyi-framework/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>3.5.0</version>
+        <version>3.6.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 3 - 2
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -100,12 +100,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/login", "/captchaImage").anonymous()
                 .antMatchers(
                         HttpMethod.GET,
+                        "/",
                         "/*.html",
                         "/**/*.html",
                         "/**/*.css",
-                        "/**/*.js"
+                        "/**/*.js",
+                        "/profile/**"
                 ).permitAll()
-                .antMatchers("/profile/**").anonymous()
                 .antMatchers("/common/download**").anonymous()
                 .antMatchers("/common/download/resource**").anonymous()
                 .antMatchers("/swagger-ui.html").anonymous()

+ 1 - 1
ruoyi-generator/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>3.5.0</version>
+        <version>3.6.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 0 - 36
ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm

@@ -258,46 +258,10 @@
 import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName}, export${BusinessName} } from "@/api/${moduleName}/${businessName}";
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "imageUpload")
-import ImageUpload from '@/components/ImageUpload';
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "fileUpload")
-import FileUpload from '@/components/FileUpload';
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "editor")
-import Editor from '@/components/Editor';
-#break
-#end
-#end
 
 export default {
   name: "${BusinessName}",
   components: {
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "imageUpload")
-    ImageUpload,
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "fileUpload")
-    FileUpload,
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "editor")
-    Editor,
-#break
-#end
-#end
     Treeselect
   },
   data() {

+ 0 - 38
ruoyi-generator/src/main/resources/vm/vue/index.vue.vm

@@ -310,47 +310,9 @@
 
 <script>
 import { list${BusinessName}, get${BusinessName}, del${BusinessName}, add${BusinessName}, update${BusinessName}, export${BusinessName} } from "@/api/${moduleName}/${businessName}";
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "imageUpload")
-import ImageUpload from '@/components/ImageUpload';
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "fileUpload")
-import FileUpload from '@/components/FileUpload';
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "editor")
-import Editor from '@/components/Editor';
-#break
-#end
-#end
 
 export default {
   name: "${BusinessName}",
-  components: {
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "imageUpload")
-    ImageUpload,
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "fileUpload")
-    FileUpload,
-#break
-#end
-#end
-#foreach($column in $columns)
-#if($column.insert && !$column.superColumn && !$column.pk && $column.htmlType == "editor")
-    Editor,
-#break
-#end
-#end
-  },
   data() {
     return {
       // 遮罩层

+ 1 - 1
ruoyi-quartz/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>3.5.0</version>
+        <version>3.6.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
ruoyi-system/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>3.5.0</version>
+        <version>3.6.0</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 35 - 0
ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/MetaVo.java

@@ -1,5 +1,7 @@
 package com.ruoyi.system.domain.vo;
 
+import com.ruoyi.common.utils.StringUtils;
+
 /**
  * 路由显示信息
  * 
@@ -22,6 +24,11 @@ public class MetaVo
      */
     private boolean noCache;
 
+    /**
+     * 内链地址(http(s)://开头)
+     */
+    private String link;
+
     public MetaVo()
     {
     }
@@ -39,6 +46,24 @@ public class MetaVo
         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;
@@ -68,4 +93,14 @@ public class MetaVo
     {
         this.icon = icon;
     }
+
+    public String getLink()
+    {
+        return link;
+    }
+
+    public void setLink(String link)
+    {
+        this.link = link;
+    }
 }

+ 37 - 2
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysMenuServiceImpl.java

@@ -10,6 +10,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import com.ruoyi.common.constant.Constants;
 import com.ruoyi.common.constant.UserConstants;
 import com.ruoyi.common.core.domain.TreeSelect;
 import com.ruoyi.common.core.domain.entity.SysMenu;
@@ -150,7 +151,7 @@ public class SysMenuServiceImpl implements ISysMenuService
             router.setName(getRouteName(menu));
             router.setPath(getRouterPath(menu));
             router.setComponent(getComponent(menu));
-            router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache())));
+            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()))
             {
@@ -166,7 +167,21 @@ public class SysMenuServiceImpl implements ISysMenuService
                 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())));
+                children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
+                childrenList.add(children);
+                router.setChildren(childrenList);
+            }
+            else if (menu.getParentId().intValue() == 0 && isInnerLink(menu))
+            {
+                router.setMeta(null);
+                router.setPath("/inner");
+                List<RouterVo> childrenList = new ArrayList<RouterVo>();
+                RouterVo children = new RouterVo();
+                String routerPath = StringUtils.replaceEach(menu.getPath(), new String[] { Constants.HTTP, Constants.HTTPS }, new String[] { "", "" });
+                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);
             }
@@ -338,6 +353,11 @@ public class SysMenuServiceImpl implements ISysMenuService
     public String getRouterPath(SysMenu menu)
     {
         String routerPath = menu.getPath();
+        // 内链打开外网方式
+        if (menu.getParentId().intValue() != 0 && isInnerLink(menu))
+        {
+            routerPath = StringUtils.replaceEach(routerPath, new String[] { Constants.HTTP, Constants.HTTPS }, new String[] { "", "" });
+        }
         // 非外链并且是一级目录(类型为目录)
         if (0 == menu.getParentId().intValue() && UserConstants.TYPE_DIR.equals(menu.getMenuType())
                 && UserConstants.NO_FRAME.equals(menu.getIsFrame()))
@@ -365,6 +385,10 @@ public class SysMenuServiceImpl implements ISysMenuService
         {
             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;
@@ -384,6 +408,17 @@ public class SysMenuServiceImpl implements ISysMenuService
                 && menu.getIsFrame().equals(UserConstants.NO_FRAME);
     }
 
+    /**
+     * 是否为内链组件
+     * 
+     * @param menu 菜单信息
+     * @return 结果
+     */
+    public boolean isInnerLink(SysMenu menu)
+    {
+        return menu.getIsFrame().equals(UserConstants.NO_FRAME) && StringUtils.ishttp(menu.getPath());
+    }
+
     /**
      * 是否为parent_view组件
      * 

+ 1 - 0
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java

@@ -275,6 +275,7 @@ public class SysUserServiceImpl implements ISysUserService
      * @param roleIds 角色组
      */
     @Override
+    @Transactional
     public void insertUserAuth(Long userId, Long[] roleIds)
     {
         userRoleMapper.deleteUserRoleByUserId(userId);

+ 1 - 1
ruoyi-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "ruoyi",
-  "version": "3.5.0",
+  "version": "3.6.0",
   "description": "若依管理系统",
   "author": "若依",
   "license": "MIT",

+ 7 - 6
ruoyi-ui/src/components/Editor/index.vue

@@ -9,7 +9,7 @@
       :headers="headers"
       style="display: none"
       ref="upload"
-      v-if="this.uploadUrl"
+      v-if="this.type == 'url'"
     >
     </el-upload>
     <div class="editor" ref="editor" :style="styles"></div>
@@ -46,14 +46,15 @@ export default {
       type: Boolean,
       default: false,
     },
-    /* 上传地址 */
-    uploadUrl: {
+    /* 类型(base64格式、url格式) */
+    type: {
       type: String,
-      default: "",
+      default: "url",
     }
   },
   data() {
     return {
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken()
       },
@@ -119,7 +120,7 @@ export default {
       const editor = this.$refs.editor;
       this.Quill = new Quill(editor, this.options);
       // 如果设置了上传地址则自定义图片上传事件
-      if (this.uploadUrl) {
+      if (this.type == 'url') {
         let toolbar = this.Quill.getModule("toolbar");
         toolbar.addHandler("image", (value) => {
           this.uploadType = "image";
@@ -165,7 +166,7 @@ export default {
         // 获取光标所在位置
         let length = quill.getSelection().index;
         // 插入图片  res.url为服务器返回的图片地址
-        quill.insertEmbed(length, "image", res.url);
+        quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName);
         // 调整光标到最后
         quill.setSelection(length + 1);
       } else {

+ 48 - 29
ruoyi-ui/src/components/FileUpload/index.vue

@@ -4,7 +4,7 @@
       :action="uploadFileUrl"
       :before-upload="handleBeforeUpload"
       :file-list="fileList"
-      :limit="1"
+      :limit="limit"
       :on-error="handleUploadError"
       :on-exceed="handleExceed"
       :on-success="handleUploadSuccess"
@@ -26,8 +26,8 @@
 
     <!-- 文件列表 -->
     <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
-      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in list">
-        <el-link :href="file.url" :underline="false" target="_blank">
+      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
+        <el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
           <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
         </el-link>
         <div class="ele-upload-list__item-content-action">
@@ -42,9 +42,15 @@
 import { getToken } from "@/utils/auth";
 
 export default {
+  name: "FileUpload",
   props: {
     // 值
     value: [String, Object, Array],
+    // 数量限制
+    limit: {
+      type: Number,
+      default: 5,
+    },
     // 大小限制(MB)
     fileSize: {
       type: Number,
@@ -63,6 +69,7 @@ export default {
   },
   data() {
     return {
+      baseUrl: process.env.VUE_APP_BASE_API,
       uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken(),
@@ -70,30 +77,35 @@ export default {
       fileList: [],
     };
   },
+  watch: {
+    value: {
+      handler(val) {
+        if (val) {
+          let temp = 1;
+          // 首先将值转为数组
+          const list = Array.isArray(val) ? val : this.value.split(',');
+          // 然后将数组转为对象数组
+          this.fileList = list.map(item => {
+            if (typeof item === "string") {
+              item = { name: item, url: item };
+            }
+            item.uid = item.uid || new Date().getTime() + temp++;
+            return item;
+          });
+        } else {
+          this.fileList = [];
+          return [];
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
   computed: {
     // 是否显示提示
     showTip() {
       return this.isShowTip && (this.fileType || this.fileSize);
     },
-    // 列表
-    list() {
-      let temp = 1;
-      if (this.value) {
-        // 首先将值转为数组
-        const list = Array.isArray(this.value) ? this.value : [this.value];
-        // 然后将数组转为对象数组
-        return list.map((item) => {
-          if (typeof item === "string") {
-            item = { name: item, url: item };
-          }
-          item.uid = item.uid || new Date().getTime() + temp++;
-          return item;
-        });
-      } else {
-        this.fileList = [];
-        return [];
-      }
-    },
   },
   methods: {
     // 上传前校检格式和大小
@@ -126,7 +138,7 @@ export default {
     },
     // 文件个数超出
     handleExceed() {
-      this.$message.error(`只允许上传单个文件`);
+      this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
     },
     // 上传失败
     handleUploadError(err) {
@@ -135,12 +147,13 @@ export default {
     // 上传成功回调
     handleUploadSuccess(res, file) {
       this.$message.success("上传成功");
-      this.$emit("input", res.url);
+      this.fileList.push({ name: res.fileName, url: res.fileName });
+      this.$emit("input", this.listToString(this.fileList));
     },
     // 删除文件
     handleDelete(index) {
       this.fileList.splice(index, 1);
-      this.$emit("input", '');
+      this.$emit("input", this.listToString(this.fileList));
     },
     // 获取文件名称
     getFileName(name) {
@@ -149,11 +162,17 @@ export default {
       } else {
         return "";
       }
+    },
+    // 对象转成指定字符串分隔
+    listToString(list, separator) {
+      let strs = "";
+      separator = separator || ",";
+      for (let i in list) {
+        strs += list[i].url + separator;
+      }
+      return strs != '' ? strs.substr(0, strs.length - 1) : '';
     }
-  },
-  created() {
-    this.fileList = this.list;
-  },
+  }
 };
 </script>
 

+ 3 - 1
ruoyi-ui/src/components/HeaderSearch/index.vue

@@ -70,9 +70,11 @@ export default {
       this.show = false
     },
     change(val) {
+      const path = val.path;
       if(this.ishttp(val.path)) {
         // http(s):// 路径新窗口打开
-        window.open(val.path, "_blank");
+        const pindex = path.indexOf("http");
+        window.open(path.substr(pindex, path.length), "_blank");
       } else {
         this.$router.push(val.path)
       }

+ 155 - 45
ruoyi-ui/src/components/ImageUpload/index.vue

@@ -5,33 +5,38 @@
       list-type="picture-card"
       :on-success="handleUploadSuccess"
       :before-upload="handleBeforeUpload"
+      :limit="limit"
       :on-error="handleUploadError"
+      :on-exceed="handleExceed"
       name="file"
-      :show-file-list="false"
+      :on-remove="handleRemove"
+      :show-file-list="true"
       :headers="headers"
-      style="display: inline-block; vertical-align: top"
+      :file-list="fileList"
+      :on-preview="handlePictureCardPreview"
+      :class="{hide: this.fileList.length >= this.limit}"
     >
-      <el-image v-if="!value" :src="value">
-        <div slot="error" class="image-slot">
-          <i class="el-icon-plus" />
-        </div>
-      </el-image>
-      <div v-else class="image">
-        <el-image :src="value" :style="`width:150px;height:150px;`" fit="fill"/>
-        <div class="mask">
-          <div class="actions">
-            <span title="预览" @click.stop="dialogVisible = true">
-              <i class="el-icon-zoom-in" />
-            </span>
-            <span title="移除" @click.stop="removeImage">
-              <i class="el-icon-delete" />
-            </span>
-          </div>
-        </div>
-      </div>
+      <i class="el-icon-plus"></i>
     </el-upload>
-    <el-dialog :visible.sync="dialogVisible" title="预览" width="800" append-to-body>
-      <img :src="value" style="display: block; max-width: 100%; margin: 0 auto;">
+    
+    <!-- 上传提示 -->
+    <div class="el-upload__tip" slot="tip" v-if="showTip">
+      请上传
+      <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
+      <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
+      的文件
+    </div>
+
+    <el-dialog
+      :visible.sync="dialogVisible"
+      title="预览"
+      width="800"
+      append-to-body
+    >
+      <img
+        :src="dialogImageUrl"
+        style="display: block; max-width: 100%; margin: 0 auto"
+      />
     </el-dialog>
   </div>
 </template>
@@ -40,36 +45,128 @@
 import { getToken } from "@/utils/auth";
 
 export default {
+  props: {
+    value: [String, Object, Array],
+    // 图片数量限制
+    limit: {
+      type: Number,
+      default: 5,
+    },
+    // 大小限制(MB)
+    fileSize: {
+       type: Number,
+      default: 5,
+    },
+    // 文件类型, 例如['png', 'jpg', 'jpeg']
+    fileType: {
+      type: Array,
+      default: () => ["png", "jpg", "jpeg"],
+    },
+    // 是否显示提示
+    isShowTip: {
+      type: Boolean,
+      default: true
+    }
+  },
   data() {
     return {
+      dialogImageUrl: "",
       dialogVisible: false,
+      hideUpload: false,
+      baseUrl: process.env.VUE_APP_BASE_API,
       uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken(),
       },
+      fileList: []
     };
   },
-  props: {
+  watch: {
     value: {
-      type: String,
-      default: "",
+      handler(val) {
+        if (val) {
+          // 首先将值转为数组
+          const list = Array.isArray(val) ? val : this.value.split(',');
+          // 然后将数组转为对象数组
+          this.fileList = list.map(item => {
+            if (typeof item === "string") {
+              if (item.indexOf(this.baseUrl) === -1) {
+                  item = { name: this.baseUrl + item, url: this.baseUrl + item };
+              } else {
+                  item = { name: item, url: item };
+              }
+            }
+            return item;
+          });
+        } else {
+          this.fileList = [];
+          return [];
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  computed: {
+    // 是否显示提示
+    showTip() {
+      return this.isShowTip && (this.fileType || this.fileSize);
     },
   },
   methods: {
-    removeImage() {
-      this.$emit("input", "");
+    // 删除图片
+    handleRemove(file, fileList) {
+      const findex = this.fileList.indexOf(file.name);
+      this.fileList.splice(findex, 1);
+      this.$emit("input", this.listToString(this.fileList));
     },
+    // 上传成功回调
     handleUploadSuccess(res) {
-      this.$emit("input", res.url);
+      this.fileList.push({ name: res.fileName, url: res.fileName });
+      this.$emit("input", this.listToString(this.fileList));
       this.loading.close();
     },
-    handleBeforeUpload() {
+    // 上传前loading加载
+    handleBeforeUpload(file) {
+      let isImg = false;
+      if (this.fileType.length) {
+        let fileExtension = "";
+        if (file.name.lastIndexOf(".") > -1) {
+          fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
+        }
+        isImg = this.fileType.some(type => {
+          if (file.type.indexOf(type) > -1) return true;
+          if (fileExtension && fileExtension.indexOf(type) > -1) return true;
+          return false;
+        });
+      } else {
+        isImg = file.type.indexOf("image") > -1;
+      }
+
+      if (!isImg) {
+        this.$message.error(
+          `文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`
+        );
+        return false;
+      }
+      if (this.fileSize) {
+        const isLt = file.size / 1024 / 1024 < this.fileSize;
+        if (!isLt) {
+          this.$message.error(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
+          return false;
+        }
+      }
       this.loading = this.$loading({
         lock: true,
         text: "上传中",
         background: "rgba(0, 0, 0, 0.7)",
       });
     },
+    // 文件个数超出
+    handleExceed() {
+      this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
+    },
+    // 上传失败
     handleUploadError() {
       this.$message({
         type: "error",
@@ -77,24 +174,37 @@ export default {
       });
       this.loading.close();
     },
-  },
-  watch: {},
+    // 预览
+    handlePictureCardPreview(file) {
+      this.dialogImageUrl = file.url;
+      this.dialogVisible = true;
+    },
+    // 对象转成指定字符串分隔
+    listToString(list, separator) {
+      let strs = "";
+      separator = separator || ",";
+      for (let i in list) {
+        strs += list[i].url + separator;
+      }
+      return strs != '' ? strs.substr(0, strs.length - 1) : '';
+    }
+  }
 };
 </script>
-
 <style scoped lang="scss">
-.image {
-  position: relative;
-  .mask {
+// .el-upload--picture-card 控制加号部分
+::v-deep.hide .el-upload--picture-card {
+    display: none;
+}
+// 去掉动画效果
+::v-deep .el-list-enter-active,
+::v-deep .el-list-leave-active {
+    transition: all 0s;
+}
+
+::v-deep .el-list-enter, .el-list-leave-active {
     opacity: 0;
-    position: absolute;
-    top: 0;
-    width: 100%;
-    background-color: rgba(0, 0, 0, 0.5);
-    transition: all 0.3s;
-  }
-  &:hover .mask {
-    opacity: 1;
-  }
+    transform: translateY(0);
 }
-</style>
+</style>
+

+ 2 - 2
ruoyi-ui/src/components/TopNav/index.vue

@@ -73,9 +73,9 @@ export default {
             if(router.path === "/") {
               router.children[item].path = "/redirect/" + router.children[item].path;
             } else {
-			  if(!this.ishttp(router.children[item].path)) {
+              if(!this.ishttp(router.children[item].path)) {
                 router.children[item].path = router.path + "/" + router.children[item].path;
-			  }
+              }
             }
             router.children[item].parentPath = router.path;
           }

+ 64 - 0
ruoyi-ui/src/directive/dialog/drag.js

@@ -0,0 +1,64 @@
+/**
+* v-dialogDrag 弹窗拖拽
+* Copyright (c) 2019 ruoyi
+*/
+
+export default {
+  bind(el, binding, vnode, oldVnode) {
+    const value = binding.value
+    if (value == false) return
+    // 获取拖拽内容头部
+    const dialogHeaderEl = el.querySelector('.el-dialog__header');
+    const dragDom = el.querySelector('.el-dialog');
+    dialogHeaderEl.style.cursor = 'move';
+    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
+    const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null);
+    dragDom.style.position = 'absolute';
+    dragDom.style.marginTop = 0;
+    let width = dragDom.style.width;
+    if (width.includes('%')) {
+      width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100);
+    } else {
+      width = +width.replace(/\px/g, '');
+    }
+    dragDom.style.left = `${(document.body.clientWidth - width) / 2}px`;
+    // 鼠标按下事件
+    dialogHeaderEl.onmousedown = (e) => {
+      // 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
+      const disX = e.clientX - dialogHeaderEl.offsetLeft;
+      const disY = e.clientY - dialogHeaderEl.offsetTop;
+
+      // 获取到的值带px 正则匹配替换
+      let styL, styT;
+
+      // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
+      if (sty.left.includes('%')) {
+        styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
+        styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
+      } else {
+        styL = +sty.left.replace(/\px/g, '');
+        styT = +sty.top.replace(/\px/g, '');
+      };
+
+      // 鼠标拖拽事件
+      document.onmousemove = function (e) {
+        // 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
+        const l = e.clientX - disX;
+        const t = e.clientY - disY;
+
+        let finallyL = l + styL
+        let finallyT = t + styT
+
+        // 移动当前元素
+        dragDom.style.left = `${finallyL}px`;
+        dragDom.style.top = `${finallyT}px`;
+
+      };
+
+      document.onmouseup = function (e) {
+        document.onmousemove = null;
+        document.onmouseup = null;
+      };
+    }
+  }
+};

+ 5 - 2
ruoyi-ui/src/directive/permission/index.js → ruoyi-ui/src/directive/index.js

@@ -1,14 +1,17 @@
-import hasRole from './hasRole'
-import hasPermi from './hasPermi'
+import hasRole from './permission/hasRole'
+import hasPermi from './permission/hasPermi'
+import dialogDrag from './dialog/drag'
 
 const install = function(Vue) {
   Vue.directive('hasRole', hasRole)
   Vue.directive('hasPermi', hasPermi)
+  Vue.directive('dialogDrag', dialogDrag)
 }
 
 if (window.Vue) {
   window['hasRole'] = hasRole
   window['hasPermi'] = hasPermi
+  window['dialogDrag'] = dialogDrag
   Vue.use(install); // eslint-disable-line
 }
 

+ 1 - 1
ruoyi-ui/src/directive/permission/hasPermi.js

@@ -1,5 +1,5 @@
  /**
- * 操作权限处理
+ * v-hasPermi 操作权限处理
  * Copyright (c) 2019 ruoyi
  */
  

+ 1 - 1
ruoyi-ui/src/directive/permission/hasRole.js

@@ -1,5 +1,5 @@
  /**
- * 角色权限处理
+ * v-hasRole 角色权限处理
  * Copyright (c) 2019 ruoyi
  */
  

+ 1 - 1
ruoyi-ui/src/layout/components/AppMain.vue

@@ -51,7 +51,7 @@ export default {
 // fix css style bug in open el-dialog
 .el-popup-parent--hidden {
   .fixed-header {
-    padding-right: 15px;
+    padding-right: 17px;
   }
 }
 </style>

+ 27 - 0
ruoyi-ui/src/layout/components/InnerLink/index.vue

@@ -0,0 +1,27 @@
+<script>
+export default {
+  data() {
+    return {};
+  },
+  render() {
+    const { $route: { meta: { link } }, } = this;
+    if ({ link }.link === "") {
+      return "404";
+    }
+    let url = { link }.link;
+    const height = document.documentElement.clientHeight - 94.5 + "px";
+    const style = { height: height };
+
+    return (
+      <div style={style}>
+        <iframe
+          src={url}
+          frameborder="no"
+          style="width: 100%; height: 100%"
+          scrolling="auto"
+        ></iframe>
+      </div>
+    );
+  },
+};
+</script>

+ 11 - 2
ruoyi-ui/src/main.js

@@ -10,7 +10,7 @@ import '@/assets/styles/ruoyi.scss' // ruoyi css
 import App from './App'
 import store from './store'
 import router from './router'
-import permission from './directive/permission'
+import directive from './directive' //directive
 
 import './assets/icons' // icon
 import './permission' // permission control
@@ -20,6 +20,12 @@ import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels,
 import Pagination from "@/components/Pagination";
 // 自定义表格工具组件
 import RightToolbar from "@/components/RightToolbar"
+// 富文本组件
+import Editor from "@/components/Editor"
+// 文件上传组件
+import FileUpload from "@/components/FileUpload"
+// 图片上传组件
+import ImageUpload from "@/components/ImageUpload"
 // 字典标签组件
 import DictTag from '@/components/DictTag'
 // 头部标签组件
@@ -52,8 +58,11 @@ Vue.prototype.msgInfo = function (msg) {
 Vue.component('DictTag', DictTag)
 Vue.component('Pagination', Pagination)
 Vue.component('RightToolbar', RightToolbar)
+Vue.component('Editor', Editor)
+Vue.component('FileUpload', FileUpload)
+Vue.component('ImageUpload', ImageUpload)
 
-Vue.use(permission)
+Vue.use(directive)
 Vue.use(VueMeta)
 
 /**

+ 1 - 0
ruoyi-ui/src/router/index.js

@@ -6,6 +6,7 @@ Vue.use(Router)
 /* Layout */
 import Layout from '@/layout'
 import ParentView from '@/components/ParentView';
+import InnerLink from '@/layout/components/InnerLink'
 
 /**
  * Note: 路由配置项

+ 3 - 0
ruoyi-ui/src/store/modules/permission.js

@@ -2,6 +2,7 @@ import { constantRoutes } 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: {
@@ -65,6 +66,8 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
         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)
       }

+ 40 - 1
ruoyi-ui/src/views/index.vue

@@ -147,6 +147,45 @@
             <span>更新日志</span>
           </div>
           <el-collapse accordion>
+            <el-collapse-item title="v3.6.0 - 2021-07-12">
+              <ol>
+                <li>角色管理新增分配用户功能</li>
+                <li>用户管理新增分配角色功能</li>
+                <li>日志列表支持排序操作</li>
+                <li>优化参数&字典缓存操作</li>
+                <li>系统布局配置支持动态标题开关</li>
+                <li>菜单路由配置支持内链访问</li>
+                <li>默认访问后端首页新增提示语</li>
+                <li>富文本默认上传返回url类型</li>
+                <li>新增自定义弹窗拖拽指令</li>
+                <li>全局注册常用通用组件</li>
+                <li>全局挂载字典标签组件</li>
+                <li>ImageUpload组件支持多图片上传</li>
+                <li>FileUpload组件支持多文件上传</li>
+                <li>文件上传组件添加数量限制属性</li>
+                <li>富文本编辑组件添加类型属性</li>
+                <li>富文本组件工具栏配置视频</li>
+                <li>封装通用iframe组件</li>
+                <li>限制超级管理员不允许操作</li>
+                <li>用户信息长度校验限制</li>
+                <li>分页组件新增pagerCount属性</li>
+                <li>添加bat脚本执行应用</li>
+                <li>升级oshi到最新版本v5.7.4</li>
+                <li>升级element-ui到最新版本2.15.2</li>
+                <li>升级pagehelper到最新版1.3.1</li>
+                <li>升级commons.io到最新版本v2.10.0</li>
+                <li>升级commons.fileupload到最新版本v1.4</li>
+                <li>升级swagger到最新版本v3.0.0</li>
+                <li>修复关闭confirm提示框控制台报错问题</li>
+                <li>修复存在的SQL注入漏洞问题</li>
+                <li>定时任务屏蔽rmi远程调用</li>
+                <li>修复用户搜索分页变量错误</li>
+                <li>修复导出角色数据范围翻译缺少仅本人</li>
+                <li>修复表单构建选择下拉选择控制台报错问题</li>
+                <li>优化图片工具类读取文件</li>
+                <li>其他细节优化</li>
+              </ol>
+            </el-collapse-item>
             <el-collapse-item title="v3.5.0 - 2021-05-25">
               <ol>
                 <li>新增菜单导航显示风格TopNav(false为左侧导航菜单,true为顶部导航菜单)</li>
@@ -566,7 +605,7 @@ export default {
   data() {
     return {
       // 版本号
-      version: "3.5.0",
+      version: "3.6.0",
     };
   },
   methods: {

+ 0 - 4
ruoyi-ui/src/views/system/notice/index.vue

@@ -177,13 +177,9 @@
 
 <script>
 import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice";
-import Editor from '@/components/Editor';
 
 export default {
   name: "Notice",
-  components: {
-    Editor
-  },
   data() {
     return {
       // 遮罩层