Pārlūkot izejas kodu

Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	README.md
“zyj” 4 gadi atpakaļ
vecāks
revīzija
3525f98b77

+ 1 - 0
.github/FUNDING.yml

@@ -0,0 +1 @@
+custom: http://doc.ruoyi.vip/ruoyi-vue/other/donate.html

+ 12 - 0
README.md

@@ -1,4 +1,5 @@
 ## 修改说明
+如此项目报错请在 issue 中提问
 
 ### 此项目 官网同步更新
 ### 基于 Postgresql 实现
@@ -9,6 +10,17 @@
 2.   操作一致
 
 
+<p align="center">
+	<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
+</p>
+<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v3.8.1</h1>
+<h4 align="center">基于SpringBoot+Vue前后端分离的Java快速开发框架</h4>
+<p align="center">
+	<a href="https://gitee.com/y_project/RuoYi-Vue/stargazers"><img src="https://gitee.com/y_project/RuoYi-Vue/badge/star.svg?theme=dark"></a>
+	<a href="https://gitee.com/y_project/RuoYi-Vue"><img src="https://img.shields.io/badge/RuoYi-v3.8.1-brightgreen.svg"></a>
+	<a href="https://gitee.com/y_project/RuoYi-Vue/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
+</p>
+
 ## 平台简介
 若依是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
 

+ 16 - 2
pom.xml

@@ -6,14 +6,14 @@
 	
     <groupId>com.ruoyi</groupId>
     <artifactId>ruoyi</artifactId>
-    <version>3.8.0</version>
+    <version>3.8.1</version>
 
     <name>ruoyi</name>
     <url>http://www.ruoyi.vip</url>
     <description>若依管理系统</description>
     
     <properties>
-        <ruoyi.version>3.8.0</ruoyi.version>
+        <ruoyi.version>3.8.1</ruoyi.version>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>1.8</java.version>
@@ -32,6 +32,7 @@
         <commons.collections.version>3.2.2</commons.collections.version>
         <poi.version>4.1.2</poi.version>
         <velocity.version>2.3</velocity.version>
+        <log4j2.version>2.17.1</log4j2.version>
         <jwt.version>0.9.1</jwt.version>
     </properties>
 	
@@ -150,6 +151,19 @@
                 <version>${fastjson.version}</version>
             </dependency>
 
+            <!-- log4j日志组件 -->
+            <dependency>
+                <groupId>org.apache.logging.log4j</groupId>
+                <artifactId>log4j-api</artifactId>
+                <version>${log4j2.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.logging.log4j</groupId>
+                <artifactId>log4j-to-slf4j</artifactId>
+                <version>${log4j2.version}</version>
+            </dependency>
+
             <!-- Token生成与解析-->
             <dependency>
                 <groupId>io.jsonwebtoken</groupId>

+ 1 - 1
ruoyi-admin/pom.xml

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

+ 3 - 2
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysUserController.java

@@ -104,9 +104,10 @@ public class SysUserController extends BaseController
         ajax.put("posts", postService.selectPostAll());
         if (StringUtils.isNotNull(userId))
         {
-            ajax.put(AjaxResult.DATA_TAG, userService.selectUserById(userId));
+            SysUser sysUser = userService.selectUserById(userId);
+            ajax.put(AjaxResult.DATA_TAG, sysUser);
             ajax.put("postIds", postService.selectPostListByUserId(userId));
-            ajax.put("roleIds", roleService.selectRoleListByUserId(userId));
+            ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
         }
         return ajax;
     }

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

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

+ 1 - 1
ruoyi-common/pom.xml

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

+ 1 - 1
ruoyi-framework/pom.xml

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

+ 3 - 7
ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java

@@ -60,14 +60,10 @@ public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
         String url = request.getRequestURI();
 
         // 唯一值(没有消息头则使用请求地址)
-        String submitKey = request.getHeader(header);
-        if (StringUtils.isEmpty(submitKey))
-        {
-            submitKey = url;
-        }
+        String submitKey = StringUtils.trimToEmpty(request.getHeader(header));
 
-        // 唯一标识(指定key + 消息头)
-        String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
+        // 唯一标识(指定key + url + 消息头)
+        String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + url + submitKey;
 
         Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
         if (sessionObj != null)

+ 1 - 1
ruoyi-generator/pom.xml

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

+ 8 - 2
ruoyi-generator/src/main/resources/vm/vue/index-tree.vue.vm

@@ -105,6 +105,12 @@
           <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
+#elseif($column.list && $column.htmlType == "imageUpload")
+      <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+        <template slot-scope="scope">
+          <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+        </template>
+      </el-table-column>
 #elseif($column.list && "" != $column.dictType)
       <el-table-column label="${comment}" align="center" prop="${javaField}">
         <template slot-scope="scope">
@@ -174,11 +180,11 @@
         </el-form-item>
 #elseif($column.htmlType == "imageUpload")
         <el-form-item label="${comment}">
-          <imageUpload v-model="form.${field}"/>
+          <image-upload v-model="form.${field}"/>
         </el-form-item>
 #elseif($column.htmlType == "fileUpload")
         <el-form-item label="${comment}">
-          <fileUpload v-model="form.${field}"/>
+          <file-upload v-model="form.${field}"/>
         </el-form-item>
 #elseif($column.htmlType == "editor")
         <el-form-item label="${comment}">

+ 8 - 2
ruoyi-generator/src/main/resources/vm/vue/index.vue.vm

@@ -133,6 +133,12 @@
           <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
+#elseif($column.list && $column.htmlType == "imageUpload")
+      <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+        <template slot-scope="scope">
+          <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+        </template>
+      </el-table-column>
 #elseif($column.list && "" != $column.dictType)
       <el-table-column label="${comment}" align="center" prop="${javaField}">
         <template slot-scope="scope">
@@ -195,11 +201,11 @@
         </el-form-item>
 #elseif($column.htmlType == "imageUpload")
         <el-form-item label="${comment}">
-          <imageUpload v-model="form.${field}"/>
+          <image-upload v-model="form.${field}"/>
         </el-form-item>
 #elseif($column.htmlType == "fileUpload")
         <el-form-item label="${comment}">
-          <fileUpload v-model="form.${field}"/>
+          <file-upload v-model="form.${field}"/>
         </el-form-item>
 #elseif($column.htmlType == "editor")
         <el-form-item label="${comment}">

+ 9 - 3
ruoyi-generator/src/main/resources/vm/vue/v3/index-tree.vue.vm

@@ -64,7 +64,7 @@
 #end
 #end
       <el-form-item>
-	    <el-button type="primary" icon="Search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button type="primary" icon="Search" size="mini" @click="handleQuery">搜索</el-button>
         <el-button icon="Refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
@@ -105,6 +105,12 @@
           <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
+#elseif($column.list && $column.htmlType == "imageUpload")
+      <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+        <template #default="scope">
+          <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+        </template>
+      </el-table-column>
 #elseif($column.list && "" != $column.dictType)
       <el-table-column label="${comment}" align="center" prop="${javaField}">
         <template #default="scope">
@@ -179,11 +185,11 @@
         </el-form-item>
 #elseif($column.htmlType == "imageUpload")
         <el-form-item label="${comment}">
-          <imageUpload v-model="form.${field}"/>
+          <image-upload v-model="form.${field}"/>
         </el-form-item>
 #elseif($column.htmlType == "fileUpload")
         <el-form-item label="${comment}">
-          <fileUpload v-model="form.${field}"/>
+          <file-upload v-model="form.${field}"/>
         </el-form-item>
 #elseif($column.htmlType == "editor")
         <el-form-item label="${comment}">

+ 8 - 2
ruoyi-generator/src/main/resources/vm/vue/v3/index.vue.vm

@@ -133,6 +133,12 @@
           <span>{{ parseTime(scope.row.${javaField}, '{y}-{m}-{d}') }}</span>
         </template>
       </el-table-column>
+#elseif($column.list && $column.htmlType == "imageUpload")
+      <el-table-column label="${comment}" align="center" prop="${javaField}" width="100">
+        <template #default="scope">
+          <image-preview :src="scope.row.${javaField}" :width="50" :height="50"/>
+        </template>
+      </el-table-column>
 #elseif($column.list && "" != $column.dictType)
       <el-table-column label="${comment}" align="center" prop="${javaField}">
         <template #default="scope">
@@ -195,11 +201,11 @@
         </el-form-item>
 #elseif($column.htmlType == "imageUpload")
         <el-form-item label="${comment}">
-          <imageUpload v-model="form.${field}"/>
+          <image-upload v-model="form.${field}"/>
         </el-form-item>
 #elseif($column.htmlType == "fileUpload")
         <el-form-item label="${comment}">
-          <fileUpload v-model="form.${field}"/>
+          <file-upload v-model="form.${field}"/>
         </el-form-item>
 #elseif($column.htmlType == "editor")
         <el-form-item label="${comment}">

+ 1 - 1
ruoyi-quartz/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>ruoyi</artifactId>
         <groupId>com.ruoyi</groupId>
-        <version>3.8.0</version>
+        <version>3.8.1</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.8.0</version>
+        <version>3.8.1</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 

+ 1 - 1
ruoyi-ui/package.json

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

+ 2 - 2
ruoyi-ui/src/api/system/user.js

@@ -1,5 +1,5 @@
 import request from '@/utils/request'
-import { praseStrEmpty } from "@/utils/ruoyi";
+import { parseStrEmpty } from "@/utils/ruoyi";
 
 // 查询用户列表
 export function listUser(query) {
@@ -13,7 +13,7 @@ export function listUser(query) {
 // 查询用户详细
 export function getUser(userId) {
   return request({
-    url: '/system/user/' + praseStrEmpty(userId),
+    url: '/system/user/' + parseStrEmpty(userId),
     method: 'get'
   })
 }

+ 114 - 114
ruoyi-ui/src/components/Crontab/hour.vue

@@ -1,114 +1,114 @@
-<template>
-	<el-form size="small">
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="1">
-				小时,允许的通配符[, - * /]
-			</el-radio>
-		</el-form-item>
-
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="2">
-				周期从
-				<el-input-number v-model='cycle01' :min="0" :max="22" /> -
-				<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="23" /> 小时
-			</el-radio>
-		</el-form-item>
-
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="3">
-				从
-				<el-input-number v-model='average01' :min="0" :max="22" /> 小时开始,每
-				<el-input-number v-model='average02' :min="1" :max="23 - average01 || 0" /> 小时执行一次
-			</el-radio>
-		</el-form-item>
-
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="4">
-				指定
-				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
-					<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
-	</el-form>
-</template>
-
-<script>
-export default {
-	data() {
-		return {
-			radioValue: 1,
-			cycle01: 0,
-			cycle02: 1,
-			average01: 0,
-			average02: 1,
-			checkboxList: [],
-			checkNum: this.$options.propsData.check
-		}
-	},
-	name: 'crontab-hour',
-	props: ['check', 'cron'],
-	methods: {
-		// 单选按钮值变化时
-		radioChange() {
-			switch (this.radioValue) {
-				case 1:
-        	this.$emit('update', 'hour', '*')
-        	break;
-				case 2:
-					this.$emit('update', 'hour', this.cycleTotal);
-					break;
-				case 3:
-					this.$emit('update', 'hour', this.averageTotal);
-					break;
-				case 4:
-					this.$emit('update', 'hour', this.checkboxString);
-					break;
-			}
-		},
-		// 周期两个值变化时
-		cycleChange() {
-			if (this.radioValue == '2') {
-				this.$emit('update', 'hour', this.cycleTotal);
-			}
-		},
-		// 平均两个值变化时
-		averageChange() {
-			if (this.radioValue == '3') {
-				this.$emit('update', 'hour', this.averageTotal);
-			}
-		},
-		// checkbox值变化时
-		checkboxChange() {
-			if (this.radioValue == '4') {
-				this.$emit('update', 'hour', this.checkboxString);
-			}
-		}
-	},
-	watch: {
-		'radioValue': 'radioChange',
-		'cycleTotal': 'cycleChange',
-		'averageTotal': 'averageChange',
-		'checkboxString': 'checkboxChange'
-	},
-	computed: {
-		// 计算两个周期值
-		cycleTotal: function () {
-			const cycle01 = this.checkNum(this.cycle01, 0, 22)
-			const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 23)
-			return cycle01 + '-' + cycle02;
-		},
-		// 计算平均用到的值
-		averageTotal: function () {
-			const average01 = this.checkNum(this.average01, 0, 22)
-			const average02 = this.checkNum(this.average02, 1, 23 - average01 || 0)
-			return average01 + '/' + average02;
-		},
-		// 计算勾选的checkbox值合集
-		checkboxString: function () {
-			let str = this.checkboxList.join();
-			return str == '' ? '*' : str;
-		}
-	}
-}
-</script>
+<template>
+	<el-form size="small">
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="1">
+				小时,允许的通配符[, - * /]
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="2">
+				周期从
+				<el-input-number v-model='cycle01' :min="0" :max="22" /> -
+				<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="23" /> 小时
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="3">
+				从
+				<el-input-number v-model='average01' :min="0" :max="22" /> 小时开始,每
+				<el-input-number v-model='average02' :min="1" :max="23 - average01 || 0" /> 小时执行一次
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="4">
+				指定
+				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
+					<el-option v-for="item in 24" :key="item" :value="item-1">{{item-1}}</el-option>
+				</el-select>
+			</el-radio>
+		</el-form-item>
+	</el-form>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			radioValue: 1,
+			cycle01: 0,
+			cycle02: 1,
+			average01: 0,
+			average02: 1,
+			checkboxList: [],
+			checkNum: this.$options.propsData.check
+		}
+	},
+	name: 'crontab-hour',
+	props: ['check', 'cron'],
+	methods: {
+		// 单选按钮值变化时
+		radioChange() {
+			switch (this.radioValue) {
+				case 1:
+        	this.$emit('update', 'hour', '*')
+        	break;
+				case 2:
+					this.$emit('update', 'hour', this.cycleTotal);
+					break;
+				case 3:
+					this.$emit('update', 'hour', this.averageTotal);
+					break;
+				case 4:
+					this.$emit('update', 'hour', this.checkboxString);
+					break;
+			}
+		},
+		// 周期两个值变化时
+		cycleChange() {
+			if (this.radioValue == '2') {
+				this.$emit('update', 'hour', this.cycleTotal);
+			}
+		},
+		// 平均两个值变化时
+		averageChange() {
+			if (this.radioValue == '3') {
+				this.$emit('update', 'hour', this.averageTotal);
+			}
+		},
+		// checkbox值变化时
+		checkboxChange() {
+			if (this.radioValue == '4') {
+				this.$emit('update', 'hour', this.checkboxString);
+			}
+		}
+	},
+	watch: {
+		'radioValue': 'radioChange',
+		'cycleTotal': 'cycleChange',
+		'averageTotal': 'averageChange',
+		'checkboxString': 'checkboxChange'
+	},
+	computed: {
+		// 计算两个周期值
+		cycleTotal: function () {
+			const cycle01 = this.checkNum(this.cycle01, 0, 22)
+			const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 23)
+			return cycle01 + '-' + cycle02;
+		},
+		// 计算平均用到的值
+		averageTotal: function () {
+			const average01 = this.checkNum(this.average01, 0, 22)
+			const average02 = this.checkNum(this.average02, 1, 23 - average01 || 0)
+			return average01 + '/' + average02;
+		},
+		// 计算勾选的checkbox值合集
+		checkboxString: function () {
+			let str = this.checkboxList.join();
+			return str == '' ? '*' : str;
+		}
+	}
+}
+</script>

+ 68 - 51
ruoyi-ui/src/components/ImagePreview/index.vue

@@ -1,67 +1,84 @@
 <template>
-    <el-image :src="`${realSrc}`" fit="cover" :style="`width:${realWidth};height:${realHeight};`" :preview-src-list="[`${realSrc}`]">
-        <div slot="error" class="image-slot">
-          <i class="el-icon-picture-outline"></i>
-        </div>
-    </el-image>
+  <el-image
+    :src="`${realSrc}`"
+    fit="cover"
+    :style="`width:${realWidth};height:${realHeight};`"
+    :preview-src-list="realSrcList"
+  >
+    <div slot="error" class="image-slot">
+      <i class="el-icon-picture-outline"></i>
+    </div>
+  </el-image>
 </template>
 
 <script>
-import { isExternal } from '@/utils/validate'
+import { isExternal } from "@/utils/validate";
 
 export default {
-    name: 'ImagePreview',
-    props: {
-        src: {
-            type: String,
-            required: true
-        },
-        width: {
-            type: [Number, String],
-            default: ''
-        },
-        height: {
-            type: [Number, String],
-            default: ''
-        }
+  name: "ImagePreview",
+  props: {
+    src: {
+      type: String,
+      required: true
+    },
+    width: {
+      type: [Number, String],
+      default: ""
     },
-    computed: {
-        realSrc() {
-            if (isExternal(this.src)) {
-                return this.src
-            }
-            return process.env.VUE_APP_BASE_API + this.src
-        },
-        realWidth() {
-            return typeof this.width == 'string' ? this.width : `${this.width}px`
-        },
-        realHeight() {
-            return typeof this.height == 'string' ? this.height : `${this.height}px`
+    height: {
+      type: [Number, String],
+      default: ""
+    }
+  },
+  computed: {
+    realSrc() {
+      let real_src = this.src.split(",")[0];
+      if (isExternal(real_src)) {
+        return real_src;
+      }
+      return process.env.VUE_APP_BASE_API + real_src;
+    },
+    realSrcList() {
+      let real_src_list = this.src.split(",");
+      let srcList = [];
+      real_src_list.forEach(item => {
+        if (isExternal(item)) {
+          return srcList.push(item);
         }
+        return srcList.push(process.env.VUE_APP_BASE_API + item);
+      });
+      return srcList;
+    },
+    realWidth() {
+      return typeof this.width == "string" ? this.width : `${this.width}px`;
+    },
+    realHeight() {
+      return typeof this.height == "string" ? this.height : `${this.height}px`;
     }
-}
+  },
+};
 </script>
 
 <style lang="scss" scoped>
 .el-image {
-    border-radius: 5px;
-    background-color: #ebeef5;
-    box-shadow: 0 0 5px 1px #ccc;
-    ::v-deep .el-image__inner {
-        transition: all 0.3s;
-        cursor: pointer;
-        &:hover {
-            transform: scale(1.2);
-        }
-    }
-    ::v-deep .image-slot {
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        width: 100%;
-        height: 100%;
-        color: #909399;
-        font-size: 30px;
+  border-radius: 5px;
+  background-color: #ebeef5;
+  box-shadow: 0 0 5px 1px #ccc;
+  ::v-deep .el-image__inner {
+    transition: all 0.3s;
+    cursor: pointer;
+    &:hover {
+      transform: scale(1.2);
     }
+  }
+  ::v-deep .image-slot {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+    color: #909399;
+    font-size: 30px;
+  }
 }
 </style>

+ 40 - 3
ruoyi-ui/src/utils/request.js

@@ -4,9 +4,12 @@ import store from '@/store'
 import { getToken } from '@/utils/auth'
 import errorCode from '@/utils/errorCode'
 import { tansParams, blobValidate } from "@/utils/ruoyi";
+import cache from '@/plugins/cache'
 import { saveAs } from 'file-saver'
 
 let downloadLoadingInstance;
+// 是否显示重新登录
+let isReloginShow;
 
 axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
 // 创建axios实例
@@ -21,6 +24,8 @@ const service = axios.create({
 service.interceptors.request.use(config => {
   // 是否需要设置 token
   const isToken = (config.headers || {}).isToken === false
+  // 是否需要防止数据重复提交
+  const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
   if (getToken() && !isToken) {
     config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
   }
@@ -31,6 +36,29 @@ service.interceptors.request.use(config => {
     config.params = {};
     config.url = url;
   }
+  if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
+    const requestObj = {
+      url: config.url,
+      data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
+      time: new Date().getTime()
+    }
+    const sessionObj = cache.session.getJSON('sessionObj')
+    if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
+      cache.session.setJSON('sessionObj', requestObj)
+    } else {
+      const s_url = sessionObj.url;                  // 请求地址
+      const s_data = sessionObj.data;                // 请求数据
+      const s_time = sessionObj.time;                // 请求时间
+      const interval = 1000;                         // 间隔时间(ms),小于此时间视为重复提交
+      if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
+        const message = '数据正在处理,请勿重复提交';
+        console.warn(`[${s_url}]: ` + message)
+        return Promise.reject(new Error(message))
+      } else {
+        cache.session.setJSON('sessionObj', requestObj)
+      }
+    }
+  }
   return config
 }, error => {
     console.log(error)
@@ -48,16 +76,25 @@ service.interceptors.response.use(res => {
       return res.data
     }
     if (code === 401) {
-      MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
+      if (!isReloginShow) {
+        isReloginShow = true;
+        MessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
           confirmButtonText: '重新登录',
           cancelButtonText: '取消',
           type: 'warning'
         }
       ).then(() => {
+        isReloginShow = false;
         store.dispatch('LogOut').then(() => {
-          location.href = '/index';
+          // 如果是登录页面不需要重新加载
+          if (window.location.hash.indexOf("#/login") != 0) {
+            location.href = '/index';
+          }
         })
-      }).catch(() => {});
+      }).catch(() => {
+        isReloginShow = false;
+      });
+    }
       return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
     } else if (code === 500) {
       Message({

+ 170 - 165
ruoyi-ui/src/utils/ruoyi.js

@@ -1,3 +1,5 @@
+
+
 /**
  * 通用js方法封装处理
  * Copyright (c) 2019 ruoyi
@@ -5,130 +7,133 @@
 
 // 日期格式化
 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
+  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();
-	}
+  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;
+  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) {
-	var actions = [];
-	Object.keys(datas).some((key) => {
-		if (datas[key].value == ('' + value)) {
-			actions.push(datas[key].label);
-			return true;
-		}
-	})
-	return actions.join('');
+  var actions = [];
+  Object.keys(datas).some((key) => {
+    if (datas[key].value == ('' + value)) {
+      actions.push(datas[key].label);
+      return true;
+    }
+  })
+  return actions.join('');
 }
 
 // 回显数据字典(字符串数组)
 export function selectDictLabels(datas, value, separator) {
-	var actions = [];
-	var currentSeparator = undefined === separator ? "," : separator;
-	var temp = value.split(currentSeparator);
-	Object.keys(value.split(currentSeparator)).some((val) => {
-		Object.keys(datas).some((key) => {
-			if (datas[key].value == ('' + temp[val])) {
-				actions.push(datas[key].label + currentSeparator);
-			}
-		})
-	})
-	return actions.join('').substring(0, actions.join('').length - 1);
+  if(value === undefined) {
+    return "";
+  }
+  var actions = [];
+  var currentSeparator = undefined === separator ? "," : separator;
+  var temp = value.split(currentSeparator);
+  Object.keys(value.split(currentSeparator)).some((val) => {
+    Object.keys(datas).some((key) => {
+      if (datas[key].value == ('' + temp[val])) {
+        actions.push(datas[key].label + 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 : '';
+  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 praseStrEmpty(str) {
-	if (!str || str == "undefined" || str == "null") {
-		return "";
-	}
-	return str;
+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];
-        }
+  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;
+  }
+  return source;
 };
 
 /**
@@ -139,47 +144,47 @@ export function mergeRecursive(source, target) {
  * @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;
+  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;
 }
 
 /**
@@ -187,34 +192,34 @@ export function handleTree(data, id, parentId, children) {
 * @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 && typeof (value) !== "undefined") {
-			if (typeof value === 'object') {
-				for (const key of Object.keys(value)) {
-					if (value[key] !== null && typeof (value[key]) !== 'undefined') {
-						let params = propName + '[' + key + ']';
-						var subPart = encodeURIComponent(params) + "=";
-						result += subPart + encodeURIComponent(value[key]) + "&";
-					}
-				}
-			} else {
-				result += part + encodeURIComponent(value) + "&";
-			}
-		}
-	}
-	return result
+  let result = ''
+  for (const propName of Object.keys(params)) {
+    const value = params[propName];
+    var part = encodeURIComponent(propName) + "=";
+    if (value !== null && typeof (value) !== "undefined") {
+      if (typeof value === 'object') {
+        for (const key of Object.keys(value)) {
+          if (value[key] !== null && 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;
-    }
-}
+  try {
+    const text = await data.text();
+    JSON.parse(text);
+    return false;
+  } catch (error) {
+    return true;
+  }
+}

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

@@ -147,6 +147,39 @@
             <span>更新日志</span>
           </div>
           <el-collapse accordion>
+            <el-collapse-item title="v3.8.1 - 2022-01-01">
+              <ol>
+                <li>新增Vue3前端代码生成模板</li>
+                <li>新增图片预览组件</li>
+                <li>新增压缩插件实现打包Gzip</li>
+                <li>自定义xss校验注解实现</li>
+                <li>自定义文字复制剪贴指令</li>
+                <li>代码生成预览支持复制内容</li>
+                <li>路由支持单独配置菜单或角色权限</li>
+                <li>用户管理部门查询选择节点后分页参数初始</li>
+                <li>修复用户分配角色属性错误</li>
+                <li>修复打包后字体图标偶现的乱码问题</li>
+                <li>修复菜单管理重置表单出现的错误</li>
+                <li>修复版本差异导致的懒加载报错问题</li>
+                <li>修复Cron组件中周回显问题</li>
+                <li>修复定时任务多参数逗号分隔的问题</li>
+                <li>修复根据ID查询列表可能出现的主键溢出问题</li>
+                <li>修复tomcat配置参数已过期问题</li>
+                <li>升级clipboard到最新版本2.0.8</li>
+                <li>升级oshi到最新版本v5.8.6</li>
+                <li>升级fastjson到最新版1.2.79</li>
+                <li>升级spring-boot到最新版本2.5.8</li>
+                <li>升级log4j2到2.17.1,防止漏洞风险</li>
+                <li>优化下载解析blob异常提示</li>
+                <li>优化代码生成字典组重复问题</li>
+                <li>优化查询用户的角色组&岗位组代码</li>
+                <li>优化定时任务cron表达式小时设置24</li>
+                <li>优化用户导入提示溢出则显示滚动条</li>
+                <li>优化防重复提交标识组合为(key+url+header)</li>
+                <li>优化分页方法设置成通用方便灵活调用</li>
+                <li>其他细节优化</li>
+              </ol>
+            </el-collapse-item>
             <el-collapse-item title="v3.8.0 - 2021-12-01">
               <ol>
                 <li>新增配套并同步的Vue3前端版本</li>
@@ -717,7 +750,7 @@ export default {
   data() {
     return {
       // 版本号
-      version: "3.8.0",
+      version: "3.8.1",
     };
   },
   methods: {

+ 1 - 1
ruoyi-ui/src/views/system/user/index.vue

@@ -495,7 +495,7 @@ export default {
     // 节点单击事件
     handleNodeClick(data) {
       this.queryParams.deptId = data.id;
-      this.getList();
+      this.handleQuery();
     },
     // 用户状态修改
     handleStatusChange(row) {

+ 1 - 0
ruoyi-ui/src/views/tool/gen/editTable.vue

@@ -33,6 +33,7 @@
                 <el-option label="Double" value="Double" />
                 <el-option label="BigDecimal" value="BigDecimal" />
                 <el-option label="Date" value="Date" />
+                <el-option label="Boolean" value="Boolean" />
               </el-select>
             </template>
           </el-table-column>