Forráskód Böngészése

JeecgBoot 2.4.6版本发布

zhangdaiscott 3 éve
szülő
commit
664413e5d7
57 módosított fájl, 1308 hozzáadás és 331 törlés
  1. 1 1
      ant-design-vue-jeecg/README.md
  2. 3 3
      ant-design-vue-jeecg/package.json
  3. 16 5
      ant-design-vue-jeecg/src/components/JVxeCells/JVxeFileCell.vue
  4. 16 16
      ant-design-vue-jeecg/src/components/JVxeCells/JVxeImageCell.vue
  5. 6 6
      ant-design-vue-jeecg/src/components/_util/Area.js
  6. 11 0
      ant-design-vue-jeecg/src/components/_util/StringUtil.js
  7. 3 4
      ant-design-vue-jeecg/src/components/jeecg/JAreaLinkage.vue
  8. 4 0
      ant-design-vue-jeecg/src/components/jeecg/JCodeEditor.vue
  9. 1 1
      ant-design-vue-jeecg/src/components/jeecg/JDate.vue
  10. 45 10
      ant-design-vue-jeecg/src/components/jeecg/JEasyCron/EasyCron.vue
  11. 2 2
      ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/day.vue
  12. 2 2
      ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/hour.vue
  13. 2 2
      ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/minute.vue
  14. 12 1
      ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/mixin.js
  15. 2 2
      ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/month.vue
  16. 2 2
      ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/second.vue
  17. 9 8
      ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/week.vue
  18. 118 32
      ant-design-vue-jeecg/src/components/jeecg/JEditableTable.vue
  19. 11 3
      ant-design-vue-jeecg/src/components/jeecg/JEditor.vue
  20. 242 0
      ant-design-vue-jeecg/src/components/jeecg/JModal/JModal.vue
  21. 124 0
      ant-design-vue-jeecg/src/components/jeecg/JModal/JPrompt.vue
  22. 18 0
      ant-design-vue-jeecg/src/components/jeecg/JModal/index.js
  23. 3 1
      ant-design-vue-jeecg/src/components/jeecg/JPopup.vue
  24. 33 4
      ant-design-vue-jeecg/src/components/jeecg/JSelectMultiple.vue
  25. 2 1
      ant-design-vue-jeecg/src/components/jeecg/JSuperQuery.vue
  26. 9 3
      ant-design-vue-jeecg/src/components/jeecg/JUpload.vue
  27. 5 1
      ant-design-vue-jeecg/src/components/jeecg/JVxeTable/components/JVxeTable.js
  28. 12 3
      ant-design-vue-jeecg/src/components/jeecg/JVxeTable/components/cells/JVxeUploadCell.vue
  29. 17 3
      ant-design-vue-jeecg/src/components/jeecg/index.js
  30. 6 2
      ant-design-vue-jeecg/src/components/jeecg/modal/JPopupOnlReport.vue
  31. 58 36
      ant-design-vue-jeecg/src/components/jeecgbiz/JSelectDepart.vue
  32. 54 20
      ant-design-vue-jeecg/src/components/jeecgbiz/JSelectUserByDep.vue
  33. 93 34
      ant-design-vue-jeecg/src/components/jeecgbiz/modal/JSelectDepartModal.vue
  34. 16 16
      ant-design-vue-jeecg/src/components/jeecgbiz/modal/JSelectUserByDepModal.vue
  35. 7 2
      ant-design-vue-jeecg/src/components/tools/Logo.vue
  36. 15 0
      ant-design-vue-jeecg/src/config/router.config.js
  37. 7 5
      ant-design-vue-jeecg/src/mixins/JeecgListMixin.js
  38. 4 2
      ant-design-vue-jeecg/src/mixins/OnlineCommonUtil.js
  39. 15 6
      ant-design-vue-jeecg/src/permission.js
  40. 0 4
      ant-design-vue-jeecg/src/store/index.js
  41. 1 0
      ant-design-vue-jeecg/src/store/modules/user.js
  42. 1 0
      ant-design-vue-jeecg/src/store/mutation-types.js
  43. 2 0
      ant-design-vue-jeecg/src/utils/encryption/signMd5Utils.js
  44. 42 25
      ant-design-vue-jeecg/src/utils/request.js
  45. 32 1
      ant-design-vue-jeecg/src/utils/util.js
  46. 1 1
      ant-design-vue-jeecg/src/views/account/settings/AvatarModal.vue
  47. 3 2
      ant-design-vue-jeecg/src/views/jeecg/SelectDemo.vue
  48. 6 1
      ant-design-vue-jeecg/src/views/system/QuartzJobList.vue
  49. 0 10
      ant-design-vue-jeecg/src/views/system/SysDataSourceList.vue
  50. 1 1
      ant-design-vue-jeecg/src/views/system/SysOnlineList.vue
  51. 1 1
      ant-design-vue-jeecg/src/views/system/UserList.vue
  52. 8 7
      ant-design-vue-jeecg/src/views/system/modules/SysCategoryModal.vue
  53. 43 7
      ant-design-vue-jeecg/src/views/system/modules/SysDataSourceModal.vue
  54. 1 1
      ant-design-vue-jeecg/src/views/system/modules/UserModal.vue
  55. 12 12
      ant-design-vue-jeecg/src/views/user/Login.vue
  56. 18 19
      ant-design-vue-jeecg/src/views/user/LoginSelectTenant.vue
  57. 130 0
      ant-design-vue-jeecg/src/views/user/oauth2/OAuth2Login.vue

+ 1 - 1
ant-design-vue-jeecg/README.md

@@ -1,7 +1,7 @@
 Ant Design Jeecg Vue
 ====
 
-当前最新版本: 2.4.5(发布日期:20210607
+当前最新版本: 2.4.6(发布日期:20210816
 
 Overview
 ----

+ 3 - 3
ant-design-vue-jeecg/package.json

@@ -1,6 +1,6 @@
 {
   "name": "vue-antd-jeecg",
-  "version": "2.4.5",
+  "version": "2.4.6",
   "private": true,
   "scripts": {
     "pre": "cnpm install || yarn --registry https://registry.npm.taobao.org || npm install --registry https://registry.npm.taobao.org ",
@@ -11,7 +11,7 @@
   },
   "dependencies": {
     "ant-design-vue": "^1.7.2",
-    "@jeecg/antd-online-mini": "2.4.5-RC",
+    "@jeecg/antd-online-mini": "2.4.6-beta",
     "@antv/data-set": "^0.11.4",
     "viser-vue": "^2.4.8",
     "axios": "^0.18.0",
@@ -39,7 +39,7 @@
     "tinymce": "^5.3.2",
     "@toast-ui/editor": "^2.1.2",
     "vue-area-linkage": "^5.1.0",
-    "area-data": "^5.0.6",
+    "china-area-data": "^5.0.1",
     "dom-align": "1.12.0",
     "xe-utils": "2.4.8",
     "vxe-table": "2.9.13",

+ 16 - 5
ant-design-vue-jeecg/src/components/JVxeCells/JVxeFileCell.vue

@@ -12,9 +12,9 @@
           <span style="margin-left:5px">{{ ellipsisFileName }}</span>
         </a-tooltip>
 
-        <a-tooltip v-else :title="file.name">
-          <a-icon type="paper-clip" style="color:red;"/>
-          <span style="color:red;margin-left:5px">{{ ellipsisFileName }}</span>
+        <a-tooltip v-else :title="file.message||'上传失败'">
+          <a-icon type="exclamation-circle" style="color:red;"/>
+          <span style="margin-left:5px">{{ ellipsisFileName }}</span>
         </a-tooltip>
 
         <template style="width: 30px">
@@ -179,8 +179,19 @@
           value['responseName'] = file.response[this.responseName]
         }
         if (file.status === 'done') {
-          value['path'] = file.response[this.responseName]
-          this.handleChangeCommon(value)
+          if (typeof file.response.success === 'boolean') {
+            if (file.response.success) {
+              value['path'] = file.response[this.responseName]
+              this.handleChangeCommon(value)
+            } else {
+              value['status'] = 'error'
+              value['message'] = file.response.message || '未知错误'
+            }
+          } else {
+            // 考虑到如果设置action上传路径为非jeecg-boot后台,可能不会返回 success 属性的情况,就默认为成功
+            value['path'] = file.response[this.responseName]
+            this.handleChangeCommon(value)
+          }
         } else if (file.status === 'error') {
           value['message'] = file.response.message || '未知错误'
         }

+ 16 - 16
ant-design-vue-jeecg/src/components/JVxeCells/JVxeImageCell.vue

@@ -10,20 +10,9 @@
         <template v-else-if="file['path']">
           <img class="j-editable-image" :src="imgSrc" alt="无图片" @click="handleMoreOperation"/>
         </template>
-        <template v-else>
-          <a-icon type="exclamation-circle" style="color: red;" @click="handleClickShowImageError"/>
-        </template>
-        <template slot="addonBefore" style="width: 30px">
-          <a-tooltip v-if="file.status==='uploading'" :title="`上传中(${Math.floor(file.percent)}%)`">
-            <a-icon type="loading"/>
-          </a-tooltip>
-          <a-tooltip v-else-if="file.status==='done'" title="上传完成">
-            <a-icon type="check-circle" style="color:#00DB00;"/>
-          </a-tooltip>
-          <a-tooltip v-else title="上传失败">
-            <a-icon type="exclamation-circle" style="color:red;"/>
-          </a-tooltip>
-        </template>
+        <a-tooltip v-else :title="file.message||'上传失败'" @click="handleClickShowImageError">
+          <a-icon type="exclamation-circle" style="color:red;"/>
+        </a-tooltip>
 
         <template style="width: 30px">
           <a-dropdown :trigger="['click']" placement="bottomRight" style="margin-left: 10px;">
@@ -196,8 +185,19 @@
           value['responseName'] = file.response[this.responseName]
         }
         if (file.status === 'done') {
-          value['path'] = file.response[this.responseName]
-          this.handleChangeCommon(value)
+          if (typeof file.response.success === 'boolean') {
+            if (file.response.success) {
+              value['path'] = file.response[this.responseName]
+              this.handleChangeCommon(value)
+            } else {
+              value['status'] = 'error'
+              value['message'] = file.response.message || '未知错误'
+            }
+          } else {
+            // 考虑到如果设置action上传路径为非jeecg-boot后台,可能不会返回 success 属性的情况,就默认为成功
+            value['path'] = file.response[this.responseName]
+            this.handleChangeCommon(value)
+          }
         } else if (file.status === 'error') {
           value['message'] = file.response.message || '未知错误'
         }

+ 6 - 6
ant-design-vue-jeecg/src/components/_util/Area.js

@@ -1,5 +1,3 @@
-import { pcaa } from 'area-data'
-
 /**
  * 省市区
  */
@@ -8,7 +6,7 @@ export default class Area {
    * 构造器
    * @param express
    */
-  constructor() {
+  constructor(pcaa) {
     let arr = []
     const province = pcaa['86']
     Object.keys(province).map(key=>{
@@ -17,9 +15,11 @@ export default class Area {
       Object.keys(city).map(key2=>{
         arr.push({id:key2, text:city[key2], pid:key, index:2});
         const qu = pcaa[key2];
-        Object.keys(qu).map(key3=>{
-          arr.push({id:key3, text:qu[key3], pid:key2, index:3});
-        })
+        if(qu){
+          Object.keys(qu).map(key3=>{
+            arr.push({id:key3, text:qu[key3], pid:key2, index:3});
+          })
+        }
       })
     })
     this.all = arr;

+ 11 - 0
ant-design-vue-jeecg/src/components/_util/StringUtil.js

@@ -32,4 +32,15 @@ export const cutStrByFullLength = (str = '', maxLength) => {
     }
     return pre
   }, '')
+}
+
+// 下划线转换驼峰
+export function underLinetoHump(name) {
+  return name.replace(/\_(\w)/g, function(all, letter){
+    return letter.toUpperCase();
+  });
+}
+// 驼峰转换下划线
+export function humptoUnderLine(name) {
+  return name.replace(/([A-Z])/g,"_$1").toLowerCase();
 }

+ 3 - 4
ant-design-vue-jeecg/src/components/jeecg/JAreaLinkage.vue

@@ -29,7 +29,6 @@
 </template>
 
 <script>
-  import { pcaa } from 'area-data'
   import Area from '@/components/_util/Area'
 
   export default {
@@ -53,7 +52,7 @@
     },
     data() {
       return {
-        pcaa,
+        pcaa: this.$Jpcaa,
         innerValue: [],
         usedListeners: ['change'],
         enums: {
@@ -114,7 +113,7 @@
       /** 通过地区code获取子级 */
       loadDataByCode(value) {
         let options = []
-        let data = pcaa[value]
+        let data = this.pcaa[value]
         if (data) {
           for (let key in data) {
             if (data.hasOwnProperty(key)) {
@@ -139,7 +138,7 @@
       },
       initAreaData(){
         if(!this.areaData){
-          this.areaData = new Area();
+          this.areaData = new Area(this.$Jpcaa);
         }
       },
 

+ 4 - 0
ant-design-vue-jeecg/src/components/jeecg/JCodeEditor.vue

@@ -400,6 +400,10 @@
     .null-tip-hidden{
       display: none;
     }
+    /**选中样式偶然出现高度不够的情况*/
+    .CodeMirror-selected{
+      min-height: 19px !important;
+    }
   }
 
   /* 全屏样式 */

+ 1 - 1
ant-design-vue-jeecg/src/components/jeecg/JDate.vue

@@ -8,7 +8,7 @@
     :showTime="showTime"
     :format="dateFormat"
     :getCalendarContainer="getCalendarContainer"
-  />
+    v-bind="$attrs"/>
 </template>
 <script>
   import moment from 'moment'

+ 45 - 10
ant-design-vue-jeecg/src/components/jeecg/JEasyCron/EasyCron.vue

@@ -158,7 +158,49 @@ export default {
     cronValue_c(newVal, oldVal) {
       this.calTriggerList()
       this.$emit('change', newVal)
-
+      this.assignInput()
+    },
+    minute() {
+      if (this.second === '*') {
+        this.second = '0'
+      }
+    },
+    hour() {
+      if (this.minute === '*') {
+        this.minute = '0'
+      }
+    },
+    day(day) {
+      if (day !== '?' && this.hour === '*') {
+        this.hour = '0'
+      }
+    },
+    week(week) {
+      if (week !== '?' && this.hour === '*') {
+        this.hour = '0'
+      }
+    },
+    month() {
+      if (this.day === '?' && this.week === '*') {
+        this.week = '1'
+      } else if (this.week === '?' && this.day === '*') {
+        this.day = '1'
+      }
+    },
+    year() {
+      if (this.month === '*') {
+        this.month = '1'
+      }
+    },
+  },
+  created() {
+    this.formatValue()
+    this.$nextTick(() => {
+      this.calTriggerListInner()
+    })
+  },
+  methods: {
+    assignInput() {
       Object.assign(this.inputValues, {
         second: this.second,
         minute: this.minute,
@@ -169,15 +211,7 @@ export default {
         year: this.year,
         cron: this.cronValue_c,
       })
-    }
-  },
-  created() {
-    this.formatValue()
-    this.$nextTick(() => {
-      this.calTriggerListInner()
-    })
-  },
-  methods: {
+    },
     formatValue() {
       if (!this.cronValue) return
       const values = this.cronValue.split(' ').filter(item => !!item)
@@ -190,6 +224,7 @@ export default {
       if (values.length > i) this.month = values[i++]
       if (values.length > i) this.week = values[i++]
       if (values.length > i) this.year = values[i]
+      this.assignInput()
     },
     calTriggerList: simpleDebounce(function () {
       this.calTriggerListInner()

+ 2 - 2
ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/day.vue

@@ -38,8 +38,8 @@
         <a-radio value="TYPE_SPECIFY" class="choice" :disabled="disableChoice">指定</a-radio>
         <div class="list">
           <a-checkbox-group v-model="valueList">
-            <template v-for="i in maxValue+1">
-              <a-checkbox class="list-check-item" :key="`key-${i-1}`" :value="i-1" :disabled="type!==TYPE_SPECIFY || disabled">{{i-1}}</a-checkbox>
+            <template v-for="i of specifyRange">
+              <a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{i}}</a-checkbox>
             </template>
           </a-checkbox-group>
         </div>

+ 2 - 2
ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/hour.vue

@@ -25,8 +25,8 @@
         <a-radio value="TYPE_SPECIFY" class="choice" :disabled="disabled">指定</a-radio>
         <div class="list">
           <a-checkbox-group v-model="valueList">
-            <template v-for="i in maxValue+1">
-              <a-checkbox class="list-check-item" :key="`key-${i-1}`" :value="i-1" :disabled="type!==TYPE_SPECIFY || disabled">{{i-1}}</a-checkbox>
+            <template v-for="i in specifyRange">
+              <a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{i}}</a-checkbox>
             </template>
           </a-checkbox-group>
         </div>

+ 2 - 2
ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/minute.vue

@@ -25,8 +25,8 @@
         <a-radio value="TYPE_SPECIFY" class="choice" :disabled="disabled">指定</a-radio>
         <div class="list">
           <a-checkbox-group v-model="valueList">
-            <template v-for="i in maxValue+1">
-              <a-checkbox class="list-check-item" :key="`key-${i-1}`" :value="i-1" :disabled="type!==TYPE_SPECIFY || disabled">{{i-1}}</a-checkbox>
+            <template v-for="i in specifyRange">
+              <a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{i}}</a-checkbox>
             </template>
           </a-checkbox-group>
         </div>

+ 12 - 1
ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/mixin.js

@@ -89,6 +89,9 @@ export default {
           result.push('L')
           break
         case TYPE_SPECIFY:
+          if (this.valueList.length === 0) {
+            this.valueList.push(this.minValue)
+          }
           result.push(this.valueList.join(','))
           break
         default:
@@ -96,7 +99,15 @@ export default {
           break
       }
       return result.length > 0 ? result.join('') : this.DEFAULT_VALUE
-    }
+    },
+    // 指定值范围区间,介于最小值和最大值之间
+    specifyRange() {
+      let range = []
+      for (let i = this.minValue; i <= this.maxValue; i++) {
+        range.push(i)
+      }
+      return range
+    },
   },
   methods: {
     parseProp (value) {

+ 2 - 2
ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/month.vue

@@ -25,8 +25,8 @@
         <a-radio value="TYPE_SPECIFY" class="choice" :disabled="disabled">指定</a-radio>
         <div class="list">
           <a-checkbox-group v-model="valueList">
-            <template v-for="i in maxValue+1">
-              <a-checkbox class="list-check-item" :key="`key-${i-1}`" :value="i-1" :disabled="type!==TYPE_SPECIFY || disabled">{{i-1}}</a-checkbox>
+            <template v-for="i of specifyRange">
+              <a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{i}}</a-checkbox>
             </template>
           </a-checkbox-group>
         </div>

+ 2 - 2
ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/second.vue

@@ -25,8 +25,8 @@
         <a-radio value="TYPE_SPECIFY" class="choice" :disabled="disabled">指定</a-radio>
         <div class="list">
           <a-checkbox-group v-model="valueList">
-            <template v-for="i in maxValue+1">
-              <a-checkbox class="list-check-item" :key="`key-${i-1}`" :value="i-1" :disabled="type!==TYPE_SPECIFY || disabled">{{i-1}}</a-checkbox>
+            <template v-for="i in specifyRange">
+              <a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{i}}</a-checkbox>
             </template>
           </a-checkbox-group>
         </div>

+ 9 - 8
ant-design-vue-jeecg/src/components/jeecg/JEasyCron/tabs/week.vue

@@ -36,8 +36,8 @@
         <a-radio value="TYPE_SPECIFY" class="choice" :disabled="disableChoice">指定</a-radio>
         <div class="list">
           <a-checkbox-group v-model="valueList">
-            <template v-for="i in maxValue+1">
-              <a-checkbox class="list-check-item" :key="`key-${i-1}`" :value="i-1" :disabled="type!==TYPE_SPECIFY || disabled">{{i-1}}</a-checkbox>
+            <template v-for="i in specifyRange">
+              <a-checkbox class="list-check-item" :key="`key-${i}`" :value="i" :disabled="type!==TYPE_SPECIFY || disabled">{{i}}</a-checkbox>
             </template>
           </a-checkbox-group>
         </div>
@@ -51,13 +51,14 @@ import mixin from './mixin'
 import { replaceWeekName, WEEK_MAP_EN } from './const.js'
 
 const WEEK_MAP = {
-  '周日': 0,
   '周一': 1,
   '周二': 2,
   '周三': 3,
   '周四': 4,
   '周五': 5,
-  '周六': 6
+  '周六': 6,
+  // 按照国人习惯,将周日放到每周的最后一天
+  '周日': 7,
 }
 
 export default {
@@ -101,10 +102,10 @@ export default {
   created() {
     this.DEFAULT_VALUE = '*'
     // 0,7表示周日 1表示周一
-    this.minValue = 0
-    this.maxValue = 6
-    this.valueRange.start = 0
-    this.valueRange.end = 6
+    this.minValue = 1
+    this.maxValue = 7
+    this.valueRange.start = 1
+    this.valueRange.end = 7
     this.valueLoop.start = 2
     this.valueLoop.interval = 1
     this.parseProp(this.prop)

+ 118 - 32
ant-design-vue-jeecg/src/components/jeecg/JEditableTable.vue

@@ -1,5 +1,5 @@
 <!-- JEditableTable -->
-<!-- @version 1.6.1 -->
+<!-- @version 1.6.2 -->
 <!-- @author sjlei -->
 <template>
   <a-spin :spinning="loading">
@@ -11,7 +11,33 @@
       <a-col>
         <!-- 操作按钮 -->
         <div v-if="actionButton" class="action-button">
-          <a-button v-if="buttonPermission('add')" type="primary" icon="plus" @click="handleClickAdd" :disabled="disabled">新增</a-button>
+          <a-button-group v-if="buttonPermission('add')">
+            <a-button type="primary" icon="plus" @click="handleClickAdd" :disabled="disabled">新增</a-button>
+            <a-popover v-if="addButtonSettings" placement="right" overlayClassName="j-add-btn-settings">
+              <a-row slot="title">
+                <a-col :span="12">选项</a-col>
+                <a-col :span="12" style="text-align: right;">
+                  <a-tooltip title="保存为默认值">
+                    <a-button type="link" icon="save" size="small" style="position: relative;left:4px;" @click="onAddButtonSettingsSave"/>
+                  </a-tooltip>
+                </a-col>
+              </a-row>
+              <template slot="content">
+                <a-form-model layout="horizontal" :labelCol="{span:8}" :wrapperCol="{span:16}">
+                  <a-form-model-item label="添加行数">
+                    <a-input-number v-model="settings.addRowNum" :min="1"/>
+                  </a-form-model-item>
+                  <a-form-model-item label="添加位置">
+                    <a-input-number v-model="settings.addIndex" :min="0" :max="rows.length"/>
+                    <p style="font-size: 12px;color:#aaa;line-height: 14px;text-align: right;margin: 0;">0 = 最底部</p>
+                  </a-form-model-item>
+                  <a-divider style="margin: 8px 0;"/>
+                  <a-checkbox v-model="settings.addScrollToBottom">添加后滚动到底部</a-checkbox>
+                </a-form-model>
+              </template>
+              <a-button icon="setting" type="primary"></a-button>
+            </a-popover>
+          </a-button-group>
           <span class="gap"></span>
           <template v-if="selectedRowIds.length>0">
             <a-popconfirm
@@ -318,7 +344,7 @@
                             <a-tooltip v-else-if="file.status==='done'" title="上传完成">
                               <a-icon type="check-circle" style="color:#00DB00;"/>
                             </a-tooltip>
-                            <a-tooltip v-else title="上传失败">
+                            <a-tooltip v-else :title="file.message||'上传失败'">
                               <a-icon type="exclamation-circle" style="color:red;"/>
                             </a-tooltip>
                           </template>
@@ -409,9 +435,9 @@
                             <span style="margin-left:5px">{{ getEllipsisWord(file.name,5) }}</span>
                           </a-tooltip>
 
-                          <a-tooltip v-else :title="file.name">
-                            <a-icon type="paper-clip" style="color:red;"/>
-                            <span style="color:red;margin-left:5px">{{ getEllipsisWord(file.name,5) }}</span>
+                          <a-tooltip v-else :title="file.message||'上传失败'">
+                            <a-icon type="exclamation-circle" style="color:red;"/>
+                            <span style="margin-left:5px">{{ getEllipsisWord(file.name,5) }}</span>
                           </a-tooltip>
 
                           <template style="width: 30px">
@@ -464,20 +490,9 @@
                           <template v-else-if="uploadValues[id]['path']">
                             <img class="j-editable-image" :src="getCellImageView(id)" alt="无图片" @click="handleMoreOperation(id,'img',col)"/>
                           </template>
-                          <template v-else>
-                            <a-icon type="exclamation-circle" style="color: red;" @click="handleClickShowImageError(id)"/>
-                          </template>
-                          <template slot="addonBefore" style="width: 30px">
-                            <a-tooltip v-if="file.status==='uploading'" :title="`上传中(${Math.floor(file.percent)}%)`">
-                              <a-icon type="loading"/>
-                            </a-tooltip>
-                            <a-tooltip v-else-if="file.status==='done'" title="上传完成">
-                              <a-icon type="check-circle" style="color:#00DB00;"/>
-                            </a-tooltip>
-                            <a-tooltip v-else title="上传失败">
-                              <a-icon type="exclamation-circle" style="color:red;"/>
-                            </a-tooltip>
-                          </template>
+                          <a-tooltip v-else :title="file.message||'上传失败'" @click="handleClickShowImageError(id)">
+                            <a-icon type="exclamation-circle" style="color:red;"/>
+                          </a-tooltip>
 
                           <template style="width: 30px">
                             <a-dropdown :trigger="['click']" placement="bottomRight" :getPopupContainer="getParentContainer" style="margin-left: 10px;">
@@ -738,6 +753,11 @@
         type: Boolean,
         default: false
       },
+      // 是否显示添加按钮选项
+      addButtonSettings: {
+        type: Boolean,
+        default: false
+      },
       // 是否显示行号
       rowNumber: {
         type: Boolean,
@@ -866,7 +886,16 @@
         lastPushTimeMap: new Map(),
         number:0,
         //不显示的按钮编码
-        excludeCode:[]
+        excludeCode:[],
+        // 选项配置
+        settings: {
+          // 添加行数
+          addRowNum: 1,
+          // 添加位置(下标),0 = 最底部
+          addIndex: 0,
+          // 添加后滚动到底部
+          addScrollToBottom: false,
+        },
       }
     },
     created() {
@@ -881,6 +910,7 @@
           event.stopPropagation()
         }
       }
+      this.getSavedAddButtonSettings()
     },
     // 计算属性
     computed: {
@@ -1412,22 +1442,18 @@
         let tbody = this.getElement('tbody')
         let offsetHeight = tbody.offsetHeight
         let realScrollTop = tbody.scrollTop + offsetHeight
-        if (forceScrollToBottom === false) {
-          // 只有滚动条在底部的时候才自动滚动
-          if (!((tbody.scrollHeight - realScrollTop) <= 10)) {
-            return
-          }
+        if (forceScrollToBottom) {
+          this.$nextTick(() => {
+            this.resetScrollTop(this.$refs.scrollView.scrollHeight)
+          })
         }
-        this.$nextTick(() => {
-          tbody.scrollTop = tbody.scrollHeight
-        })
       },
       /**
        * 在指定位置添加一行
        * @param insertIndex 添加位置下标
        * @param num 添加的行数,默认1
        */
-      insert(insertIndex, num = 1) {
+      insert(insertIndex, num = 1, forceScrollToBottom = false) {
         if (this.checkTooFastClick('insert', 1500)) {
           return
         }
@@ -1455,6 +1481,12 @@
           num, insertIndex,
           target: this
         })
+        // 设置滚动条位置
+        if (forceScrollToBottom) {
+          this.$nextTick(() => {
+            this.resetScrollTop(this.$refs.scrollView.scrollHeight)
+          })
+        }
       },
       /** 删除被选中的行 */
       removeSelectedRows() {
@@ -2095,7 +2127,12 @@
 
       },
       handleClickAdd() {
-        this.add()
+        let {addRowNum, addIndex, addScrollToBottom} = this.settings
+        if (addIndex <= 0) {
+          this.add(addRowNum, addScrollToBottom)
+        } else {
+          this.insert(addIndex, addRowNum, addScrollToBottom)
+        }
       },
       handleConfirmDelete() {
         this.removeSelectedRows()
@@ -2353,7 +2390,21 @@
           value['responseName'] = file.response[column.responseName]
         }
         if (file.status === 'done') {
-          value['path'] = file.response[column.responseName]
+          if (typeof file.response.success === 'boolean') {
+            // 如果文件上传,被拦截器拦下,还会返回最外层的status = done
+            // 但是内部的success会返回false并携带异常信息
+            // 整个上传操作还是失败的
+            // https://github.com/zhangdaiscott/jeecg-boot/issues/2691
+            if (file.response.success) {
+              value['path'] = file.response[column.responseName]
+            } else {
+              value['status'] = 'error'
+              value['message'] = file.response.message || '未知错误'
+            }
+          } else {
+            // 考虑到如果设置action上传路径为非jeecg-boot后台,可能不会返回 success 属性的情况,就默认为成功
+            value['path'] = file.response[column.responseName]
+          }
         } else if (file.status === 'error') {
           value['message'] = file.response.message || '未知错误'
         }
@@ -2415,6 +2466,25 @@
           })
         }
       },
+
+      /** 添加按钮设置保存为默认值 */
+      onAddButtonSettingsSave() {
+        let obj = {
+          addRowNum: this.settings.addRowNum,
+          addIndex: this.settings.addIndex,
+          addScrollToBottom: this.settings.addScrollToBottom,
+        }
+        this.$ls.set('jet-add-btn-settings', obj)
+        this.$message.success('保存成功')
+      },
+      /** 获取保存的添加按钮默认值 */
+      getSavedAddButtonSettings() {
+        let obj= this.$ls.get('jet-add-btn-settings')
+        if (obj) {
+          Object.assign(this.settings, obj)
+        }
+      },
+
       /** 记录用到数据绑定的组件的值 */
       bindValuesChange(value, id, key) {
         this.$set(this[key], id, value)
@@ -3280,3 +3350,19 @@
   }
 
 </style>
+<style lang="less">
+// 新增按钮配置气泡的样式
+.j-add-btn-settings {
+  width: 240px;
+
+  .ant-form {
+    .ant-form-item {
+      margin-bottom: 0;
+
+      .ant-input-number {
+        width: 100%;
+      }
+    }
+  }
+}
+</style>

+ 11 - 3
ant-design-vue-jeecg/src/components/jeecg/JEditor.vue

@@ -134,9 +134,17 @@
         }else{
           //update--begin--autor:wangshuai-----date:20200724------for:富文本编辑器切换tab无法修改------
           let tabLayout = getVmParentByName(this, 'TabLayout')
-          tabLayout.excuteCallback(()=>{
-            this.reload()
-          })
+          //update--begin--autor:liusq-----date:20210713------for:处理特殊情况excuteCallback不能使用------
+          try {
+            tabLayout.excuteCallback(() => {
+              this.reload()
+            })
+          } catch (error) {
+            if (tabLayout) {
+              this.reload()
+            }
+          }
+          //update--end--autor:liusq-----date:20210713------for:处理特殊情况excuteCallback不能使用------
           //update--begin--autor:wangshuai-----date:20200724------for:文本编辑器切换tab无法修改------
         }
       },

+ 242 - 0
ant-design-vue-jeecg/src/components/jeecg/JModal/JModal.vue

@@ -0,0 +1,242 @@
+<template>
+  <a-modal
+    ref="modal"
+    :class="getClass(modalClass)"
+    :style="getStyle(modalStyle)"
+    :visible="visible"
+    v-bind="_attrs"
+    v-on="$listeners"
+    @ok="handleOk"
+    @cancel="handleCancel"
+    destroyOnClose
+  >
+
+    <slot></slot>
+    <!--有设置标题-->
+    <template v-if="!isNoTitle" slot="title">
+      <a-row class="j-modal-title-row" type="flex">
+        <a-col class="left">
+          <slot name="title">{{ title }}</slot>
+        </a-col>
+        <a-col v-if="switchFullscreen" class="right" @click="toggleFullscreen">
+          <a-button class="ant-modal-close ant-modal-close-x" ghost type="link" :icon="fullscreenButtonIcon"/>
+        </a-col>
+      </a-row>
+    </template>
+    <!--没有设置标题-->
+    <template v-else slot="title">
+      <a-row class="j-modal-title-row" type="flex">
+        <a-col v-if="switchFullscreen" class="right" @click="toggleFullscreen">
+          <a-button class="ant-modal-close ant-modal-close-x" ghost type="link" :icon="fullscreenButtonIcon"/>
+        </a-col>
+      </a-row>
+    </template>
+
+    <!-- 处理 scopedSlots -->
+    <template v-for="slotName of scopedSlotsKeys" :slot="slotName">
+      <slot :name="slotName"></slot>
+    </template>
+
+    <!-- 处理 slots -->
+    <template v-for="slotName of slotsKeys" v-slot:[slotName]>
+      <slot :name="slotName"></slot>
+    </template>
+
+  </a-modal>
+</template>
+
+<script>
+
+import { getClass, getStyle } from '@/utils/props-util'
+import { triggerWindowResizeEvent } from '@/utils/util'
+
+export default {
+    name: 'JModal',
+    props: {
+      title: String,
+      // 可使用 .sync 修饰符
+      visible: Boolean,
+      // 是否全屏弹窗,当全屏时无论如何都会禁止 body 滚动。可使用 .sync 修饰符
+      fullscreen: {
+        type: Boolean,
+        default: false
+      },
+      // 是否允许切换全屏(允许后右上角会出现一个按钮)
+      switchFullscreen: {
+        type: Boolean,
+        default: false
+      },
+      // 点击确定按钮的时候是否关闭弹窗
+      okClose: {
+        type: Boolean,
+        default: true
+      },
+    },
+    data() {
+      return {
+        // 内部使用的 slots ,不再处理
+        usedSlots: ['title'],
+        // 实际控制是否全屏的参数
+        innerFullscreen: this.fullscreen,
+      }
+    },
+    computed: {
+      // 一些未处理的参数或特殊处理的参数绑定到 a-modal 上
+      _attrs() {
+        let attrs = { ...this.$attrs }
+        // 如果全屏就将宽度设为 100%
+        if (this.innerFullscreen) {
+          attrs['width'] = '100%'
+        }
+        return attrs
+      },
+      modalClass() {
+        return {
+          'j-modal-box': true,
+          'fullscreen': this.innerFullscreen,
+          'no-title': this.isNoTitle,
+          'no-footer': this.isNoFooter,
+        }
+      },
+      modalStyle() {
+        let style = {}
+        // 如果全屏就将top设为 0
+        if (this.innerFullscreen) {
+          style['top'] = '0'
+        }
+        return style
+      },
+      isNoTitle() {
+        return !this.title && !this.allSlotsKeys.includes('title')
+      },
+      isNoFooter() {
+        return this._attrs['footer'] === null
+      },
+      slotsKeys() {
+        return Object.keys(this.$slots).filter(key => !this.usedSlots.includes(key))
+      },
+      scopedSlotsKeys() {
+        return Object.keys(this.$scopedSlots).filter(key => !this.usedSlots.includes(key))
+      },
+      allSlotsKeys() {
+        return Object.keys(this.$slots).concat(Object.keys(this.$scopedSlots))
+      },
+      // 切换全屏的按钮图标
+      fullscreenButtonIcon() {
+        return this.innerFullscreen ? 'fullscreen-exit' : 'fullscreen'
+      },
+    },
+    watch: {
+      visible() {
+        if (this.visible) {
+          this.innerFullscreen = this.fullscreen
+        }
+      },
+      innerFullscreen(val) {
+        this.$emit('update:fullscreen', val)
+      },
+    },
+    methods: {
+
+      getClass(clazz) {
+        return { ...getClass(this), ...clazz }
+      },
+      getStyle(style) {
+        return { ...getStyle(this), ...style }
+      },
+
+      close() {
+        this.$emit('update:visible', false)
+      },
+
+      handleOk() {
+        if (this.okClose) {
+          this.close()
+        }
+      },
+      handleCancel() {
+        this.close()
+      },
+
+      /** 切换全屏 */
+      toggleFullscreen() {
+        this.innerFullscreen = !this.innerFullscreen
+        triggerWindowResizeEvent()
+      },
+
+    }
+  }
+</script>
+
+<style lang="less">
+  
+  .j-modal-box {
+    &.fullscreen {
+      top: 0;
+      left: 0;
+      padding: 0;
+
+      // 兼容1.6.2版本的antdv
+      & .ant-modal {
+        top: 0;
+        padding: 0;
+        height: 100vh;
+      }
+
+      & .ant-modal-content {
+        height: 100vh;
+        border-radius: 0;
+
+        & .ant-modal-body {
+          /* title 和 footer 各占 55px */
+          height: calc(100% - 55px - 55px);
+          overflow: auto;
+        }
+      }
+
+      &.no-title, &.no-footer {
+        .ant-modal-body {
+          height: calc(100% - 55px);
+        }
+      }
+      &.no-title.no-footer {
+        .ant-modal-body {
+          height: 100%;
+        }
+      }
+    }
+
+    .j-modal-title-row {
+      .left {
+        width: calc(100% - 56px - 56px);
+      }
+
+      .right {
+        width: 56px;
+        position: inherit;
+
+        .ant-modal-close {
+          right: 56px;
+          color: rgba(0, 0, 0, 0.45);
+
+          &:hover {
+            color: rgba(0, 0, 0, 0.75);
+          }
+        }
+      }
+    }
+    &.no-title{
+      .ant-modal-header {
+        padding: 0px 24px;
+        border-bottom: 0px !important;
+      }
+    }
+  }
+
+  @media (max-width: 767px) {
+    .j-modal-box.fullscreen {
+      margin: 0;
+      max-width: 100vw;
+    }
+  }
+</style>

+ 124 - 0
ant-design-vue-jeecg/src/components/jeecg/JModal/JPrompt.vue

@@ -0,0 +1,124 @@
+<template>
+  <j-modal :visible="visible" :confirmLoading="loading" :after-close="afterClose" v-bind="modalProps" @ok="onOk" @cancel="onCancel">
+    <a-spin :spinning="loading">
+      <div v-html="content"></div>
+      <a-form-model ref="form" :model="model" :rules="rules">
+        <a-form-model-item prop="input">
+          <a-input ref="input" v-model="model.input" v-bind="inputProps" @pressEnter="onInputPressEnter"/>
+        </a-form-model-item>
+      </a-form-model>
+    </a-spin>
+  </j-modal>
+</template>
+
+<script>
+import pick from 'lodash.pick'
+
+export default {
+  name: 'JPrompt',
+  data() {
+    return {
+      visible: false,
+      loading: false,
+      content: '',
+      // 弹窗参数
+      modalProps: {
+        title: '',
+      },
+      inputProps: {
+        placeholder: '',
+      },
+      // form model
+      model: {
+        input: '',
+      },
+      // 校验
+      rule: [],
+      // 回调函数
+      callback: {},
+    }
+  },
+  computed: {
+    rules() {
+      return {
+        input: this.rule
+      }
+    },
+  },
+  methods: {
+    show(options) {
+      this.content = options.content
+      if (Array.isArray(options.rule)) {
+        this.rule = options.rule
+      }
+      if (options.defaultValue != null) {
+        this.model.input = options.defaultValue
+      }
+      // 取出常用的弹窗参数
+      let pickModalProps = pick(options, 'title', 'centered', 'cancelText', 'closable', 'mask', 'maskClosable', 'okText', 'okType', 'okButtonProps', 'cancelButtonProps', 'width', 'wrapClassName', 'zIndex', 'dialogStyle', 'dialogClass')
+      this.modalProps = Object.assign({}, pickModalProps, options.modalProps)
+      // 取出常用的input参数
+      let pickInputProps = pick(options, 'placeholder', 'allowClear')
+      this.inputProps = Object.assign({}, pickInputProps, options.inputProps)
+      // 回调函数
+      this.callback = pick(options, 'onOk', 'onOkAsync', 'onCancel')
+      this.visible = true
+      this.$nextTick(() => this.$refs.input.focus())
+    },
+
+    onOk() {
+      this.$refs.form.validate((ok, err) => {
+        if (ok) {
+          let event = {value: this.model.input, target: this}
+          // 异步方法优先级高于同步方法
+          if (typeof this.callback.onOkAsync === 'function') {
+            this.callback.onOkAsync(event)
+          } else if (typeof this.callback.onOk === 'function') {
+            this.callback.onOk(event)
+            this.close()
+          } else {
+            this.close()
+          }
+        }
+      })
+    },
+    onCancel() {
+      if (typeof this.callback.onCancel === 'function') {
+        this.callback.onCancel(this.model.input)
+      }
+      this.close()
+    },
+
+    onInputPressEnter() {
+      this.onOk()
+    },
+
+    close() {
+      this.visible = this.loading ? this.visible : false
+    },
+
+    forceClose() {
+      this.visible = false
+    },
+
+    showLoading() {
+      this.loading = true
+    },
+    hideLoading() {
+      this.loading = false
+    },
+
+    afterClose(e) {
+      if (typeof this.modalProps.afterClose === 'function') {
+        this.modalProps.afterClose(e)
+      }
+      this.$emit('after-close', e)
+    },
+
+  },
+}
+</script>
+
+<style scoped>
+
+</style>

+ 18 - 0
ant-design-vue-jeecg/src/components/jeecg/JModal/index.js

@@ -0,0 +1,18 @@
+import JModal from './JModal'
+import JPrompt from './JPrompt'
+
+export default {
+  install(Vue) {
+    Vue.component(JModal.name, JModal)
+
+    const JPromptExtend = Vue.extend(JPrompt)
+    Vue.prototype.$JPrompt = function (options = {}) {
+      // 创建prompt实例
+      const vm = new JPromptExtend().$mount()
+      vm.show(options)
+      // 关闭后销毁
+      vm.$on('after-close', () => vm.$destroy())
+      return vm
+    }
+  },
+}

+ 3 - 1
ant-design-vue-jeecg/src/components/jeecg/JPopup.vue

@@ -173,9 +173,11 @@
             let tempDestArr = []
             for(let rw of rows){
               let val = rw[orgFieldsArr[i]]
-              if(!val){
+              // update--begin--autor:liusq-----date:20210713------for:处理val等于0的情况issues/I3ZL4T------
+              if(typeof val=='undefined'|| val==null || val.toString()==""){
                 val = ""
               }
+              // update--end--autor:liusq-----date:20210713------for:处理val等于0的情况issues/I3ZL4T------
               tempDestArr.push(val)
             }
             res[destFieldsArr[i]] = tempDestArr.join(",")

+ 33 - 4
ant-design-vue-jeecg/src/components/jeecg/JSelectMultiple.vue

@@ -1,7 +1,7 @@
 <template>
   <a-select :value="arrayValue" @change="onChange" mode="multiple" :placeholder="placeholder">
     <a-select-option
-      v-for="(item,index) in options"
+      v-for="(item,index) in selectOptions"
       :key="index"
       :getPopupContainer="getParentContainer"
       :value="item.value">
@@ -12,6 +12,8 @@
 
 <script>
   //option {label:,value:}
+  import { getAction } from '@api/manage'
+
   export default {
     name: 'JSelectMultiple',
     props: {
@@ -31,7 +33,8 @@
       },
       options:{
         type: Array,
-        required: true
+        default:()=>[],
+        required: false
       },
       triggerChange:{
         type: Boolean,
@@ -48,12 +51,22 @@
         default:'',
         required:false
       },
+      dictCode:{
+        type:String,
+        required:false
+      },
     },
     data(){
       return {
-        arrayValue:!this.value?[]:this.value.split(this.spliter)
+        arrayValue:!this.value?[]:this.value.split(this.spliter),
+        dictOptions: [],
       }
     },
+    computed:{
+      selectOptions(){
+        return this.dictOptions.length > 0 ? this.dictOptions : this.options
+      },
+    },
     watch:{
       value (val) {
         if(!val){
@@ -63,6 +76,11 @@
         }
       }
     },
+    mounted(){
+      if (this.dictCode) {
+        this.loadDictOptions()
+      }
+    },
     methods:{
       onChange (selectedValue) {
         if(this.triggerChange){
@@ -77,7 +95,18 @@
         }else{
           return document.querySelector(this.popContainer)
         }
-      }
+      },
+      // 根据字典code查询字典项
+      loadDictOptions(){
+        getAction(`/sys/dict/getDictItems/${this.dictCode}`,{}).then(res=>{
+          if (res.success) {
+            this.dictOptions = res.result.map(item => ({value: item.value, label: item.text}))
+          } else {
+            console.error('getDictItems error: : ', res)
+            this.dictOptions = []
+          }
+        })
+      },
     },
 
   }

+ 2 - 1
ant-design-vue-jeecg/src/components/jeecg/JSuperQuery.vue

@@ -412,8 +412,9 @@
         this.queryParamsModel.splice(index, 1)
       },
       handleSelected(node, item) {
-        let { type, options, dictCode, dictTable, dictText, customReturnField, popup } = node.dataRef
+        let { type, dbType, options, dictCode, dictTable, dictText, customReturnField, popup } = node.dataRef
         item['type'] = type
+        item['dbType'] = dbType
         item['options'] = options
         item['dictCode'] = dictCode
         item['dictTable'] = dictTable

+ 9 - 3
ant-design-vue-jeecg/src/components/jeecg/JUpload.vue

@@ -17,7 +17,7 @@
       :headers="headers"
       :data="{'biz':bizPath}"
       :fileList="fileList"
-      :beforeUpload="beforeUpload"
+      :beforeUpload="doBeforeUpload"
       @change="handleChange"
       :disabled="disabled"
       :returnUrl="returnUrl"
@@ -139,6 +139,9 @@
         type: Boolean,
         default: true
       },
+      beforeUpload: {
+        type: Function
+      },
     },
     watch:{
       value:{
@@ -242,7 +245,7 @@
         }
         this.$emit('change', path);
       },
-      beforeUpload(file){
+      doBeforeUpload(file){
         this.uploadGoOn=true
         var fileType = file.type;
         if(this.fileType===FILE_TYPE_IMG){
@@ -252,7 +255,10 @@
             return false;
           }
         }
-        //TODO 扩展功能验证文件大小
+        // 扩展 beforeUpload 验证
+        if (typeof this.beforeUpload === 'function') {
+          return this.beforeUpload(file)
+        }
         return true
       },
       handleChange(info) {

+ 5 - 1
ant-design-vue-jeecg/src/components/jeecg/JVxeTable/components/JVxeTable.js

@@ -668,7 +668,11 @@ export default {
     async loadNewData(dataSource) {
       if (Array.isArray(dataSource)) {
         let {xTable} = this.$refs.vxe.$refs
-        return await xTable.loadData(dataSource)
+        // issues/2784
+        // 先清空所有数据
+        xTable.loadData([])
+        // 再新增
+        return xTable.insertAt(dataSource)
       }
       return []
     },

+ 12 - 3
ant-design-vue-jeecg/src/components/jeecg/JVxeTable/components/cells/JVxeUploadCell.vue

@@ -14,7 +14,7 @@
           <a-tooltip v-else-if="file.status === 'done'" title="上传完成">
             <a-icon type="check-circle" style="color:#00DB00;"/>
           </a-tooltip>
-          <a-tooltip v-else title="上传失败">
+          <a-tooltip v-else :title="file.message||'上传失败'">
             <a-icon type="exclamation-circle" style="color:red;"/>
           </a-tooltip>
         </template>
@@ -118,8 +118,17 @@
           value['responseName'] = file.response[col.responseName]
         }
         if (file.status === 'done') {
-          value['path'] = file.response[col.responseName]
-          this.handleChangeCommon(value)
+          if (typeof file.response.success === 'boolean') {
+            if (file.response.success) {
+              value['path'] = file.response[col.responseName]
+            } else {
+              value['status'] = 'error'
+              value['message'] = file.response.message || '未知错误'
+            }
+          } else {
+            // 考虑到如果设置action上传路径为非jeecg-boot后台,可能不会返回 success 属性的情况,就默认为成功
+            value['path'] = file.response[col.responseName]
+          }
         } else if (file.status === 'error') {
           value['message'] = file.response.message || '未知错误'
         }

+ 17 - 3
ant-design-vue-jeecg/src/components/jeecg/index.js

@@ -26,19 +26,24 @@ import JSlider from './JSlider.vue'
 import JSwitch from './JSwitch.vue'
 import JTime from './JTime.vue'
 import JTreeTable from './JTreeTable.vue'
-import JEasyCron from "@/components/jeecg/JEasyCron";
-
+import JEasyCron from '@/components/jeecg/JEasyCron'
 //jeecgbiz
 import JSelectDepart from '../jeecgbiz/JSelectDepart.vue'
 import JSelectMultiUser from '../jeecgbiz/JSelectMultiUser.vue'
 import JSelectPosition from '../jeecgbiz/JSelectPosition.vue'
 import JSelectRole from '../jeecgbiz/JSelectRole.vue'
 import JSelectUserByDep from '../jeecgbiz/JSelectUserByDep.vue'
+//引入需要全局注册的js函数和变量
+import { Modal, notification,message } from 'ant-design-vue'
+import lodash_object from 'lodash'
+import debounce from 'lodash/debounce'
+import pick from 'lodash.pick'
+import data from 'china-area-data'
 
 export default {
   install(Vue) {
+    Vue.use(JModal)
     Vue.component('JMarkdownEditor', JMarkdownEditor)
-    Vue.component(JModal.name, JModal)
     Vue.component('JPopupOnlReport', JPopupOnlReport)
     Vue.component('JFilePop', JFilePop)
     Vue.component('JInputPop', JInputPop)
@@ -73,5 +78,14 @@ export default {
     Vue.component('JSelectRole', JSelectRole)
     Vue.component('JSelectUserByDep', JSelectUserByDep)
     Vue.component(JEasyCron.name, JEasyCron)
+
+    //注册全局js函数和变量
+    Vue.prototype.$Jnotification = notification
+    Vue.prototype.$Jmodal = Modal
+    Vue.prototype.$Jmessage = message
+    Vue.prototype.$Jlodash = lodash_object
+    Vue.prototype.$Jdebounce= debounce
+    Vue.prototype.$Jpick = pick
+    Vue.prototype.$Jpcaa = data
   }
 }

+ 6 - 2
ant-design-vue-jeecg/src/components/jeecg/modal/JPopupOnlReport.vue

@@ -137,8 +137,12 @@
       param:{
         deep:true,
         handler(){
-          this.dynamicParamHandler()
-          this.loadData();
+          // update--begin--autor:liusq-----date:20210706------for:JPopup组件在modal中使用报错#2729------
+          if(this.visible){
+            this.dynamicParamHandler()
+            this.loadData();
+          }
+          // update--begin--autor:liusq-----date:20210706------for:JPopup组件在modal中使用报错#2729------
         },
       },
       sorter: {

+ 58 - 36
ant-design-vue-jeecg/src/components/jeecgbiz/JSelectDepart.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="components-input-demo-presuffix">
     <!---->
-    <a-input @click="openModal" placeholder="请点击选择部门" v-model="departNames" readOnly :disabled="disabled">
+    <a-input @click="openModal" placeholder="请点击选择部门" v-model="textVals" readOnly :disabled="disabled">
       <a-icon slot="prefix" type="cluster" title="部门选择控件"/>
-      <a-icon v-if="departIds" slot="suffix" type="close-circle" @click="handleEmpty" title="清空"/>
+      <a-icon v-if="storeVals" slot="suffix" type="close-circle" @click="handleEmpty" title="清空"/>
     </a-input>
 
     <j-select-depart-modal
@@ -11,7 +11,10 @@
       :modal-width="modalWidth"
       :multi="multi"
       :rootOpened="rootOpened"
-      :depart-id="departIds"
+      :depart-id="value"
+      :store="storeField"
+      :text="textField"
+      :treeOpera="treeOpera"
       @ok="handleOK"
       @initComp="initComp"/>
   </div>
@@ -19,6 +22,7 @@
 
 <script>
   import JSelectDepartModal from './modal/JSelectDepartModal'
+  import { underLinetoHump } from '@/components/_util/StringUtil'
   export default {
     name: 'JSelectDepart',
     components:{
@@ -52,56 +56,70 @@
       // 自定义返回字段,默认返回 id
       customReturnField: {
         type: String,
-        default: 'id'
+        default: ''
       },
       backDepart: {
         type: Boolean,
         default: false,
         required: false
+      },
+      // 存储字段 [key field]
+      store: {
+        type: String,
+        default: 'id',
+        required: false
+      },
+      // 显示字段 [label field]
+      text: {
+        type: String,
+        default: 'departName',
+        required: false
+      },
+      treeOpera: {
+        type: Boolean,
+        default: false,
+        required: false
       }
+      
     },
     data(){
       return {
         visible:false,
         confirmLoading:false,
-        departNames:"",
-        departIds:''
+        storeVals: '', //[key values]
+        textVals: '' //[label values]
+      }
+    },
+    computed:{
+      storeField(){
+        let field = this.customReturnField
+        if(!field){
+          field = this.store;
+        }
+        return underLinetoHump(field)
+      },
+      textField(){
+        return underLinetoHump(this.text)
       }
     },
     mounted(){
-      this.departIds = this.value
+      this.storeVals = this.value
     },
     watch:{
       value(val){
-        //update-begin-author:wangshuai date:20201124 for:组件 JSelectDepart.vue不是默认id时新内容编辑问题 gitee I247X2
-        // if (this.customReturnField === 'id') {
-          this.departIds = val
-        // }
-        //update-end-author:wangshuai date:20201124 for:组件 JSelectDepart.vue不是默认id时新内容编辑问题 gitee I247X2
+        this.storeVals = val
       }
     },
     methods:{
-      initComp(departNames){
-        this.departNames = departNames
-        //update-begin-author:lvdandan date:20200513 for:TESTA-438 部门选择组件自定义返回值,数据无法回填
-        //TODO 当返回字段为部门名称时会有问题,因为部门名称不唯一
-        //返回字段不为id时,根据返回字段获取id
-        if(this.customReturnField !== 'id' && this.value){
-          const dataList = this.$refs.innerDepartSelectModal.dataList;
-          console.log('this.value',this.value)
-          this.departIds = this.value.split(',').map(item => {
-            const data = dataList.filter(d=>d[this.customReturnField] === item)
-            return data.length > 0 ? data[0].id : ''
-          }).join(',')
-        }
-        //update-end-author:lvdandan date:20200513 for:TESTA-438 部门选择组件自定义返回值,数据无法回填
+      initComp(textVals){
+        this.textVals = textVals
       },
       //返回选中的部门信息
       backDeparInfo(){
         if(this.backDepart===true){
           if(this.departIds && this.departIds.length>0){
-            let arr1 = this.departIds.split(',')
-            let arr2 = this.departNames.split(',')
+            let arr1 = this.storeVals.split(',')
+            let arr2 = this.textVals.split(',')
             let info = []
             for(let i=0;i<arr1.length;i++){
               info.push({
@@ -116,17 +134,21 @@
       openModal(){
         this.$refs.innerDepartSelectModal.show()
       },
-      handleOK(rows, idstr) {
-        let value = ''
+      handleOK(rows) {
         if (!rows && rows.length <= 0) {
-          this.departNames = ''
-          this.departIds = ''
+          this.textVals = ''
+          this.storeVals = ''
         } else {
-          value = rows.map(row => row[this.customReturnField]).join(',')
-          this.departNames = rows.map(row => row['departName']).join(',')
-          this.departIds = idstr
+          let arr1 = []
+          let arr2 = []
+          for(let dep of rows){
+            arr1.push(dep[this.storeField])
+            arr2.push(dep[this.textField])
+          }
+          this.storeVals = arr1.join(',')
+          this.textVals = arr2.join(',')
         }
-        this.$emit("change", value)
+        this.$emit("change", this.storeVals)
         this.backDeparInfo()
       },
       getDepartNames(){

+ 54 - 20
ant-design-vue-jeecg/src/components/jeecgbiz/JSelectUserByDep.vue

@@ -1,19 +1,28 @@
 <template>
   <div>
     <a-input-search
-      v-model="userNames"
+      v-model="textVals"
       placeholder="请先选择用户"
       readOnly
       unselectable="on"
       @search="onSearchDepUser">
       <a-button slot="enterButton" :disabled="disabled">选择用户</a-button>
     </a-input-search>
-    <j-select-user-by-dep-modal ref="selectModal" :modal-width="modalWidth" :multi="multi" @ok="selectOK" :user-ids="value" @initComp="initComp"/>
+    <j-select-user-by-dep-modal
+      ref="selectModal"
+      :modal-width="modalWidth"
+      :multi="multi"
+      @ok="selectOK"
+      :user-ids="value"
+      :store="storeField"
+      :text="textField"
+      @initComp="initComp"/>
   </div>
 </template>
 
 <script>
   import JSelectUserByDepModal from './modal/JSelectUserByDepModal'
+  import { underLinetoHump } from '@/components/_util/StringUtil'
 
   export default {
     name: 'JSelectUserByDep',
@@ -42,20 +51,44 @@
         type: Boolean,
         default: false,
         required: false
+      },
+      // 存储字段 [key field]
+      store: {
+        type: String,
+        default: 'username',
+        required: false
+      },
+      // 显示字段 [label field]
+      text: {
+        type: String,
+        default: 'realname',
+        required: false
       }
     },
     data() {
       return {
-        userIds: "",
-        userNames: ""
+        storeVals: '', //[key values]
+        textVals: '' //[label values]
+      }
+    },
+    computed:{
+      storeField(){
+        let field = this.customReturnField
+        if(!field){
+          field = this.store;
+        }
+        return underLinetoHump(field)
+      },
+      textField(){
+        return underLinetoHump(this.text)
       }
     },
     mounted() {
-      this.userIds = this.value
+      this.storeVals = this.value
     },
     watch: {
       value(val) {
-        this.userIds = val
+        this.storeVals = val
       }
     },
     model: {
@@ -63,15 +96,15 @@
       event: 'change'
     },
     methods: {
-      initComp(userNames) {
-        this.userNames = userNames
+      initComp(textVals) {
+        this.textVals = textVals
       },
       //返回选中的用户信息
       backDeparInfo(){
         if(this.backUser===true){
-          if(this.userIds && this.userIds.length>0){
-            let arr1 = this.userIds.split(',')
-            let arr2 = this.userNames.split(',')
+          if(this.storeVals && this.storeVals.length>0){
+            let arr1 = this.storeVals.split(',')
+            let arr2 = this.textVals.split(',')
             let info = []
             for(let i=0;i<arr1.length;i++){
               info.push({
@@ -86,21 +119,22 @@
       onSearchDepUser() {
         this.$refs.selectModal.showModal()
       },
-      selectOK(rows, idstr) {
+      selectOK(rows) {
         console.log("当前选中用户", rows)
-        console.log("当前选中用户ID", idstr)
         if (!rows) {
-          this.userNames = ''
-          this.userIds = ''
+          this.storeVals = ''
+          this.textVals = ''
         } else {
-          let temp = ''
+          let temp1 = []
+          let temp2 = []
           for (let item of rows) {
-            temp += ',' + item.realname
+            temp1.push(item[this.storeField])
+            temp2.push(item[this.textField])
           }
-          this.userNames = temp.substring(1)
-          this.userIds = idstr
+          this.storeVals = temp1.join(',')
+          this.textVals = temp2.join(',')
         }
-        this.$emit("change", this.userIds)
+        this.$emit("change", this.storeVals)
       }
     }
   }

+ 93 - 34
ant-design-vue-jeecg/src/components/jeecgbiz/modal/JSelectDepartModal.vue

@@ -6,6 +6,7 @@
     :confirmLoading="confirmLoading"
     @ok="handleSubmit"
     @cancel="handleCancel"
+    @update:fullscreen="isFullscreen"
     wrapClassName="j-depart-select-modal"
     switchFullscreen
     cancelText="关闭">
@@ -13,9 +14,9 @@
       <a-input-search style="margin-bottom: 1px" placeholder="请输入部门名称按回车进行搜索" @search="onSearch" />
       <a-tree
         checkable
-        class="my-dept-select-tree"
+        :class="treeScreenClass"
         :treeData="treeData"
-        :checkStrictly="true"
+        :checkStrictly="checkStrictly"
         @check="onCheck"
         @select="onSelect"
         @expand="onExpand"
@@ -32,8 +33,23 @@
           <span v-else>{{title}}</span>
         </template>
       </a-tree>
-
     </a-spin>
+    <!--底部父子关联操作和确认取消按钮-->
+    <template slot="footer" v-if="treeOpera && multi">
+      <div class="drawer-bootom-button">
+        <a-dropdown style="float: left" :trigger="['click']" placement="topCenter">
+          <a-menu slot="overlay">
+            <a-menu-item key="1" @click="switchCheckStrictly(1)">父子关联</a-menu-item>
+            <a-menu-item key="2" @click="switchCheckStrictly(2)">取消关联</a-menu-item>
+          </a-menu>
+          <a-button>
+            树操作 <a-icon type="up" />
+          </a-button>
+        </a-dropdown>
+        <a-button @click="handleCancel" type="primary" style="margin-right: 0.8rem">关闭</a-button>
+        <a-button @click="handleSubmit" type="primary" >确认</a-button>
+      </div>
+    </template>
   </j-modal>
 </template>
 
@@ -41,7 +57,7 @@
   import { queryDepartTreeList } from '@/api/api'
   export default {
     name: 'JSelectDepartModal',
-    props:['modalWidth','multi','rootOpened','departId'],
+    props:['modalWidth','multi','rootOpened','departId', 'store', 'text','treeOpera'],
     data(){
       return {
         visible:false,
@@ -52,7 +68,9 @@
         dataList:[],
         checkedKeys:[],
         checkedRows:[],
-        searchValue:""
+        searchValue:"",
+        checkStrictly: true,
+        fullscreen:false
       }
     },
     created(){
@@ -64,15 +82,18 @@
       },
       visible: {
         handler() {
-          if (this.departId) {
-            this.checkedKeys = this.departId.split(",");
-            // console.log('this.departId', this.departId)
-          } else {
-            this.checkedKeys = [];
-          }
+          this.initDepartComponent(true)
         }
       }
     },
+    computed:{
+      treeScreenClass() {
+        return {
+          'my-dept-select-tree': true,
+          'fullscreen': this.fullscreen,
+        }
+      },
+    },
     methods:{
       show(){
         this.visible=true
@@ -80,6 +101,7 @@
         this.checkedKeys=[]
       },
       loadDepart(){
+        // 这个方法是找到所有的部门信息
         queryDepartTreeList().then(res=>{
           if(res.success){
             let arr = [...res.result]
@@ -92,20 +114,23 @@
           }
         })
       },
-      initDepartComponent(){
-        let names = ''
+      initDepartComponent(flag){
+        let arr = []
+        //该方法两个地方用 1.visible改变事件重新设置选中项 2.组件编辑页面回显
+        let fieldName = flag==true?'key':this.text
         if(this.departId){
-          let currDepartId = this.departId
+          let arr2 = this.departId.split(',')
           for(let item of this.dataList){
-            if(currDepartId.indexOf(item.key)>=0){
-              names+=","+item.title
+            if(arr2.indexOf(item[this.store])>=0){
+              arr.push(item[fieldName])
             }
           }
-          if(names){
-            names = names.substring(1)
-          }
         }
-        this.$emit("initComp",names)
+        if(flag==true){
+          this.checkedKeys = [...arr]
+        }else{
+          this.$emit("initComp", arr.join(','))
+        }
       },
       reWriterWithSlot(arr){
         for(let item of arr){
@@ -129,8 +154,11 @@
             }
           }
           this.expandedKeys=[...keys]
+          //全部keys
+          //this.allTreeKeys = [...keys]
         }else{
           this.expandedKeys=[]
+          //this.allTreeKeys = []
         }
       },
       onCheck (checkedKeys,info) {
@@ -139,25 +167,32 @@
           this.checkedKeys = [...arr]
           this.checkedRows = (this.checkedKeys.length === 0) ? [] : [info.node.dataRef]
         }else{
-          this.checkedKeys = checkedKeys.checked
+          if(this.checkStrictly){
+            this.checkedKeys = checkedKeys.checked
+          }else{
+            this.checkedKeys = checkedKeys
+          }
           this.checkedRows = this.getCheckedRows(this.checkedKeys)
         }
       },
       onSelect(selectedKeys,info) {
-        let keys = []
-        keys.push(selectedKeys[0])
-        if(!this.checkedKeys || this.checkedKeys.length===0 || !this.multi){
-          this.checkedKeys = [...keys]
-          this.checkedRows=[info.node.dataRef]
-        }else{
-          let currKey = info.node.dataRef.key
-          if(this.checkedKeys.indexOf(currKey)>=0){
-            this.checkedKeys = this.checkedKeys.filter(item=> item !==currKey)
+        //取消关联的情况下才走onSelect的逻辑
+        if(this.checkStrictly){
+          let keys = []
+          keys.push(selectedKeys[0])
+          if(!this.checkedKeys || this.checkedKeys.length===0 || !this.multi){
+            this.checkedKeys = [...keys]
+            this.checkedRows=[info.node.dataRef]
           }else{
-            this.checkedKeys.push(...keys)
+            let currKey = info.node.dataRef.key
+            if(this.checkedKeys.indexOf(currKey)>=0){
+              this.checkedKeys = this.checkedKeys.filter(item=> item !==currKey)
+            }else{
+              this.checkedKeys.push(...keys)
+            }
           }
+          this.checkedRows = this.getCheckedRows(this.checkedKeys)
         }
-        this.checkedRows = this.getCheckedRows(this.checkedKeys)
       },
       onExpand (expandedKeys) {
         this.expandedKeys = expandedKeys
@@ -235,6 +270,16 @@
           }
         }
         return rows
+      },
+      switchCheckStrictly (v) {
+        if(v==1){
+          this.checkStrictly = false
+        }else if(v==2){
+          this.checkStrictly = true
+        }
+      },
+      isFullscreen(val){
+        this.fullscreen=val
       }
     }
   }
@@ -244,8 +289,22 @@
 <style lang="less" scoped>
   // 限制部门选择树高度,避免部门太多时点击确定不便
   .my-dept-select-tree{
-    height: 350px;
+    height:350px;
+    
+    &.fullscreen{
+      height: calc(100vh - 250px);
+    }
     overflow-y: scroll;
   }
-
+  .drawer-bootom-button {
+    position: absolute;
+    bottom: 0;
+    width: 100%;
+    border-top: 1px solid #e8e8e8;
+    padding: 10px 16px;
+    text-align: right;
+    left: 0;
+    background: #fff;
+    border-radius: 0 0 2px 2px;
+  }
 </style>

+ 16 - 16
ant-design-vue-jeecg/src/components/jeecgbiz/modal/JSelectUserByDepModal.vue

@@ -63,7 +63,7 @@
   export default {
     name: 'JSelectUserByDepModal',
     components: {},
-    props: ['modalWidth', 'multi', 'userIds'],
+    props: ['modalWidth', 'multi', 'userIds', 'store', 'text'],
     data() {
       return {
         queryParam: {
@@ -159,27 +159,27 @@
         if (this.userIds) {
           // 这里最后加一个 , 的原因是因为无论如何都要使用 in 查询,防止后台进行了模糊匹配,导致查询结果不准确
           let values = this.userIds.split(',') + ','
-          getUserList({
-            username: values,
-            pageNo: 1,
-            pageSize: values.length
-          }).then((res) => {
-            if (res.success) {
-              this.selectionRows = []
-              let selectedRowKeys = []
-              let realNames = []
-              res.result.records.forEach(user => {
-                realNames.push(user['realname'])
+          let param = {[this.store]: values}
+          getAction('/sys/user/getMultiUser', param).then((list)=>{
+            this.selectionRows = []
+            let selectedRowKeys = []
+            let textArray = []
+            if(list && list.length>0){
+              for(let user of list){
+                textArray.push(user[this.text])
                 selectedRowKeys.push(user['id'])
                 this.selectionRows.push(user)
-              })
-              this.selectedRowKeys = selectedRowKeys
-              this.$emit('initComp', realNames.join(','))
+              }
             }
+            this.selectedRowKeys = selectedRowKeys
+            this.$emit('initComp', textArray.join(','))
           })
+
         } else {
           // JSelectUserByDep组件bug issues/I16634
           this.$emit('initComp', '')
+          // 前端用户选择单选无法置空的问题 #2610
+          this.selectedRowKeys = []
         }
       },
       async loadData(arg) {
@@ -254,7 +254,7 @@
       handleSubmit() {
         let that = this;
         this.getSelectUserRows();
-        that.$emit('ok', that.selectUserRows, that.selectUserIds);
+        that.$emit('ok', that.selectUserRows);
         that.searchReset(0)
         that.close();
       },

+ 7 - 2
ant-design-vue-jeecg/src/components/tools/Logo.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="logo">
-    <router-link :to="{name:'dashboard'}">
+    <router-link :to="routerLinkTo">
 
       <!-- update-begin- author:sunjianlei --- date:20190814 --- for: logo颜色根据主题颜色变化 -->
       <img v-if="navTheme === 'dark'" src="~@/assets/logo-white.png" alt="logo">
@@ -28,7 +28,12 @@
         type: Boolean,
         default: true,
         required: false
-      }
+      },
+      // 点击Logo跳转地址
+      routerLinkTo: {
+        type: Object,
+        default: () => ({name: 'dashboard'}),
+      },
     }
   }
 </script>

+ 15 - 0
ant-design-vue-jeecg/src/config/router.config.js

@@ -347,6 +347,21 @@ export const constantRouterMap = [
   //   ]
   // },
 
+  {
+    // OAuth2 APP页面路由
+    path: '/oauth2-app',
+    component: BlankLayout,
+    redirect: '/oauth2-app/login',
+    children: [
+      {
+        // OAuth2 登录路由
+        path: 'login',
+        name: 'login',
+        component: () => import(/* webpackChunkName: "oauth2-app.login" */ '@/views/user/oauth2/OAuth2Login')
+      },
+    ]
+  },
+
   {
     path: '/test',
     component: BlankLayout,

+ 7 - 5
ant-design-vue-jeecg/src/mixins/JeecgListMixin.js

@@ -8,7 +8,6 @@ import { deleteAction, getAction,downFile,getFileAccessHttpUrl } from '@/api/man
 import Vue from 'vue'
 import { ACCESS_TOKEN, TENANT_ID } from "@/store/mutation-types"
 import store from '@/store'
-import {Modal} from 'ant-design-vue'
 
 export const JeecgListMixin = {
   data(){
@@ -94,11 +93,11 @@ export const JeecgListMixin = {
             this.ipagination.total = 0;
           }
           //update-end---author:zhangyafei    Date:20201118  for:适配不分页的数据列表------------
-        }
-        if(res.code===510){
+        }else{
           this.$message.warning(res.message)
         }
-        this.loading = false;
+      }).finally(() => {
+        this.loading = false
       })
     },
     initDictConfig(){
@@ -296,10 +295,12 @@ export const JeecgListMixin = {
     },
     /* 导入 */
     handleImportExcel(info){
+      this.loading = true;
       if (info.file.status !== 'uploading') {
         console.log(info.file, info.fileList);
       }
       if (info.file.status === 'done') {
+        this.loading = false;
         if (info.file.response.success) {
           // this.$message.success(`${info.file.name} 文件上传成功`);
           if (info.file.response.code === 201) {
@@ -321,11 +322,12 @@ export const JeecgListMixin = {
           this.$message.error(`${info.file.name} ${info.file.response.message}.`);
         }
       } else if (info.file.status === 'error') {
+        this.loading = false;
         if (info.file.response.status === 500) {
           let data = info.file.response
           const token = Vue.ls.get(ACCESS_TOKEN)
           if (token && data.message.includes("Token失效")) {
-            Modal.error({
+            this.$error({
               title: '登录已过期',
               content: '很抱歉,登录已过期,请重新登录',
               okText: '重新登录',

+ 4 - 2
ant-design-vue-jeecg/src/mixins/OnlineCommonUtil.js

@@ -1,14 +1,16 @@
 import { formatDate } from '@/utils/util'
 import Area from '@/components/_util/Area'
+import { postAction } from '@/api/manage'
 
 const onlUtil = {
   data(){
     return {
-      mixin_pca:''
+      mixin_pca:'',
+      flowCodePre: 'onl_'
     }
   },
   created(){
-    this.mixin_pca = new Area()
+    this.mixin_pca = new Area(this.$Jpcaa)
   },
   methods:{
     simpleDateFormat(millisecond, format){

+ 15 - 6
ant-design-vue-jeecg/src/permission.js

@@ -4,19 +4,20 @@ import store from './store'
 import NProgress from 'nprogress' // progress bar
 import 'nprogress/nprogress.css' // progress bar style
 import notification from 'ant-design-vue/es/notification'
-import { ACCESS_TOKEN,INDEX_MAIN_PAGE_PATH } from '@/store/mutation-types'
-import { generateIndexRouter } from "@/utils/util"
+import { ACCESS_TOKEN,INDEX_MAIN_PAGE_PATH, OAUTH2_LOGIN_PAGE_PATH } from '@/store/mutation-types'
+import { generateIndexRouter, isOAuth2AppEnv } from '@/utils/util'
 
 NProgress.configure({ showSpinner: false }) // NProgress Configuration
 
 const whiteList = ['/user/login', '/user/register', '/user/register-result','/user/alteration'] // no redirect whitelist
+whiteList.push(OAUTH2_LOGIN_PAGE_PATH)
 
 router.beforeEach((to, from, next) => {
   NProgress.start() // start progress bar
 
   if (Vue.ls.get(ACCESS_TOKEN)) {
     /* has token */
-    if (to.path === '/user/login') {
+    if (to.path === '/user/login' || to.path === OAUTH2_LOGIN_PAGE_PATH) {
       next({ path: INDEX_MAIN_PAGE_PATH })
       NProgress.done()
     } else {
@@ -59,10 +60,18 @@ router.beforeEach((to, from, next) => {
     }
   } else {
     if (whiteList.indexOf(to.path) !== -1) {
-      // 在免登录白名单,直接进入
-      next()
+      // 在免登录白名单,如果进入的页面是login页面并且当前是OAuth2app环境,就进入OAuth2登录页面
+      if (to.path === '/user/login' && isOAuth2AppEnv()) {
+        next({path: OAUTH2_LOGIN_PAGE_PATH})
+      } else {
+        // 在免登录白名单,直接进入
+        next()
+      }
+      NProgress.done()
     } else {
-      next({ path: '/user/login', query: { redirect: to.fullPath } })
+      // 如果当前是在OAuth2APP环境,就跳转到OAuth2登录页面
+      let path = isOAuth2AppEnv() ? OAUTH2_LOGIN_PAGE_PATH : '/user/login'
+      next({ path: path, query: { redirect: to.fullPath } })
       NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
     }
   }

+ 0 - 4
ant-design-vue-jeecg/src/store/index.js

@@ -4,8 +4,6 @@ import Vuex from 'vuex'
 import app from './modules/app'
 import user from './modules/user'
 import permission from './modules/permission'
-import enhance from './modules/enhance'
-import online from './modules/online'
 import getters from './getters'
 
 Vue.use(Vuex)
@@ -15,8 +13,6 @@ export default new Vuex.Store({
     app,
     user,
     permission,
-    enhance,
-    online
   },
   state: {
 

+ 1 - 0
ant-design-vue-jeecg/src/store/modules/user.js

@@ -159,6 +159,7 @@ const user = {
         Vue.ls.remove(USER_NAME)
         Vue.ls.remove(UI_CACHE_DB_DICT_DATA)
         Vue.ls.remove(CACHE_INCLUDED_ROUTES)
+        Vue.ls.remove(TENANT_ID)
         //console.log('logoutToken: '+ logoutToken)
         logout(logoutToken).then(() => {
           if (process.env.VUE_APP_SSO == 'true') {

+ 1 - 0
ant-design-vue-jeecg/src/store/mutation-types.js

@@ -17,6 +17,7 @@ export const ENCRYPTED_STRING = 'ENCRYPTED_STRING'
 export const ENHANCE_PRE = 'enhance_'
 export const UI_CACHE_DB_DICT_DATA = 'UI_CACHE_DB_DICT_DATA'
 export const INDEX_MAIN_PAGE_PATH = '/dashboard/analysis'
+export const OAUTH2_LOGIN_PAGE_PATH = '/oauth2-app/login'
 export const TENANT_ID = 'TENANT_ID'
 export const ONL_AUTH_FIELDS = 'ONL_AUTH_FIELDS'
 //路由缓存问题,关闭了tab页时再打开就不刷新 #842

+ 2 - 0
ant-design-vue-jeecg/src/utils/encryption/signMd5Utils.js

@@ -49,11 +49,13 @@ export default class signMd5Utils {
       result = {};
 
     // 获取URL上最后带逗号的参数变量 sys/dict/getDictItems/sys_user,realname,username
+    //【这边条件没有encode】带条件参数例子:/sys/dict/getDictItems/sys_user,realname,id,username!='admin'%20order%20by%20create_time
     let lastpathVariable = url.substring(url.lastIndexOf('/') + 1);
     if(lastpathVariable.includes(",")){
       if(lastpathVariable.includes("?")){
         lastpathVariable = lastpathVariable.substring(0, lastpathVariable.indexOf("?"));
       }
+      //解决Sign 签名校验失败 #2728
       result["x-path-variable"] = decodeURI(lastpathVariable);
     }
     if (urlArray && urlArray[1]) {

+ 42 - 25
ant-design-vue-jeecg/src/utils/request.js

@@ -2,7 +2,7 @@ import Vue from 'vue'
 import axios from 'axios'
 import store from '@/store'
 import { VueAxios } from './axios'
-import {Modal, notification} from 'ant-design-vue'
+import router from '@/router/index'
 import { ACCESS_TOKEN, TENANT_ID } from "@/store/mutation-types"
 
 /**
@@ -28,7 +28,7 @@ const err = (error) => {
     console.log("------异常响应------",error.response.status)
     switch (error.response.status) {
       case 403:
-        notification.error({ message: '系统提示', description: '拒绝访问',duration: 4})
+        Vue.prototype.$Jnotification.error({ message: '系统提示', description: '拒绝访问',duration: 4})
         break
       case 500:
         console.log("------error.response------",error.response)
@@ -39,40 +39,43 @@ const err = (error) => {
           break;
         }
         // update-end- --- author:liusq ------ date:20200910 ---- for:处理Blob情况----
-        //notification.error({ message: '系统提示', description:'Token失效,请重新登录!',duration: 4})
         if(token && data.message.includes("Token失效")){
           // update-begin- --- author:scott ------ date:20190225 ---- for:Token失效采用弹框模式,不直接跳转----
-          Modal.error({
-            title: '登录已过期',
-            content: '很抱歉,登录已过期,请重新登录',
-            okText: '重新登录',
-            mask: false,
-            onOk: () => {
-              store.dispatch('Logout').then(() => {
-                Vue.ls.remove(ACCESS_TOKEN)
-                try {
-                  let path = window.document.location.pathname
-                  console.log("location pathname -> "+path)
-                  if(path!="/" && path.indexOf('/user/login')==-1){
+          if (/wxwork|dingtalk/i.test(navigator.userAgent)) {
+            Vue.prototype.$Jmessage.loading('登录已过期,正在重新登陆', 0)
+          } else {
+            Vue.prototype.$Jmodal.error({
+              title: '登录已过期',
+              content: '很抱歉,登录已过期,请重新登录',
+              okText: '重新登录',
+              mask: false,
+              onOk: () => {
+                store.dispatch('Logout').then(() => {
+                  Vue.ls.remove(ACCESS_TOKEN)
+                  try {
+                    let path = window.document.location.pathname
+                    console.log('location pathname -> ' + path)
+                    if (path != '/' && path.indexOf('/user/login') == -1) {
+                      window.location.reload()
+                    }
+                  } catch (e) {
                     window.location.reload()
                   }
-                }catch (e) {
-                  window.location.reload()
-                }
-              })
-            }
-          })
+                })
+              }
+            })
+          }
           // update-end- --- author:scott ------ date:20190225 ---- for:Token失效采用弹框模式,不直接跳转----
         }
         break
       case 404:
-          notification.error({ message: '系统提示', description:'很抱歉,资源未找到!',duration: 4})
+          Vue.prototype.$Jnotification.error({ message: '系统提示', description:'很抱歉,资源未找到!',duration: 4})
         break
       case 504:
-        notification.error({ message: '系统提示', description: '网络超时'})
+        Vue.prototype.$Jnotification.error({ message: '系统提示', description: '网络超时'})
         break
       case 401:
-        notification.error({ message: '系统提示', description:'未授权,请重新登录',duration: 4})
+        Vue.prototype.$Jnotification.error({ message: '系统提示', description:'未授权,请重新登录',duration: 4})
         if (token) {
           store.dispatch('Logout').then(() => {
             setTimeout(() => {
@@ -82,13 +85,19 @@ const err = (error) => {
         }
         break
       default:
-        notification.error({
+        Vue.prototype.$Jnotification.error({
           message: '系统提示',
           description: data.message,
           duration: 4
         })
         break
     }
+  } else if (error.message) {
+    if (error.message.includes('timeout')) {
+      Vue.prototype.$Jnotification.error({message: '系统提示', description: '网络超时'})
+    } else {
+      Vue.prototype.$Jnotification.error({message: '系统提示', description: error.message})
+    }
   }
   return Promise.reject(error)
 };
@@ -99,6 +108,14 @@ service.interceptors.request.use(config => {
   if (token) {
     config.headers[ 'X-Access-Token' ] = token // 让每个请求携带自定义 token 请根据实际情况自行修改
   }
+
+  // update-begin--author:sunjianlei---date:20200723---for 如果当前在low-app环境,并且携带了appId,就向Header里传递appId
+  const $route = router.currentRoute
+  if ($route && $route.name && $route.name.startsWith('low-app') && $route.params.appId) {
+    config.headers['X-Low-App-ID'] = $route.params.appId
+  }
+  // update-end--author:sunjianlei---date:20200723---for 如果当前在low-app环境,并且携带了appId,就向Header里传递appId
+
   //update-begin-author:taoyan date:2020707 for:多租户
   let tenantid = Vue.ls.get(TENANT_ID)
   if (!tenantid) {

+ 32 - 1
ant-design-vue-jeecg/src/utils/util.js

@@ -1,5 +1,7 @@
+import Vue from 'vue'
 import * as api from '@/api/api'
 import { isURL } from '@/utils/validate'
+import { ACCESS_TOKEN } from '@/store/mutation-types'
 import onlineCommons from '@jeecg/antd-online-mini'
 
 export function timeFix() {
@@ -145,6 +147,7 @@ function  generateChildRouters (data) {
       component: componentPath,
       //component: resolve => require(['@/' + component+'.vue'], resolve),
       hidden:item.hidden,
+      //component:()=> import(`@/views/${item.component}.vue`),
       meta: {
         title:item.meta.title ,
         icon: item.meta.icon,
@@ -559,4 +562,32 @@ export function removeArrayElement(array, prod, value) {
   if(index>=0){
     array.splice(index, 1);
   }
-}
+}
+
+/** 判断是否是OAuth2APP环境 */
+export function isOAuth2AppEnv() {
+  return /wxwork|dingtalk/i.test(navigator.userAgent)
+}
+
+/**
+ * 获取积木报表打印地址
+ * @param url
+ * @param id
+ * @param open 是否自动打开
+ * @returns {*}
+ */
+export function getReportPrintUrl(url, id, open) {
+  // URL支持{{ window.xxx }}占位符变量
+  url = url.replace(/{{([^}]+)?}}/g, (s1, s2) => eval(s2))
+  if (url.includes('?')) {
+    url += '&'
+  } else {
+    url += '?'
+  }
+  url += `id=${id}`
+  url += `&token=${Vue.ls.get(ACCESS_TOKEN)}`
+  if (open) {
+    window.open(url)
+  }
+  return url
+}

+ 1 - 1
ant-design-vue-jeecg/src/views/account/settings/AvatarModal.vue

@@ -1,5 +1,5 @@
 <template>
-  <a-modal :visible="visible" title="修改头像" :maskClosable="false" :confirmLoading="confirmLoading" :width="800">
+  <a-modal :visible="visible" title="修改头像" :maskClosable="false" :confirmLoading="confirmLoading" :width="800" @cancel="cancelHandel">
     <a-row>
       <a-col :xs="24" :md="12" :style="{height: '350px'}">
         <vue-cropper

+ 3 - 2
ant-design-vue-jeecg/src/views/jeecg/SelectDemo.vue

@@ -305,7 +305,7 @@
         <a-row :gutter="24">
           <a-col :span="12">
             <a-form-model-item label="cron表达式" prop="cronExpression">
-              <j-cron v-model="formData.cronExpression"></j-cron>
+               <j-easy-cron v-model="formData.cronExpression"></j-easy-cron>
             </a-form-model-item>
           </a-col>
         </a-row>
@@ -457,6 +457,7 @@
   import JSelectMultiple from '@/components/jeecg/JSelectMultiple'
   import JTreeDict from "../../components/jeecg/JTreeDict.vue";
   import JCron from "@/components/jeecg/JCron.vue";
+  import JEasyCron from "@/components/jeecg/JEasyCron";
   import JTreeSelect from '@/components/jeecg/JTreeSelect'
   import JSuperQuery from '@/components/jeecg/JSuperQuery'
   import JUpload from '@/components/jeecg/JUpload'
@@ -489,7 +490,7 @@
       JCheckbox,
       JCodeEditor,
       JDate, JEditor, JEllipsis, JSlider, JSelectMultiple,
-      JCron, JTreeSelect, JSuperQuery, JMultiSelectTag,
+      JCron, JEasyCron,JTreeSelect, JSuperQuery, JMultiSelectTag,
       JSearchSelectTag
     },
     data() {

+ 6 - 1
ant-design-vue-jeecg/src/views/system/QuartzJobList.vue

@@ -214,7 +214,12 @@
           this.isorter.order = "ascend" == sorter.order ? "asc" : "desc"
         }
         //这种筛选方式只支持单选
-        this.filters.status = filters.status[0];
+        
+        // update-begin-author:liusq date:20210624 for:前台定时任务无法翻页  #2666
+        if(filters && Object.keys(filters).length>0 && filters.status){
+          this.filters.status = filters.status[0];
+        }
+        // update-end-author:liusq date:20210624 for:前台定时任务无法翻页  #2666
         this.ipagination = pagination;
         this.loadData();
       },

+ 0 - 10
ant-design-vue-jeecg/src/views/system/SysDataSourceList.vue

@@ -122,11 +122,6 @@
             align: 'center',
             customRender: (t, r, index) => index + 1
           },
-          {
-            title: '数据源编码',
-            align: 'center',
-            dataIndex: 'code'
-          },
           {
             title: '数据源名称',
             align: 'center',
@@ -149,11 +144,6 @@
             dataIndex: 'dbUrl',
             customRender: (t) => ellipsis(t)
           },
-          {
-            title: '数据库名称',
-            align: 'center',
-            dataIndex: 'dbName'
-          },
           {
             title: '用户名',
             align: 'center',

+ 1 - 1
ant-design-vue-jeecg/src/views/system/SysOnlineList.vue

@@ -70,7 +70,7 @@
   import {getFileAccessHttpUrl} from '@/api/manage';
 
   export default {
-    name: "SysOnlineList",
+    name: "SysUserOnlineList",
     mixins:[JeecgListMixin, mixinDevice],
     components: {},
     data () {

+ 1 - 1
ant-design-vue-jeecg/src/views/system/UserList.vue

@@ -291,7 +291,7 @@
         superQueryFieldList: [
           { type: 'input', value: 'username', text: '用户账号', },
           { type: 'input', value: 'realname', text: '用户姓名', },
-          { type: 'select', value: 'sex', text: '性别', dictCode: 'sex' },
+          { type: 'select', value: 'sex', dbType: 'int', text: '性别', dictCode: 'sex' },
         ],
         url: {
           syncUser: "/act/process/extActProcess/doSyncUser",

+ 8 - 7
ant-design-vue-jeecg/src/views/system/modules/SysCategoryModal.vue

@@ -22,11 +22,11 @@
            :disabled="disabled">
           </j-tree-select>
         </a-form-model-item>
-          
+
         <a-form-model-item label="分类名称" :labelCol="labelCol" :wrapperCol="wrapperCol" prop="name">
           <a-input v-model="model.name" placeholder="请输入分类名称"></a-input>
         </a-form-model-item>
-          
+
       </a-form-model>
     </a-spin>
   </a-modal>
@@ -36,10 +36,10 @@
 
   import { httpAction,getAction } from '@/api/manage'
   import JTreeSelect from '@/components/jeecg/JTreeSelect'
-  
+
   export default {
     name: "SysCategoryModal",
-    components: { 
+    components: {
       JTreeSelect
     },
     data () {
@@ -70,7 +70,7 @@
         expandedRowKeys:[],
         pidField:"pid",
         subExpandedKeys:[]
-     
+
       }
     },
     created () {
@@ -111,7 +111,8 @@
             httpAction(httpurl,this.model,method).then((res)=>{
               if(res.success){
                 that.$message.success(res.message);
-                that.submitSuccess(this.model)
+                // close的时候清空了表单的值 导致model为空 修改值在列表页没有变 此处需要复制一下model
+                that.submitSuccess({...this.model})
               }else{
                 that.$message.warning(res.message);
               }
@@ -122,7 +123,7 @@
           }else{
             return false;
           }
-         
+
         })
       },
       handleCancel () {

+ 43 - 7
ant-design-vue-jeecg/src/views/system/modules/SysDataSourceModal.vue

@@ -41,12 +41,12 @@
           label="数据源地址">
           <a-input placeholder="请输入数据源地址" v-decorator="['dbUrl', validatorRules.dbUrl]"/>
         </a-form-item>
-        <a-form-item
+       <!-- <a-form-item
           :labelCol="labelCol"
           :wrapperCol="wrapperCol"
           label="数据库名称">
           <a-input placeholder="请输入数据库名称" v-decorator="['dbName', validatorRules.dbName]"/>
-        </a-form-item>
+        </a-form-item>-->
         <a-form-item
           :labelCol="labelCol"
           :wrapperCol="wrapperCol"
@@ -124,7 +124,7 @@
           dbUrl: { rules: [{ required: true, message: '请输入数据源地址!' }] },
           dbName: { rules: [{ required: true, message: '请输入数据库名称!' }] },
           dbUsername: { rules: [{ required: true, message: '请输入用户名!' }] },
-          dbPassword: { rules: [{ required: true, message: '请输入密码!' }] }
+          dbPassword: { rules: [{ required: false, message: '请输入密码!' }] }
         },
         url: {
           add: '/sys/dataSource/add',
@@ -142,7 +142,25 @@
           // marialDB 数据库
           '5': { dbDriver: 'org.mariadb.jdbc.Driver' },
           // postgresql 数据库
-          '6': { dbDriver: 'org.postgresql.Driver' }
+          '6': { dbDriver: 'org.postgresql.Driver' },
+          // 达梦 数据库
+          '7': { dbDriver: 'dm.jdbc.driver.DmDriver' },
+          // 人大金仓 数据库
+          '8': { dbDriver: 'com.kingbase8.Driver' },
+          // 神通 数据库
+          '9': { dbDriver: 'com.oscar.Driver' },
+          // SQLite 数据库
+          '10': { dbDriver: 'org.sqlite.JDBC' },
+          // DB2 数据库
+          '11': { dbDriver: 'com.ibm.db2.jcc.DB2Driver' },
+          // Hsqldb 数据库
+          '12': { dbDriver: 'org.hsqldb.jdbc.JDBCDriver' },
+          // Derby 数据库
+          '13': { dbDriver: 'org.apache.derby.jdbc.ClientDriver' },
+          // H2 数据库
+          '14': { dbDriver: 'org.h2.Driver' },
+          // 其他数据库
+          '15': { dbDriver: '' }
         },
         dbUrlMap: {
           // MySQL 数据库
@@ -153,10 +171,28 @@
           '2': { dbUrl: 'jdbc:oracle:thin:@127.0.0.1:1521:ORCL' },
           // SQLServer 数据库
           '3': { dbUrl: 'jdbc:sqlserver://127.0.0.1:1433;SelectMethod=cursor;DatabaseName=jeecgboot' },
-          // SQLServer 数据库
+          // Mariadb 数据库
           '5': { dbUrl: 'jdbc:mariadb://127.0.0.1:3306/jeecg-boot?characterEncoding=UTF-8&useSSL=false' },
-          // SQLServer 数据库
-          '6': { dbUrl: 'jdbc:postgresql://127.0.0.1:5432/jeecg-boot' }
+          // Postgresql 数据库
+          '6': { dbUrl: 'jdbc:postgresql://127.0.0.1:5432/jeecg-boot' },
+          // 达梦 数据库
+          '7': { dbUrl: 'jdbc:dm://127.0.0.1:5236/?jeecg-boot&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf-8' },
+          // 人大金仓 数据库
+          '8': { dbUrl: 'jdbc:kingbase8://127.0.0.1:54321/jeecg-boot' },
+          // 神通 数据库
+          '9': { dbUrl: 'jdbc:oscar://192.168.1.125:2003/jeecg-boot' },
+          // SQLite 数据库
+          '10': { dbUrl: 'jdbc:sqlite://opt/test.db' },
+          // DB2 数据库
+          '11': { dbUrl: 'jdbc:db2://127.0.0.1:50000/jeecg-boot' },
+          // Hsqldb 数据库
+          '12': { dbUrl: 'jdbc:hsqldb:hsql://127.0.0.1/jeecg-boot' },
+          // Derby 数据库
+          '13': { dbUrl: 'jdbc:derby://127.0.0.1:1527/jeecg-boot' },
+          // H2 数据库
+          '14': { dbUrl: 'jdbc:h2:tcp://127.0.0.1:8082/jeecg-boot' },
+          // 其他数据库
+          '15': { dbUrl: '' }
         }
       }
     },

+ 1 - 1
ant-design-vue-jeecg/src/views/system/modules/UserModal.vue

@@ -59,7 +59,7 @@
 
         <!--部门分配-->
         <a-form-model-item label="部门分配" :labelCol="labelCol" :wrapperCol="wrapperCol" v-show="!departDisabled">
-          <j-select-depart v-model="model.selecteddeparts" :multi="true" @back="backDepartInfo" :backDepart="true"></j-select-depart>
+          <j-select-depart v-model="model.selecteddeparts" :multi="true" @back="backDepartInfo" :backDepart="true" :treeOpera="true">></j-select-depart>
         </a-form-model-item>
 
         <!--租户分配-->

+ 12 - 12
ant-design-vue-jeecg/src/views/user/Login.vue

@@ -35,18 +35,18 @@
 </template>
 
 <script>
-  import Vue from 'vue'
-  import { ACCESS_TOKEN ,ENCRYPTED_STRING} from "@/store/mutation-types"
-  import ThirdLogin from './third/ThirdLogin'
-  import LoginSelectTenant from "./LoginSelectTenant"
-  import TwoStepCaptcha from '@/components/tools/TwoStepCaptcha'
-  import { encryption , getEncryptedString } from '@/utils/encryption/aesEncrypt'
-  import { timeFix } from "@/utils/util"
-
-  import LoginAccount from './LoginAccount'
-  import LoginPhone from './LoginPhone'
-
-  export default {
+import Vue from 'vue'
+import { ACCESS_TOKEN, ENCRYPTED_STRING } from '@/store/mutation-types'
+import ThirdLogin from './third/ThirdLogin'
+import LoginSelectTenant from './LoginSelectTenant'
+import TwoStepCaptcha from '@/components/tools/TwoStepCaptcha'
+import { getEncryptedString } from '@/utils/encryption/aesEncrypt'
+import { timeFix } from '@/utils/util'
+
+import LoginAccount from './LoginAccount'
+import LoginPhone from './LoginPhone'
+
+export default {
     components: {
       LoginSelectTenant,
       TwoStepCaptcha,

+ 18 - 19
ant-design-vue-jeecg/src/views/user/LoginSelectTenant.vue

@@ -50,12 +50,11 @@
 
 <script>
 
-  import Vue from 'vue'
-  import { getAction,putAction } from '@/api/manage'
-  import { USER_INFO } from "@/store/mutation-types"
-  import store from './Login'
+import Vue from 'vue'
+import { putAction } from '@/api/manage'
+import { USER_INFO } from '@/store/mutation-types'
 
-  export default {
+export default {
     name: 'LoginSelectTenant',
     data(){
       return {
@@ -111,18 +110,19 @@
           this.isMultiDepart = false
         }
       },
-      bizTenant(ids){
-        if(!ids || ids.length==0){
-          this.isMultiTenant = false
-        } else if(ids.indexOf(',')<0){
-          this.tenant_id = ids;
-          this.isMultiTenant = false
-        }else{
-          this.visible = true
-          this.isMultiTenant = true
-          getAction('/sys/tenant/queryList', {ids: ids}).then(res=>{
-            this.tenantList = res.result
-          })
+      bizTenantList(loginResult) {
+        let tenantList = loginResult.tenantList
+        if (Array.isArray(tenantList)) {
+          if (tenantList.length === 0) {
+            this.isMultiTenant = false
+          } else if (tenantList.length === 1) {
+            this.tenant_id = tenantList[0].id
+            this.isMultiTenant = false
+          } else {
+            this.visible = true
+            this.isMultiTenant = true
+            this.tenantList = tenantList
+          }
         }
       },
       show(loginResult){
@@ -131,8 +131,7 @@
 
         let user = Vue.ls.get(USER_INFO)
         this.username = user.username
-        let ids = user.relTenantIds
-        this.bizTenant(ids);
+        this.bizTenantList(loginResult);
 
         if(this.visible===false){
           this.$store.dispatch('saveTenant', this.tenant_id);

+ 130 - 0
ant-design-vue-jeecg/src/views/user/oauth2/OAuth2Login.vue

@@ -0,0 +1,130 @@
+<template>
+  <div>
+    <div id="loader-wrapper">
+      <div id="loader"></div>
+      <div class="loader-section section-left"></div>
+      <div class="loader-section section-right"></div>
+      <div class="load_title">正在登录 JeecgBoot 低代码平台,请耐心等待</div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapActions } from 'vuex'
+import { isOAuth2AppEnv, timeFix } from '@/utils/util'
+import { INDEX_MAIN_PAGE_PATH } from '@/store/mutation-types'
+
+export default {
+  name: 'OAuth2Login',
+  data() {
+    return {
+      env: {
+        thirdApp: false,
+        wxWork: false,
+        dingtalk: false,
+      },
+    }
+  },
+  beforeCreate() {
+    // 如果当前 不是 OAuth2APP环境,就重定向到 /user/login 页面
+    if (!isOAuth2AppEnv()) {
+      this.$router.replace({path: '/user/login'})
+    }
+  },
+  created() {
+    this.checkEnv()
+    this.doOAuth2Login()
+  },
+  methods: {
+    ...mapActions(['ThirdLogin']),
+
+    /** 检测当前的环境 */
+    checkEnv() {
+      // 判断当时是否是企业微信环境
+      if (/wxwork/i.test(navigator.userAgent)) {
+        this.env.thirdApp = true
+        this.env.wxWork = true
+      }
+      // 判断当时是否是钉钉环境
+      if (/dingtalk/i.test(navigator.userAgent)) {
+        this.env.thirdApp = true
+        this.env.dingtalk = true
+      }
+    },
+
+    /** 进行OAuth2登录操作 */
+    doOAuth2Login() {
+      if (this.env.thirdApp) {
+        // 判断是否携带了Token,是就说明登录成功
+        if (this.$route.query.oauth2LoginToken) {
+          this.thirdType = this.$route.query.thirdType
+          let token = this.$route.query.oauth2LoginToken
+          this.doThirdLogin(token)
+        } else if (this.env.wxWork) {
+          this.doWechatEnterpriseOAuth2Login()
+        } else if (this.env.dingtalk) {
+          this.doDingTalkOAuth2Login()
+        }
+      }
+    },
+
+    // 根据token执行登录
+    doThirdLogin(token) {
+      let param = {}
+      param.thirdType = this.thirdType
+      param.token = token
+      this.ThirdLogin(param).then(res => {
+        if (res.success) {
+          this.loginSuccess()
+        } else {
+          this.requestFailed(res)
+        }
+      })
+    },
+    loginSuccess() {
+      // 登陆成功,重定向到主页
+      this.$router.replace({path: INDEX_MAIN_PAGE_PATH})
+      // TODO 这个提示是否还需要?
+      this.$notification.success({
+        message: '欢迎',
+        description: `${timeFix()},欢迎回来`,
+      })
+    },
+    requestFailed(err) {
+      this.$error({
+        title: '登录失败',
+        content: ((err.response || {}).data || {}).message || err.message || '请求出现错误,请稍后再试',
+        okText: '重新登陆',
+        onOk() {
+          window.location.reload()
+        },
+        onCancel() {
+          window.location.reload()
+        },
+      })
+    },
+
+    /** 企业微信OAuth2登录 */
+    doWechatEnterpriseOAuth2Login() {
+      this.sysOAuth2Login('wechat_enterprise')
+    },
+
+    /** 钉钉OAuth2登录 */
+    doDingTalkOAuth2Login() {
+      this.sysOAuth2Login('dingtalk')
+    },
+
+    /** 后台构造oauth2登录地址 */
+    sysOAuth2Login(source) {
+      let url = `${window._CONFIG['domianURL']}/sys/thirdLogin/oauth2/${source}/login`
+      url += `?state=${encodeURIComponent(window.location.origin)}`
+      window.location.href = url
+    },
+
+  },
+}
+</script>
+
+<style scoped>
+
+</style>