Эх сурвалжийг харах

feat(system): 删除第三方应用配置功能

- 删除钉钉和企业微信集成配置页面
- 删除第三方应用配置的保存和更新
- 删除企业微信用户同步和解绑功能
- 优化多语言文案和页面布局
zhangtao 3 долоо хоног өмнө
parent
commit
c63aa92e51
33 өөрчлөгдсөн 2937 нэмэгдсэн , 1461 устгасан
  1. 18 12
      src/layouts/default/header/MultipleHeader.vue
  2. 7 7
      src/layouts/default/header/components/user-dropdown/index.vue
  3. 0 4
      src/layouts/default/header/index.vue
  4. 1 1
      src/layouts/default/setting/SettingDrawer.tsx
  5. 3 4
      src/locales/lang/zh-CN/layout.ts
  6. 4 4
      src/store/modules/locale.ts
  7. 30 30
      src/views/dashboard/Analysis/components/BdcTabCard.vue
  8. 1 2
      src/views/dashboard/Analysis/homePage/IndexBdc.vue
  9. 0 17
      src/views/demo/page/account/center/data.tsx
  10. 1 11
      src/views/demo/page/account/center/index.vue
  11. 2 3
      src/views/sys/login/Login.vue
  12. 0 3
      src/views/sys/login/LoginForm.vue
  13. 0 1
      src/views/sys/login/LoginFormTitle.vue
  14. 5 5
      src/views/sys/login/LoginSelect.vue
  15. 69 0
      src/views/system/appconfig/ThirdApp.api.ts
  16. 61 0
      src/views/system/appconfig/ThirdApp.data.ts
  17. 316 0
      src/views/system/appconfig/ThirdAppBindWeEnterpriseModal.vue
  18. 140 0
      src/views/system/appconfig/ThirdAppConfigList.vue
  19. 69 0
      src/views/system/appconfig/ThirdAppConfigModal.vue
  20. 303 0
      src/views/system/appconfig/ThirdAppDingTalkConfigForm.vue
  21. 249 0
      src/views/system/appconfig/ThirdAppWeEnterpriseConfigForm.vue
  22. 1 1
      src/views/system/loginmini/MiniCodelogin.vue
  23. 29 29
      src/views/system/loginmini/MiniForgotpad.vue
  24. 27 19
      src/views/system/loginmini/MiniLogin.vue
  25. 0 288
      src/views/system/loginmini/MiniRegister.vue
  26. 128 50
      src/views/system/menu/MenuDrawer.vue
  27. 249 92
      src/views/system/menu/index.vue
  28. 362 103
      src/views/system/menu/menu.data.ts
  29. 0 80
      src/views/system/role/RoleDrawer.vue
  30. 185 89
      src/views/system/role/index.vue
  31. 139 79
      src/views/system/role/role.data.ts
  32. 143 145
      src/views/system/usersetting/BaseSetting.vue
  33. 395 382
      src/views/system/usersetting/TenantSetting.vue

+ 18 - 12
src/layouts/default/header/MultipleHeader.vue

@@ -11,8 +11,8 @@
   import LayoutHeader from './index.vue';
   import MultipleTabs from '../tabs/index.vue';
 
-  import { useAppStore } from "@/store/modules/app";
-  import { useGlobSetting } from "/@/hooks/setting";
+  import { useAppStore } from '@/store/modules/app';
+  import { useGlobSetting } from '/@/hooks/setting';
   import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
   import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
   import { useFullContent } from '/@/hooks/web/useFullContent';
@@ -39,8 +39,8 @@
       const { setHeaderHeight } = useLayoutHeight();
       const { prefixCls } = useDesign('layout-multiple-header');
 
-      const appStore = useAppStore()
-      const glob = useGlobSetting()
+      const appStore = useAppStore();
+      const glob = useGlobSetting();
 
       const { getCalcContentWidth, getSplit, getMenuType } = useMenuSetting();
       const { getIsMobile } = useAppInject();
@@ -56,7 +56,7 @@
           return false;
         }
         return unref(getShowInsetHeaderRef);
-      })
+      });
 
       const getShowTabs = computed(() => {
         // 控制是否显示多Tabs切换
@@ -100,7 +100,10 @@
       const getPlaceholderDomStyle = computed((): CSSProperties => {
         let height = 0;
         // update-begin--author:liaozhiyang---date:20241216---for:【issues/7561】主题切换为顶部混合模式时,页面顶部内容显示不出来,被遮盖
-        if ((unref(getShowFullHeaderRef) || !unref(getSplit)) && unref(getShowHeader) && !unref(getFullContent) || unref(getMenuType) == MenuTypeEnum.MIX) {
+        if (
+          ((unref(getShowFullHeaderRef) || !unref(getSplit)) && unref(getShowHeader) && !unref(getFullContent)) ||
+          unref(getMenuType) == MenuTypeEnum.MIX
+        ) {
           height += HEADER_HEIGHT;
         }
         // update-end--author:liaozhiyang---date:20241216---for:【issues/7561】主题切换为顶部混合模式时,页面顶部内容显示不出来,被遮盖
@@ -114,11 +117,15 @@
       });
 
       const getClass = computed(() => {
-        return [prefixCls, `${prefixCls}--${unref(getHeaderTheme)}`, {
-          [`${prefixCls}--fixed`]: unref(getIsFixed),
-          // 【JEECG作为乾坤子应用】
-          [`${prefixCls}--qiankun-micro`]: glob.isQiankunMicro,
-        }];
+        return [
+          prefixCls,
+          `${prefixCls}--${unref(getHeaderTheme)}`,
+          {
+            [`${prefixCls}--fixed`]: unref(getIsFixed),
+            // 【JEECG作为乾坤子应用】
+            [`${prefixCls}--qiankun-micro`]: glob.isQiankunMicro,
+          },
+        ];
       });
 
       return {
@@ -157,6 +164,5 @@
     &--qiankun-micro {
       position: absolute;
     }
-
   }
 </style>

+ 7 - 7
src/layouts/default/header/components/user-dropdown/index.vue

@@ -11,19 +11,19 @@
 
     <template #overlay>
       <Menu @click="handleMenuClick">
-        <MenuItem itemKey="doc" :text="t('layout.header.dropdownItemDoc')" icon="ion:document-text-outline" v-if="getShowDoc" />
+        <!-- <MenuItem itemKey="doc" :text="t('layout.header.dropdownItemDoc')" icon="ion:document-text-outline" v-if="getShowDoc" /> -->
         <MenuDivider v-if="getShowDoc" />
-        <MenuItem itemKey="account" :text="t('layout.header.dropdownItemSwitchAccount')" icon="ant-design:setting-outlined" />
-        <MenuItem itemKey="password" :text="t('layout.header.dropdownItemSwitchPassword')" icon="ant-design:edit-outlined" />
-        <MenuItem itemKey="depart" :text="t('layout.header.dropdownItemSwitchDepart')" icon="ant-design:cluster-outlined" />
-        <MenuItem itemKey="cache" :text="t('layout.header.dropdownItemRefreshCache')" icon="ion:sync-outline" />
+        <MenuItem itemKey="account" text="个人信息" icon="ant-design:setting-outlined" />
+        <MenuItem itemKey="password" text="修改密码" icon="ant-design:edit-outlined" />
+        <MenuItem itemKey="depart" text="切换部门" icon="ant-design:cluster-outlined" />
+        <MenuItem itemKey="cache" text="清除缓存" icon="ion:sync-outline" />
         <!-- <MenuItem
             v-if="getUseLockPage"
             itemKey="lock"
             :text="t('layout.header.tooltipLock')"
             icon="ion:lock-closed-outline"
         />-->
-        <MenuItem itemKey="logout" :text="t('layout.header.dropdownItemLoginOut')" icon="ion:power-outline" />
+        <MenuItem itemKey="logout" text="退出系统" icon="ion:power-outline" />
       </Menu>
     </template>
   </Dropdown>
@@ -57,7 +57,7 @@
   import { removeAuthCache, setAuthCache } from '/src/utils/auth';
   import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
   import { getRefPromise } from '/@/utils/index';
-  import { refreshDragCache } from "@/api/common/api";
+  import { refreshDragCache } from '@/api/common/api';
 
   type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart';
   const { createMessage } = useMessage();

+ 0 - 4
src/layouts/default/header/index.vue

@@ -38,8 +38,6 @@
 
       <LockScreen v-if="getUseLockPage" />
 
-      <AppLocalePicker v-if="getShowLocalePicker" :reload="true" :showText="false" :class="`${prefixCls}-action__item`" />
-
       <UserDropDown :theme="getHeaderTheme" />
 
       <SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
@@ -65,7 +63,6 @@
 
   import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
   import { SettingButtonPositionEnum } from '/@/enums/appEnum';
-  import { AppLocalePicker } from '/@/components/Application';
 
   import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ErrorAction, LockScreen } from './components';
   import { useAppInject } from '/@/hooks/web/useAppInject';
@@ -88,7 +85,6 @@
       LayoutBreadcrumb,
       LayoutMenu,
       UserDropDown,
-      AppLocalePicker,
       FullScreen,
       Notify,
       AppSearch,

+ 1 - 1
src/layouts/default/setting/SettingDrawer.tsx

@@ -317,7 +317,7 @@ export default defineComponent({
 
           <SwitchItem title={t('layout.setting.colorWeak')} event={HandlerEnum.COLOR_WEAK} def={unref(getColorWeak)} />
 
-           <SwitchItem title={t('layout.setting.aiIconSHow')} event={HandlerEnum.AI_ICON_SHOW} def={unref(getAiIconShow)} />
+           {/* <SwitchItem title={t('layout.setting.aiIconSHow')} event={HandlerEnum.AI_ICON_SHOW} def={unref(getAiIconShow)} /> */}
         </>
       );
     }

+ 3 - 4
src/locales/lang/zh-CN/layout.ts

@@ -23,9 +23,9 @@ export default {
     lockScreenBtn: '锁定',
 
     home: '首页',
-    welcomeIn:"欢迎进入",
-    refreshCacheComplete: "刷新缓存完成!",
-    refreshCacheFailure: "刷新缓存失败!",
+    welcomeIn: '欢迎进入',
+    refreshCacheComplete: '刷新缓存完成!',
+    refreshCacheFailure: '刷新缓存失败!',
   },
   multipleTab: {
     reload: '刷 新',
@@ -106,7 +106,6 @@ export default {
     fullContent: '全屏内容',
     grayMode: '灰色模式',
     colorWeak: '色弱模式',
-    aiIconSHow: 'Ai图标显示',
 
     progress: '顶部进度条',
     switchLoading: '切换loading',

+ 4 - 4
src/store/modules/locale.ts

@@ -15,9 +15,9 @@ interface LocaleState {
   localInfo: LocaleSetting;
   pathTitleMap: object;
   // myapps主题色(低代码应用列表首页)
-  appIndexTheme: string
+  appIndexTheme: string;
   // myapps - 跳转前路由地址
-  appMainPth: string
+  appMainPth: string;
 }
 
 export const useLocaleStore = defineStore({
@@ -26,14 +26,14 @@ export const useLocaleStore = defineStore({
     localInfo: lsLocaleSetting,
     pathTitleMap: {},
     appIndexTheme: '',
-    appMainPth: ''
+    appMainPth: '',
   }),
   getters: {
     getShowPicker(): boolean {
       return !!this.localInfo?.showPicker;
     },
     getLocale(): LocaleType {
-      return this.localInfo?.locale ?? 'zh_CN';
+      return 'zh_CN';
     },
     //update-begin-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
     getPathTitle: (state) => {

+ 30 - 30
src/views/dashboard/Analysis/components/BdcTabCard.vue

@@ -51,38 +51,38 @@
   const { getThemeColor } = useRootSetting();
   const interactiveColor = ref();
   const rankList = [];
-  // for (let i = 0; i < 7; i++) {
-  //   rankList.push({
-  //     name: '白鹭岛 ' + (i + 1) + ' 号店',
-  //     total: 1234.56 - i * 100,
-  //   });
-  // }
+  for (let i = 0; i < 7; i++) {
+    rankList.push({
+      name: '白鹭岛 ' + (i + 1) + ' 号店',
+      total: 1234.56 - i * 100,
+    });
+  }
 
-  // const barData = [];
-  // for (let i = 0; i < 12; i += 1) {
-  //   barData.push({
-  //     name: `${i + 1}月`,
-  //     value: Math.floor(Math.random() * 1000) + 200,
-  //   });
-  // }
-  // const barMultiData = [];
-  // for (let j = 0; j < 2; j++) {
-  //   for (let i = 0; i < 12; i += 1) {
-  //     barMultiData.push({
-  //       type: j == 0 ? 'jeecg' : 'jeebt',
-  //       name: `${i + 1}月`,
-  //       value: Math.floor(Math.random() * 1000) + 200,
-  //     });
-  //   }
-  // }
+  const barData = [];
+  for (let i = 0; i < 12; i += 1) {
+    barData.push({
+      name: `${i + 1}月`,
+      value: Math.floor(Math.random() * 1000) + 200,
+    });
+  }
+  const barMultiData = [];
+  for (let j = 0; j < 2; j++) {
+    for (let i = 0; i < 12; i += 1) {
+      barMultiData.push({
+        type: j == 0 ? 'jeecg' : 'jeebt',
+        name: `${i + 1}月`,
+        value: Math.floor(Math.random() * 1000) + 200,
+      });
+    }
+  }
 
-  // const seriesColor = computed(() => {
-  //   interactiveColor.value = [
-  //     { type: 'jeecg', color: getThemeColor.value },
-  //     { type: 'jeebt', color: getRandomColor() },
-  //   ];
-  //   return getThemeColor.value;
-  // });
+  const seriesColor = computed(() => {
+    interactiveColor.value = [
+      { type: 'jeecg', color: getThemeColor.value },
+      { type: 'jeebt', color: getRandomColor() },
+    ];
+    return getThemeColor.value;
+  });
   function getRandomColor() {
     var letters = '0123456789ABCDEF';
     var color = '#';

+ 1 - 2
src/views/dashboard/Analysis/homePage/IndexBdc.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="p-4">
+    <ChartGroupCard class="enter-y" :loading="loading" type="bdc" />
     <BdcTabCard class="!my-4 enter-y" :loading="loading" />
     <a-row>
       <a-col :span="24">
@@ -63,8 +64,6 @@
   import { ref, unref } from 'vue';
   import { Progress } from 'ant-design-vue';
   import BdcTabCard from '../components/BdcTabCard.vue';
-  // import LineMulti from '/@/components/chart/LineMulti.vue';
-  // import HeadInfo from '/@/components/chart/HeadInfo.vue';
   import { table, table1 } from '../data';
 
   const loading = ref(true);

+ 0 - 17
src/views/demo/page/account/center/data.tsx

@@ -60,23 +60,6 @@ export const details: ListItem[] = [
   },
 ];
 
-export const achieveList: TabItem[] = [
-  {
-    key: '1',
-    name: '文章',
-    component: 'Article',
-  },
-  {
-    key: '2',
-    name: '应用',
-    component: 'Application',
-  },
-  {
-    key: '3',
-    name: '项目',
-    component: 'Project',
-  },
-];
 
 export const actions: any[] = [
   { icon: 'clarity:star-line', text: '156', color: '#018ffb' },

+ 1 - 11
src/views/demo/page/account/center/index.vue

@@ -40,15 +40,6 @@
         </CollapseContainer>
       </a-col>
     </a-row>
-    <div :class="`${prefixCls}-bottom`">
-      <Tabs>
-        <template v-for="item in achieveList" :key="item.key">
-          <TabPane :tab="item.name">
-            <component :is="item.component" />
-          </TabPane>
-        </template>
-      </Tabs>
-    </div>
   </div>
 </template>
 
@@ -62,7 +53,7 @@
   import Project from './Project.vue';
 
   import headerImg from '/@/assets/images/header.jpg';
-  import { tags, teams, details, achieveList } from './data';
+  import { tags, teams, details } from './data';
   import { useUserStore } from '/@/store/modules/user';
 
   export default defineComponent({
@@ -87,7 +78,6 @@
         tags,
         teams,
         details,
-        achieveList,
       };
     },
   });

+ 2 - 3
src/views/sys/login/Login.vue

@@ -1,6 +1,5 @@
 <template>
   <div :class="prefixCls" class="relative w-full h-full px-4">
-    <AppLocalePicker class="absolute text-white top-4 right-4 enter-x xl:text-gray-600" :showText="false" v-if="!sessionTimeout && showLocale" />
     <AppDarkModeToggle class="absolute top-3 right-7 enter-x" v-if="!sessionTimeout" />
     <span class="-enter-x xl:hidden">
       <AppLogo :alwaysShowTitle="true" />
@@ -11,7 +10,7 @@
         <div class="hidden min-h-full pl-4 mr-4 xl:flex xl:flex-col xl:w-6/12">
           <AppLogo class="-enter-x" />
           <div class="my-auto">
-            <!-- <img :alt="title" src="../../../assets/svg/login-box-bg.svg" class="w-1/2 -mt-16 -enter-x" /> -->
+            <img :alt="title" src="../../../assets/svg/login-box-bg.svg" class="w-1/2 -mt-16 -enter-x" />
             <div class="mt-10 font-medium text-white -enter-x">
               <span class="inline-block mt-4 text-3xl"> {{ t('sys.login.signInTitle') }}</span>
             </div>
@@ -39,7 +38,7 @@
 <script lang="ts" setup>
   import { computed } from 'vue';
   import { AppLogo } from '/@/components/Application';
-  import { AppLocalePicker, AppDarkModeToggle } from '/@/components/Application';
+  import { AppDarkModeToggle } from '/@/components/Application';
   import LoginForm from './LoginForm.vue';
   import ForgetPasswordForm from './ForgetPasswordForm.vue';
   import RegisterForm from './RegisterForm.vue';

+ 0 - 3
src/views/sys/login/LoginForm.vue

@@ -51,9 +51,6 @@
       <Button type="primary" size="large" block @click="handleLogin" :loading="loading">
         {{ t('sys.login.loginButton') }}
       </Button>
-      <!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
-              {{ t('sys.login.registerButton') }}
-            </Button> -->
     </FormItem>
     <ARow class="enter-x">
       <ACol :md="7" :xs="24">

+ 0 - 1
src/views/sys/login/LoginFormTitle.vue

@@ -16,7 +16,6 @@
     const titleObj = {
       [LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
       [LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
-      [LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
     };
     return titleObj[unref(getLoginState)];
   });

+ 5 - 5
src/views/sys/login/LoginSelect.vue

@@ -138,11 +138,11 @@
        */
       function bizDepart(loginResult) {
         //如果登录接口返回了用户上次登录租户ID,则不需要重新选择
-        if(loginResult.userInfo?.orgCode && loginResult.userInfo?.orgCode!==''){
+        if (loginResult.userInfo?.orgCode && loginResult.userInfo?.orgCode !== '') {
           isMultiDepart.value = false;
           return;
         }
-        
+
         let multi_depart = loginResult.multi_depart;
         //0:无部门 1:一个部门 2:多个部门
         if (multi_depart == 0) {
@@ -165,11 +165,11 @@
        */
       function bizTenantList(loginResult) {
         //如果登录接口返回了用户上次登录租户ID,则不需要重新选择
-        if(loginResult.userInfo?.loginTenantId && loginResult.userInfo?.loginTenantId!==0){
+        if (loginResult.userInfo?.loginTenantId && loginResult.userInfo?.loginTenantId !== 0) {
           isMultiTenant.value = false;
           return;
         }
-        
+
         let tenantArr = loginResult.tenantList;
         if (Array.isArray(tenantArr)) {
           if (tenantArr.length === 0) {
@@ -225,7 +225,7 @@
           if (!unref(isMultiDepart) && !unref(isMultiTenant)) {
             resolve();
           } else {
-            let params = { orgCode: formState.orgCode,loginTenantId: formState.tenantId, username: unref(username) };
+            let params = { orgCode: formState.orgCode, loginTenantId: formState.tenantId, username: unref(username) };
             defHttp.put({ url: '/sys/selectDepart', params }).then((res) => {
               if (res.userInfo) {
                 userStore.setUserInfo(res.userInfo);

+ 69 - 0
src/views/system/appconfig/ThirdApp.api.ts

@@ -0,0 +1,69 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  //第三方登录配置
+  addThirdAppConfig = '/sys/thirdApp/addThirdAppConfig',
+  editThirdAppConfig = '/sys/thirdApp/editThirdAppConfig',
+  getThirdConfigByTenantId = '/sys/thirdApp/getThirdConfigByTenantId',
+  syncDingTalkDepartUserToLocal = '/sys/thirdApp/sync/dingtalk/departAndUser/toLocal',
+  getThirdUserByWechat = '/sys/thirdApp/getThirdUserByWechat',
+  wechatEnterpriseToLocal = '/sys/thirdApp/sync/wechatEnterprise/departAndUser/toLocal',
+  getThirdUserBindByWechat = '/sys/thirdApp/getThirdUserBindByWechat',
+  deleteThirdAccount = '/sys/thirdApp/deleteThirdAccount',
+}
+
+/**
+ * 第三方配置保存或者更新
+ */
+export const saveOrUpdateThirdConfig = (params, isUpdate) => {
+  let url = isUpdate ? Api.editThirdAppConfig : Api.addThirdAppConfig;
+  return defHttp.post({ url: url, params }, { joinParamsToUrl: true });
+};
+
+/**
+ * 获取第三方配置
+ * @param params
+ */
+export const getThirdConfigByTenantId = (params) => {
+  return defHttp.get({ url: Api.getThirdConfigByTenantId, params });
+};
+
+/**
+ * 同步钉钉部门用户到本地
+ * @param params
+ */
+export const syncDingTalkDepartUserToLocal = () => {
+  return defHttp.get({ url: Api.syncDingTalkDepartUserToLocal, timeout: 60000 }, { isTransformResponse: false });
+};
+
+/**
+ * 获取企业微信绑定的用户信息
+ * @param params
+ */
+export const getThirdUserByWechat = () => {
+  return defHttp.get({ url: Api.getThirdUserByWechat }, { isTransformResponse: false });
+};
+
+/**
+ * 同步企业微信用户部门到本地
+ * @param params
+ */
+export const wechatEnterpriseToLocal = (params) => {
+  return defHttp.get({ url: Api.wechatEnterpriseToLocal, params }, { isTransformResponse: false });
+};
+
+/**
+ * 获取绑定企业微信的用户
+ * @param params
+ */
+export const getThirdUserBindByWechat = () => {
+  return defHttp.get({ url: Api.getThirdUserBindByWechat }, { isTransformResponse: false });
+};
+
+/**
+ * 根据第三方账号表的id解绑账号
+ * @param params
+ */
+export const deleteThirdAccount = (params) => {
+  return defHttp.delete({ url: Api.deleteThirdAccount, params }, { isTransformResponse:false, joinParamsToUrl: true });
+};

+ 61 - 0
src/views/system/appconfig/ThirdApp.data.ts

@@ -0,0 +1,61 @@
+//第三方app配置表单
+import { FormSchema } from '/@/components/Form';
+
+//第三方app表单
+export const thirdAppFormSchema: FormSchema[] = [
+  {
+    label: 'id',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
+  {
+    label: 'thirdType',
+    field: 'thirdType',
+    component: 'Input',
+    show: false,
+  },
+  {
+    label: 'CorpId',
+    field: 'corpId',
+    component: 'Input',
+    ifShow: ({ values }) => {
+      return values.thirdType === 'dingtalk';
+    },
+    required: true,
+  },
+  {
+    label: 'Agentld',
+    field: 'agentId',
+    component: 'Input',
+    required: true,
+  },
+  {
+    label: 'AppKey',
+    field: 'clientId',
+    component: 'Input',
+    required: true,
+  },
+  {
+    label: 'AppSecret',
+    field: 'clientSecret',
+    component: 'Input',
+    required: true,
+  },{
+    label: '启用',
+    field: 'status',
+    component: 'Switch',
+    componentProps:{
+      checkedChildren:'关闭',
+      checkedValue:1,
+      unCheckedChildren:'开启',
+      unCheckedValue: 0
+    },
+    defaultValue: 1
+  },{
+    label: '租户id',
+    field: 'tenantId',
+    component: 'Input',
+    show: false,
+  },
+];

+ 316 - 0
src/views/system/appconfig/ThirdAppBindWeEnterpriseModal.vue

@@ -0,0 +1,316 @@
+<!--弹窗绑定企业微信页面-->
+<template>
+  <BasicModal @register="registerModal" :width="800" :title="title" destroyOnClose>
+    <a-spin :spinning="loading">
+      <div class="we-bind">
+        <a-row :span="24" class="we-title-background">
+          <a-col :span="12" class="border-right">
+            <span>组织用户</span>
+          </a-col>
+          <a-col :span="12" class="padding-left">
+            <span>企业微信用户</span>
+          </a-col>
+        </a-row>
+        <a-row :span="24">
+          <template v-for="(item, index) in bindData.jwUserDepartVos">
+            <a-col :span="12" class="border-right padding-left border-bottom">
+              <div class="we-account">
+                <a-avatar v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)" :size="28"></a-avatar>
+                <a-avatar v-else :size="28">
+                  {{ item.realName.length > 2 ? item.realName.substr(0, 2) : item.realName }}
+                </a-avatar>
+                <a-input style="margin-left: 20px" :value="item.realName" readonly />
+              </div>
+            </a-col>
+            <a-col :span="12" class="padding-left border-bottom">
+              <div class="we-account">
+                <span v-if="item.wechatUserId || izBind" class="we-remove"
+                  >{{ item.wechatRealName }} <span style="margin-right: 20px" @click="handleRemoveClick(index, item)">移出</span></span
+                >
+                <a-select
+                  v-else
+                  v-model:value="item.wechatUserId"
+                  :options="userList"
+                  :fieldNames="{ label: 'wechatRealName', value: 'wechatUserId' }"
+                  style="width: 200px"
+                  showSearch
+                  @select="(val, option) => handleSelect(val, option, index)"
+                />
+              </div>
+            </a-col>
+          </template>
+        </a-row>
+      </div>
+    </a-spin>
+    <template #footer>
+      <a-button v-if="!izBind" type="primary" @click="handleSubmit">同步</a-button>
+    </template>
+  </BasicModal>
+</template>
+
+<script lang="ts">
+  import { defineComponent, h, ref } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { getThirdUserByWechat, wechatEnterpriseToLocal, getThirdUserBindByWechat, deleteThirdAccount } from './ThirdApp.api';
+  import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
+  import { useMessage } from '@/hooks/web/useMessage';
+  import { Modal } from 'ant-design-vue';
+  import { useUserStore } from '@/store/modules/user';
+
+  export default defineComponent({
+    name: 'ThirdAppBindWeEnterpriseModal',
+    components: { BasicModal },
+    setup(props, { emit }) {
+      const title = ref<string>('企业微信绑定');
+      //企业微信的绑定数据
+      const bindData = ref<any>({});
+      const loading = ref<boolean>(false);
+      const btnLoading = ref<boolean>(false);
+      const { createMessage } = useMessage();
+      const userList = ref<any>([]);
+      //同步文本信息展示
+      const syncText = ref<string>('');
+      //是否已绑定数据,展示不同的列表
+      const izBind = ref<boolean>(false);
+      const userStore = useUserStore();
+      //表单赋值
+      const [registerModal, { closeModal }] = useModalInner(async (data) => {
+        loading.value = true;
+        console.log('izBind::', izBind);
+        if (!data.izBind) {
+          await getUnboundData();
+        } else {
+          await getBoundData();
+        }
+        izBind.value = data.izBind;
+      });
+
+      /**
+       * 未绑定的数据
+       */
+      async function getUnboundData() {
+        await getThirdUserByWechat().then((res) => {
+          if (res.success) {
+            let userLists = res.result.userList;
+            bindData.value = res.result;
+            userList.value = res.result.userList;
+            /*   if (userLists && userLists.length > 0) {
+            syncText.value = "";
+          } else {
+            syncText.value = "企业微信用户均已同步";
+          }*/
+            loading.value = false;
+          } else {
+            createMessage.warning(res.message);
+            loading.value = false;
+          }
+        });
+      }
+
+      /**
+       * 已绑定的数据
+       */
+      async function getBoundData() {
+        await getThirdUserBindByWechat().then((res) => {
+          if (res.success) {
+            bindData.value.jwUserDepartVos = res.result;
+            loading.value = false;
+          } else {
+            createMessage.warn(res.message);
+            loading.value = false;
+          }
+        });
+      }
+
+      /**
+       * 第三方配置点击事件
+       */
+      async function handleSubmit() {
+        btnLoading.value = true;
+        let userList = bindData.value.userList;
+        //重新封装数据,只留用户id和企业微信id即可,还需要把没绑定的用户传给后台
+        let params: any = [];
+        //查询用户绑定的企业微信用户
+        for (const item of bindData.value.jwUserDepartVos) {
+          if (item.wechatUserId) {
+            userList = userList.filter((a) => a.wechatUserId != item.wechatUserId);
+            params.push({
+              wechatUserId: item.wechatUserId,
+              wechatDepartId: item.wechatDepartId,
+              wechatRealName: item.wechatRealName,
+              userId: item.userId,
+            });
+          }
+        }
+        let text: string = '';
+        //查询未被绑定的租户
+        if (userList && userList.length > 0) {
+          for (const item of userList) {
+            params.push({ wechatUserId: item.wechatUserId, wechatDepartId: item.wechatDepartId, wechatRealName: item.wechatRealName });
+          }
+          text = '检测到未绑定的企业微信用户 ' + userList.length + ' 位,平台将会为这 ' + userList.length + ' 位用户创建新的账号';
+        }
+
+        Modal.confirm({
+          title: '确认同步',
+          content: text,
+          okText: '确认',
+          onOk: () => {
+            let json = JSON.stringify(params);
+            console.log('json::', json);
+            wechatEnterpriseToLocal({ jwUserDepartJson: json })
+              .then((res) => {
+                let options = {};
+                if (res.success) {
+                  if (res.result) {
+                    options = {
+                      width: 600,
+                      title: res.message,
+                      content: () => {
+                        let nodes;
+                        let successInfo = [`成功信息如下:`, renderTextarea(h, res.result.successInfo.map((v, i) => `${i + 1}. ${v}`).join('\n'))];
+                        if (res.success) {
+                          nodes = [...successInfo, h('br'), `无失败信息!`];
+                        } else {
+                          nodes = [
+                            `失败信息如下:`,
+                            renderTextarea(h, res.result.failInfo.map((v, i) => `${i + 1}. ${v}`).join('\n')),
+                            h('br'),
+                            ...successInfo,
+                          ];
+                        }
+                        return nodes;
+                      },
+                    };
+                  }
+                  closeModal();
+                  emit('success', options, res);
+                }
+              })
+              .finally(() => {
+                btnLoading.value = false;
+              });
+          },
+        });
+      }
+
+      /**
+       * 下拉框选择事件
+       */
+      function handleSelect(val, option, index) {
+        bindData.value.jwUserDepartVos[index].wechatUserId = option.wechatUserId;
+        bindData.value.jwUserDepartVos[index].wechatRealName = option.wechatRealName;
+        bindData.value.jwUserDepartVos[index].wechatDepartId = option.wechatDepartId;
+        userList.value = userList.value.filter((item) => item.wechatUserId != option.wechatUserId);
+      }
+
+      /**
+       * 移出事件
+       * @param index
+       * @param item
+       */
+      function handleRemoveClick(index, item) {
+        if (!izBind.value) {
+          userList.value.push({
+            wechatUserId: item.wechatUserId,
+            wechatRealName: item.wechatRealName,
+            wechatDepartId: item.wechatDepartId,
+          });
+          bindData.value.jwUserDepartVos[index].wechatUserId = '';
+          bindData.value.jwUserDepartVos[index].wechatRealName = '';
+          bindData.value.jwUserDepartVos[index].wechatDepartId = '';
+        } else {
+          Modal.confirm({
+            title: '确认取消绑定吗',
+            okText: '确认',
+            onOk: async () => {
+              await deleteThirdAccount({ id: item.thirdId, sysUserId: userStore.getUserInfo.id }).then((res) => {
+                if (res.success) {
+                  createMessage.success('取消绑定成功!');
+                  getBoundData();
+                } else {
+                  createMessage.warning(res.message);
+                }
+              });
+            },
+          });
+        }
+      }
+
+      function renderTextarea(h, value) {
+        return h(
+          'div',
+          {
+            id: 'box',
+            style: {
+              minHeight: '100px',
+              border: '1px solid #d9d9d9',
+              fontSize: '14px',
+              maxHeight: '250px',
+              whiteSpace: 'pre',
+              overflow: 'auto',
+              padding: '10px',
+            },
+          },
+          value
+        );
+      }
+
+      return {
+        title,
+        registerModal,
+        handleSubmit,
+        bindData,
+        getFileAccessHttpUrl,
+        loading,
+        userList,
+        handleSelect,
+        handleRemoveClick,
+        btnLoading,
+        izBind,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .we-bind {
+    overflow-y: auto;
+    border: 1px @border-color-base solid;
+    border-bottom: none;
+    .we-title-background {
+      background: @component-background;
+      height: 40px;
+      line-height: 40px;
+      padding: 0 10px;
+    }
+    .we-account {
+      display: flex;
+      height: 40px;
+      line-height: 40px;
+      align-items: center;
+    }
+
+    :deep(.ant-input) {
+      border: none;
+      padding: 0;
+      box-shadow: none;
+    }
+
+    .we-remove {
+      display: flex;
+      justify-content: space-between;
+      width: 100%;
+      cursor: pointer;
+    }
+    .border-right {
+      border-right: 1px @border-color-base solid;
+    }
+    .border-bottom {
+      border-bottom: 1px @border-color-base solid;
+    }
+    .padding-left {
+      padding-left: 10px;
+    }
+  }
+</style>

+ 140 - 0
src/views/system/appconfig/ThirdAppConfigList.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="ding-ding-container" :class="[`${prefixCls}`]">
+    <div class="ding-header">
+      <ul class="ding-menu-tab">
+        <li :class="activeKey === 'ding' ? 'active' : ''" @click="dingLiClick('ding')"><a>钉钉集成</a></li>
+        <li :class="activeKey === 'wechat' ? 'active' : ''" @click="dingLiClick('wechat')"><a>企业微信集成</a></li>
+      </ul>
+    </div>
+    <div v-show="activeKey === 'ding'" class="base-collapse">
+      <ThirdAppDingTalkConfigForm />
+    </div>
+    <div v-show="activeKey === 'wechat'" class="base-collapse">
+      <ThirdAppWeEnterpriseConfigForm />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+  import ThirdAppDingTalkConfigForm from './ThirdAppDingTalkConfigForm.vue';
+  import ThirdAppWeEnterpriseConfigForm from './ThirdAppWeEnterpriseConfigForm.vue';
+  import { useDesign } from '/@/hooks/web/useDesign';
+
+  export default defineComponent({
+    name: 'ThirdAppConfigList',
+    components: {
+      ThirdAppDingTalkConfigForm,
+      ThirdAppWeEnterpriseConfigForm,
+    },
+    setup() {
+      const { prefixCls } = useDesign('j-dd-container');
+
+      //选中的key
+      const activeKey = ref<string>('ding');
+
+      /**
+       * tab点击事件
+       * @param key
+       */
+      function dingLiClick(key) {
+        activeKey.value = key;
+      }
+
+      return {
+        activeKey,
+        dingLiClick,
+        prefixCls,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .ding-ding-container {
+    border-radius: 4px;
+    height: calc(100% - 80px);
+    margin: 16px;
+  }
+  .ding-header {
+    align-items: center;
+    /*begin 兼容暗夜模式*/
+    border-bottom: 1px solid @border-color-base;
+    /*end 兼容暗夜模式*/
+    box-sizing: border-box;
+    display: flex;
+    height: 50px;
+    justify-content: space-between;
+    padding: 0 24px;
+
+    ul {
+      margin-bottom: 0;
+    }
+  }
+  .ding-menu-tab {
+    display: flex;
+    height: 100%;
+
+    li {
+      align-items: center;
+      border-bottom: 2px solid transparent;
+      display: flex;
+      height: 100%;
+      margin-right: 38px;
+
+      a {
+        /*begin 兼容暗夜模式*/
+        color: @text-color !important;
+        /*end 兼容暗夜模式*/
+        font-size: 15px;
+        font-weight: 700;
+      }
+    }
+  }
+  .active {
+    border-bottom-color: #2196f3 !important;
+
+    a {
+      color: #333 !important;
+    }
+  }
+  .empty-image{
+    align-items: center;
+    display: flex;
+    flex-direction: column;
+    height: calc(100% - 50px);
+    justify-content: center;
+    width: 100%;
+  }
+</style>
+
+<style lang="less">
+  /* update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效*/
+  @prefix-cls: ~'@{namespace}-j-dd-container';
+  /*begin 兼容暗夜模式*/
+  .@{prefix-cls} {
+    background: @component-background;
+
+    .ding-header {
+      border-bottom: 1px solid @border-color-base;
+    }
+
+    .ding-menu-tab {
+      li {
+        a {
+          color: @text-color !important;
+        }
+      }
+    }
+
+    .ant-collapse-borderless {
+      background-color: @component-background;
+    }
+
+    .ant-collapse{
+      background-color: @component-background;
+    }
+  }
+  /*end 兼容暗夜模式*/
+/* update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效*/
+</style>

+ 69 - 0
src/views/system/appconfig/ThirdAppConfigModal.vue

@@ -0,0 +1,69 @@
+<template>
+  <BasicModal @register="registerModal" :width="800" :title="title" @ok="handleSubmit">
+    <BasicForm @register="registerForm" />
+  </BasicModal>
+</template>
+
+<script lang="ts">
+  import { defineComponent, ref } from 'vue';
+  import { BasicModal, useModalInner } from '/@/components/Modal';
+  import { useForm, BasicForm } from '/@/components/Form';
+  import { thirdAppFormSchema } from './ThirdApp.data';
+  import { getThirdConfigByTenantId, saveOrUpdateThirdConfig } from './ThirdApp.api';
+  export default defineComponent({
+    name: 'ThirdAppConfigModal',
+    components: { BasicModal, BasicForm },
+    setup(props, { emit }) {
+      const title = ref<string>('钉钉配置');
+      //表单配置
+      const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
+        schemas: thirdAppFormSchema,
+        showActionButtonGroup: false,
+        labelCol: { span: 24 },
+        wrapperCol: { span: 24 },
+      });
+      //表单赋值
+      const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+        setModalProps({ confirmLoading: true });
+        if (data.thirdType == 'dingtalk') {
+          title.value = '钉钉配置';
+        } else {
+          title.value = '企业微信配置';
+        }
+        //重置表单
+        await resetFields();
+        let values = await getThirdConfigByTenantId({ tenantId: data.tenantId, thirdType: data.thirdType });
+        setModalProps({ confirmLoading: false });
+        //表单赋值
+        if (values) {
+          await setFieldsValue(values);
+        } else {
+          await setFieldsValue(data);
+        }
+      });
+
+      /**
+       * 第三方配置点击事件
+       */
+      async function handleSubmit() {
+        let values = await validate();
+        let isUpdate = false;
+        if (values.id) {
+          isUpdate = true;
+        }
+        await saveOrUpdateThirdConfig(values, isUpdate);
+        emit('success');
+        closeModal();
+      }
+
+      return {
+        title,
+        registerForm,
+        registerModal,
+        handleSubmit,
+      };
+    },
+  });
+</script>
+
+<style scoped></style>

+ 303 - 0
src/views/system/appconfig/ThirdAppDingTalkConfigForm.vue

@@ -0,0 +1,303 @@
+<template>
+  <div class="base-collapse">
+    <div class="header"> 钉钉集成 </div>
+    <a-collapse expand-icon-position="right" :bordered="false">
+      <a-collapse-panel key="1">
+        <template #header>
+          <div style="font-size: 16px"> 1.获取对接信息</div>
+        </template>
+        <div class="base-desc">从钉钉开放平台获取对接信息,即可开始集成以及同步通讯录</div>
+        <div style="margin-top: 5px">
+          <a href='https://help.qiaoqiaoyun.com/expand/dingding.html' target='_blank'>如何获取对接信息?</a>
+        </div>
+      </a-collapse-panel>
+    </a-collapse>
+    <div class="sync-padding">
+      <a-collapse expand-icon-position="right" :bordered="false">
+        <a-collapse-panel key="2">
+          <template #header>
+            <div style="width: 100%; justify-content: space-between; display: flex">
+              <div style="font-size: 16px"> 2.对接信息录入</div>
+            </div>
+          </template>
+          <div class="base-desc">完成步骤1后,填入Agentld、 AppKey、AppSecret后 可对接应用与同步通讯录</div>
+          <div class="flex-flow">
+            <div class="base-title">CorpId</div>
+            <div class="base-message">
+              <a-input-password v-model:value="appConfigData.corpId" readonly />
+            </div>
+          </div>
+          <div class="flex-flow">
+            <div class="base-title">Agentld</div>
+            <div class="base-message">
+              <a-input-password v-model:value="appConfigData.agentId" readonly />
+            </div>
+          </div>
+          <div class="flex-flow">
+            <div class="base-title">AppKey</div>
+            <div class="base-message">
+              <a-input-password v-model:value="appConfigData.clientId" readonly />
+            </div>
+          </div>
+          <div class="flex-flow">
+            <div class="base-title">AppSecret</div>
+            <div class="base-message">
+              <a-input-password v-model:value="appConfigData.clientSecret" readonly />
+            </div>
+          </div>
+          <div style="margin-top: 20px; width: 100%; text-align: right">
+            <a-button @click="dingEditClick">编辑</a-button>
+          </div>
+        </a-collapse-panel>
+      </a-collapse>
+      <div class="sync-padding">
+        <div style="font-size: 16px; width: 100%"> 3.数据同步</div>
+        <div style="margin-top: 20px" class="base-desc">
+          从钉钉同步到本地
+          <ul style='list-style-type: disc;margin-left: 20px;'>
+            <li>同步部门到本地</li>
+            <li>
+              同步部门下的用户到本地
+              <a-tooltip title='同步用户与部门文档'>
+                <a-icon @click='handleIconClick' type="question-circle" class="sync-text"/>
+              </a-tooltip>
+            </li>
+          </ul>
+          <div style="float: right">
+            <a-button :loading="btnLoading" @click="syncDingTalk">{{ !btnLoading ? '同步' : '同步中' }}</a-button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <ThirdAppConfigModal @register="registerAppConfigModal" @success="handleSuccess" />
+</template>
+
+<script lang="ts">
+  import { defineComponent, h, inject, onMounted, reactive, ref, watch } from 'vue';
+  import { getThirdConfigByTenantId, syncDingTalkDepartUserToLocal } from './ThirdApp.api';
+  import { useModal } from '/@/components/Modal';
+  import ThirdAppConfigModal from './ThirdAppConfigModal.vue';
+  import { Modal } from 'ant-design-vue';
+  import { getTenantId } from '/@/utils/auth';
+  import { useMessage } from '/@/hooks/web/useMessage';
+
+  export default defineComponent({
+    name: 'OrganDingConfigForm',
+    components: {
+      ThirdAppConfigModal,
+    },
+    setup() {
+      const { createMessage } = useMessage();
+      //折叠面板选中key
+      const collapseActiveKey = ref<string>('');
+      //按钮加载事件
+      const btnLoading = ref<boolean>(false);
+      //第三方配置数据
+      const appConfigData = ref<any>({
+        agentId: undefined,
+        clientId: '',
+        clientSecret: '',
+      });
+
+      //企业微信钉钉配置modal
+      const [registerAppConfigModal, { openModal }] = useModal();
+
+      /**
+       * 钉钉编辑
+       */
+      async function dingEditClick() {
+        let tenantId = getTenantId();
+        openModal(true, {
+          tenantId: tenantId,
+          thirdType: 'dingtalk',
+        });
+      }
+
+      /**
+       * 初始化第三方数据
+       */
+      async function initThirdAppConfigData(params) {
+        let values = await getThirdConfigByTenantId(params);
+        if (values) {
+          appConfigData.value = values;
+        }
+      }
+
+      /**
+       * 成功回调
+       */
+      function handleSuccess() {
+        let tenantId = getTenantId();
+        initThirdAppConfigData({ tenantId: tenantId, thirdType: 'dingtalk' });
+      }
+
+      /**
+       * 同步钉钉
+       */
+      async function syncDingTalk() {
+        btnLoading.value = true;
+        await syncDingTalkDepartUserToLocal()
+          .then((res) => {
+            let options = {};
+            if (res.result) {
+              options = {
+                width: 600,
+                title: res.message,
+                content: () => {
+                  let nodes;
+                  let successInfo = [`成功信息如下:`, renderTextarea(h, res.result.successInfo.map((v, i) => `${i + 1}. ${v}`).join('\n'))];
+                  if (res.success) {
+                    nodes = [...successInfo, h('br'), `无失败信息!`];
+                  } else {
+                    nodes = [
+                      `失败信息如下:`,
+                      renderTextarea(h, res.result.failInfo.map((v, i) => `${i + 1}. ${v}`).join('\n')),
+                      h('br'),
+                      ...successInfo,
+                    ];
+                  }
+                  return nodes;
+                },
+              };
+            }
+            if (res.success) {
+              if (options != null) {
+                Modal.success(options);
+              } else {
+                createMessage.warning(res.message);
+              }
+            } else {
+              if (options && options.title) {
+                Modal.warning(options)
+              } else {
+                createMessage.warning({
+                  content: res.message || "同步失败,请检查对接信息录入中是否填写正确,并确认是否已开启钉钉配置!",
+                  duration: 5
+                });
+              }
+            }
+          })
+          .finally(() => {
+            btnLoading.value = false;
+          });
+      }
+
+      /**
+       * 渲染文本
+       * @param h
+       * @param value
+       */
+      function renderTextarea(h, value) {
+        return h(
+          'div',
+          {
+            id: 'box',
+            style: {
+              minHeight: '100px',
+              border: '1px solid #d9d9d9',
+              fontSize: '14px',
+              maxHeight: '250px',
+              whiteSpace: 'pre',
+              overflow: 'auto',
+              padding: '10px',
+            },
+          },
+          value
+        );
+      }
+
+      /**
+       * 钉钉同步文档
+       */
+      function handleIconClick(){
+        window.open("https://help.qiaoqiaoyun.com/expand/dingdingsyn.html","_target")
+      }
+      
+      onMounted(() => {
+        let tenantId = getTenantId();
+        initThirdAppConfigData({ tenantId: tenantId, thirdType: 'dingtalk' });
+      });
+
+      return {
+        appConfigData,
+        collapseActiveKey,
+        registerAppConfigModal,
+        dingEditClick,
+        handleSuccess,
+        syncDingTalk,
+        btnLoading,
+        handleIconClick,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .header {
+    align-items: center;
+    box-sizing: border-box;
+    display: flex;
+    height: 50px;
+    justify-content: space-between;
+    font-weight: 700;
+    font-size: 18px;
+    color: @text-color;
+  }
+
+  .flex-flow {
+    display: flex;
+    min-height: 0;
+  }
+
+  .sync-padding {
+    padding: 12px 0 16px;
+    color: @text-color;
+  }
+
+  .base-collapse {
+    margin-top: 20px;
+    padding: 0 24px;
+    font-size: 20px;
+
+    .base-desc {
+      font-size: 14px;
+    }
+
+    .base-title {
+      width: 100px;
+      text-align: left;
+      height: 50px;
+      line-height: 50px;
+    }
+
+    .base-message {
+      width: 100%;
+      height: 50px;
+      line-height: 50px;
+    }
+
+    :deep(.ant-collapse-header) {
+      padding: 12px 0 16px;
+    }
+
+    :deep(.ant-collapse-content-box) {
+      padding-left: 0;
+    }
+  }
+  /*begin 兼容暗夜模式*/
+  //暗黑模式下卡片的边框设置成none
+  [data-theme='dark'] .base-collapse .ant-collapse{
+    border: none !important;
+  }
+  /*end 兼容暗夜模式*/
+  /*文档按钮问号样式*/
+  .sync-text{
+    margin-left: 2px;
+    cursor: pointer;
+    position: relative;
+    top: 2px
+  }
+ :deep(.ant-collapse-borderless >.ant-collapse-item:last-child) {border-bottom-width:1px;}
+</style>

+ 249 - 0
src/views/system/appconfig/ThirdAppWeEnterpriseConfigForm.vue

@@ -0,0 +1,249 @@
+<template>
+  <div class="base-collapse">
+    <div class="header"> 企业微信集成 </div>
+    <a-collapse expand-icon-position="right" :bordered="false">
+      <a-collapse-panel key="1">
+        <template #header>
+          <div style="font-size: 16px"> 1.获取对接信息</div>
+        </template>
+        <div class="base-desc">从企业微信平台获取对接信息,即可开始集成以及同步通讯录</div>
+        <div style="margin-top: 5px">
+          <a href="https://help.qiaoqiaoyun.com/expand/dingding.html" target="_blank">如何获取对接信息?</a>
+        </div>
+      </a-collapse-panel>
+    </a-collapse>
+    <div>
+      <a-collapse expand-icon-position="right" :bordered="false">
+        <a-collapse-panel key="2">
+          <template #header>
+            <div style="width: 100%; justify-content: space-between; display: flex">
+              <div style="font-size: 16px"> 2.对接信息录入</div>
+            </div>
+          </template>
+          <div class="flex-flow">
+            <div class="base-title">Agentld</div>
+            <div class="base-message">
+              <a-input-password v-model:value="appConfigData.agentId" readonly />
+            </div>
+          </div>
+          <div class="flex-flow">
+            <div class="base-title">AppKey</div>
+            <div class="base-message">
+              <a-input-password v-model:value="appConfigData.clientId" readonly />
+            </div>
+          </div>
+          <div class="flex-flow">
+            <div class="base-title">AppSecret</div>
+            <div class="base-message">
+              <a-input-password v-model:value="appConfigData.clientSecret" readonly />
+            </div>
+          </div>
+          <div style="margin-top: 20px; width: 100%; text-align: right">
+            <a-button @click="weEnterpriseEditClick">编辑</a-button>
+          </div>
+        </a-collapse-panel>
+      </a-collapse>
+      <div class="sync-padding">
+        <div style="font-size: 16px; width: 100%"> 3.数据同步</div>
+        <div style="margin-top: 20px" class="base-desc">
+          从企业微信同步到敲敲云
+          <a style="margin-left: 10px" @click="seeBindWeChat">查看已绑定的企业微信用户</a>
+          <div style="float: right">
+            <a-button @loading="btnLoading" @click="thirdUserByWechat">同步</a-button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <ThirdAppConfigModal @register="registerAppConfigModal" @success="handleSuccess" />
+  <ThirdAppBindWeEnterpriseModal @register="registerBindAppConfigModal" @success="handleBindSuccess" />
+</template>
+
+<script lang="ts">
+  import { defineComponent, onMounted, ref } from 'vue';
+  import { getThirdConfigByTenantId } from './ThirdApp.api';
+  import ThirdAppConfigModal from './ThirdAppConfigModal.vue';
+  import { useModal } from '/@/components/Modal';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  import { getTenantId } from '@/utils/auth';
+  import ThirdAppBindWeEnterpriseModal from './ThirdAppBindWeEnterpriseModal.vue';
+  import { Modal } from "ant-design-vue";
+
+  export default defineComponent({
+    name: 'ThirdAppWeEnterpriseConfigForm',
+    components: {
+      ThirdAppConfigModal,
+      ThirdAppBindWeEnterpriseModal,
+    },
+    setup() {
+      const btnLoading = ref<boolean>(false);
+      //第三方配置数据
+      const appConfigData = ref<any>({
+        agentId: '',
+        clientId: '',
+        clientSecret: '',
+      });
+      //企业微信钉钉配置modal
+      const [registerAppConfigModal, { openModal }] = useModal();
+      const [registerBindAppConfigModal, { openModal: openBindModal }] = useModal();
+      const { createMessage } = useMessage();
+
+      /**
+       * 初始化数据
+       *
+       * @param params
+       */
+      async function initThirdAppConfigData(params) {
+        let values = await getThirdConfigByTenantId(params);
+        if (values) {
+          appConfigData.value = values;
+        }
+      }
+
+      /**
+       * 企业微信编辑
+       */
+      async function weEnterpriseEditClick() {
+        let tenantId = getTenantId();
+        openModal(true, {
+          tenantId: tenantId,
+          thirdType: 'wechat_enterprise',
+        });
+      }
+
+      /**
+       * 获取企业微信绑定的用户
+       */
+      async function thirdUserByWechat() {
+        openBindModal(true, { izBind: false });
+      }
+
+      /**
+       * 成功回调
+       */
+      function handleSuccess() {
+        let tenantId = getTenantId();
+        initThirdAppConfigData({ tenantId: tenantId, thirdType: 'wechat_enterprise' });
+      }
+
+      /**
+       * 绑定成功返回值
+       *
+       * @param options
+       * @param item
+       */
+      function handleBindSuccess(options, item) {
+        console.log("options:::",options)
+        console.log("item:::",item)
+        if (item.success) {
+          if (options != null) {
+            Modal.success(options);
+          } else {
+            createMessage.warning(item.message);
+          }
+        } else {
+          if (options && options.title) {
+            Modal.warning(options);
+          } else {
+            createMessage.warning({
+              content: '同步失败,请检查对接信息录入中是否填写正确,并确认是否已开启企业微信配置!',
+              duration: 5,
+            });
+          }
+        }
+      }
+      
+      /**
+       * 查看已绑定的企业微信
+       */
+      function seeBindWeChat() {
+        openBindModal(true,{ izBind: true })
+      }
+      
+      onMounted(() => {
+        let tenantId = getTenantId();
+        initThirdAppConfigData({ tenantId: tenantId, thirdType: 'wechat_enterprise' });
+      });
+
+      return {
+        appConfigData,
+        weEnterpriseEditClick,
+        registerAppConfigModal,
+        registerBindAppConfigModal,
+        handleSuccess,
+        btnLoading,
+        thirdUserByWechat,
+        handleBindSuccess,
+        seeBindWeChat,
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .header {
+    align-items: center;
+    box-sizing: border-box;
+    display: flex;
+    height: 50px;
+    justify-content: space-between;
+    font-weight: 700;
+    font-size: 18px;
+    color: @text-color;
+  }
+
+  .flex-flow {
+    display: flex;
+    min-height: 0;
+  }
+
+  .sync-padding {
+    padding: 12px 0 16px;
+    color: @text-color;
+  }
+
+  .base-collapse {
+    margin-top: 20px;
+    padding: 0 24px;
+    font-size: 20px;
+
+    .base-desc {
+      font-size: 14px;
+      color: @text-color;
+    }
+
+    .base-title {
+      width: 100px;
+      text-align: left;
+      height: 50px;
+      line-height: 50px;
+    }
+
+    .base-message {
+      width: 100%;
+      height: 50px;
+      line-height: 50px;
+    }
+
+    :deep(.ant-collapse-header) {
+      padding: 12px 0 16px;
+    }
+
+    :deep(.ant-collapse-content-box) {
+      padding-left: 0;
+    }
+  }
+  /*begin 兼容暗夜模式*/
+  //暗黑模式下卡片的边框设置成none
+  [data-theme='dark'] .base-collapse .ant-collapse {
+    border: none !important;
+  }
+  /*end 兼容暗夜模式*/
+  /*文档按钮问号样式*/
+  .sync-text {
+    margin-left: 2px;
+    cursor: pointer;
+    position: relative;
+    top: 2px;
+  }
+</style>

+ 1 - 1
src/views/system/loginmini/MiniCodelogin.vue

@@ -4,7 +4,7 @@
       <div class="aui-form">
         <div class="aui-image">
           <div class="aui-image-text">
-            <!-- <img :src="adTextImg" alt="" /> -->
+            <img :src="adTextImg" alt="" />
           </div>
         </div>
         <div class="aui-formBox aui-formEwm">

+ 29 - 29
src/views/system/loginmini/MiniForgotpad.vue

@@ -4,7 +4,7 @@
       <div class="aui-form">
         <div class="aui-image">
           <div class="aui-image-text">
-            <!-- <img :src="adTextImg" alt="" /> -->
+            <img :src="adTextImg" alt="" />
           </div>
         </div>
         <div class="aui-formBox">
@@ -13,19 +13,19 @@
               <div class="aui-step-item" :class="activeKey === 1 ? 'activeStep' : ''">
                 <div class="aui-step-tags">
                   <em>1</em>
-                  <p>{{ t('sys.login.authentication') }}</p>
+                  <p>{{t('sys.login.authentication')}}</p>
                 </div>
               </div>
               <div class="aui-step-item" :class="activeKey === 2 ? 'activeStep' : ''">
                 <div class="aui-step-tags">
                   <em>2</em>
-                  <p>{{ t('sys.login.resetLoginPassword') }}</p>
+                  <p>{{t('sys.login.resetLoginPassword')}}</p>
                 </div>
               </div>
               <div class="aui-step-item" :class="activeKey === 3 ? 'activeStep' : ''">
                 <div class="aui-step-tags">
                   <em>3</em>
-                  <p>{{ t('sys.login.resetSuccess') }}</p>
+                  <p>{{t('sys.login.resetSuccess')}}</p>
                 </div>
               </div>
             </div>
@@ -42,8 +42,8 @@
                     <a-form-item>
                       <a-input type="text" :placeholder="t('sys.login.smsCode')" v-model:value="formData.smscode" />
                     </a-form-item>
-                    <div v-if="showInterval" class="aui-code-line" @click="getLoginCode">{{ t('component.countdown.normalText') }}</div>
-                    <div v-else class="aui-code-line">{{ t('component.countdown.sendText', [unref(timeRuning)]) }}</div>
+                    <div v-if="showInterval" class="aui-code-line" @click="getLoginCode">{{t('component.countdown.normalText')}}</div>
+                    <div v-else class="aui-code-line">{{t('component.countdown.sendText',[unref(timeRuning)])}}</div>
                   </div>
                 </div>
                 <!-- 身份验证 end -->
@@ -64,21 +64,21 @@
                 </div>
                 <!-- 重置密码 end -->
               </a-form>
-              <!-- 重置成功 begin -->
-              <div class="aui-success" v-else>
-                <div class="aui-success-icon">
-                  <img :src="successImg" />
+                <!-- 重置成功 begin -->
+                <div class="aui-success" v-else>
+                  <div class="aui-success-icon">
+                    <img :src="successImg"/>
+                  </div>
+                  <h3>恭喜您,重置密码成功!</h3>
                 </div>
-                <h3>恭喜您,重置密码成功!</h3>
-              </div>
-              <!-- 重置成功 end -->
+                <!-- 重置成功 end -->
             </div>
             <div class="aui-formButton" style="padding-bottom: 40px">
               <div class="aui-flex" v-if="activeKey === 1 || activeKey === 2">
-                <a class="aui-link-login aui-flex-box" @click="nextStepClick">{{ t('sys.login.nextStep') }}</a>
+                <a class="aui-link-login aui-flex-box" @click="nextStepClick">{{t('sys.login.nextStep')}}</a>
               </div>
               <div class="aui-flex" v-else>
-                <a class="aui-linek-code aui-flex-box" @click="toLogin">{{ t('sys.login.goToLogin') }}</a>
+                <a class="aui-linek-code aui-flex-box" @click="toLogin">{{t('sys.login.goToLogin')}}</a>
               </div>
               <div class="aui-flex">
                 <a class="aui-linek-code aui-flex-box" @click="goBack"> {{ t('sys.login.backSignIn') }}</a>
@@ -98,12 +98,12 @@
   import { SmsEnum, useFormRules, useFormValid, useLoginState } from '/@/views/sys/login/useLogin';
   import { useMessage } from '/@/hooks/web/useMessage';
   import { getCaptcha, passwordChange, phoneVerify } from '/@/api/sys/user';
-  import logoImg from '/@/assets/loginmini/icon/jeecg_logo.png';
-  import adTextImg from '/@/assets/loginmini/icon/jeecg_ad_text.png';
-  import successImg from '/@/assets/loginmini/icon/icon-success.png';
+  import logoImg from '/@/assets/loginmini/icon/jeecg_logo.png'
+  import adTextImg from '/@/assets/loginmini/icon/jeecg_ad_text.png'
+  import successImg from '/@/assets/loginmini/icon/icon-success.png'
   import CaptchaModal from '@/components/jeecg/captcha/CaptchaModal.vue';
-  import { useModal } from '@/components/Modal';
-  import { ExceptionEnum } from '@/enums/exceptionEnum';
+  import { useModal } from "@/components/Modal";
+  import { ExceptionEnum } from "@/enums/exceptionEnum";
   const [captchaRegisterModal, { openModal: openCaptchaModal }] = useModal();
 
   //下一步控制
@@ -158,9 +158,9 @@
         smscode: formData.smscode,
       });
       activeKey.value = 2;
-      setTimeout(() => {
+      setTimeout(()=>{
         pwdFormRef.value.resetFields();
-      }, 300);
+      },300)
     } else {
       notification.error({
         message: '错误提示',
@@ -242,8 +242,8 @@
       return;
     }
     //update-begin---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP,1分钟超过5次短信,则提示需要验证码---
-    const result = await getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.FORGET_PASSWORD }).catch((res) => {
-      if (res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE) {
+    const result = await getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.FORGET_PASSWORD }).catch((res) =>{
+      if(res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE){
         openCaptchaModal(true, {});
       }
     });
@@ -274,14 +274,14 @@
     Object.assign(formData, { phone: '', smscode: '' });
     Object.assign(pwdFormData, { password: '', confirmPassword: '' });
     Object.assign(accountInfo, {});
-    if (unref(timer)) {
+    if(unref(timer)){
       clearInterval(unref(timer));
       timer.value = null;
       showInterval.value = true;
     }
-    setTimeout(() => {
+    setTimeout(()=>{
       formRef.value.resetFields();
-    }, 300);
+    },300)
   }
 
   defineExpose({
@@ -289,6 +289,6 @@
   });
 </script>
 <style lang="less" scoped>
-  @import '/@/assets/loginmini/style/home.less';
-  @import '/@/assets/loginmini/style/base.less';
+@import '/@/assets/loginmini/style/home.less';
+@import '/@/assets/loginmini/style/base.less';
 </style>

+ 27 - 19
src/views/system/loginmini/MiniLogin.vue

@@ -1,16 +1,15 @@
 <template>
   <div :class="prefixCls" class="login-background-img">
-    <AppLocalePicker class="absolute top-4 right-4 enter-x xl:text-gray-600" :showText="false" />
     <AppDarkModeToggle class="absolute top-3 right-7 enter-x" />
     <div class="aui-logo" v-if="!getIsMobile">
       <div>
         <h3>
-          <img :src="logoImg" alt="jeecg" />
+          <!-- <img :src="logoImg" alt="jeecg" /> -->
         </h3>
       </div>
     </div>
     <div v-else class="aui-phone-logo">
-      <img :src="logoImg" alt="jeecg" />
+      <!-- <img :src="logoImg" alt="jeecg" /> -->
     </div>
     <div v-show="type === 'login'">
       <div class="aui-content">
@@ -99,15 +98,6 @@
     <div v-show="type === 'forgot'" :class="`${prefixCls}-form`">
       <MiniForgotpad ref="forgotRef" @go-back="goBack" @success="handleSuccess" />
     </div>
-    <div v-show="type === 'register'" :class="`${prefixCls}-form`">
-      <MiniRegister ref="registerRef" @go-back="goBack" @success="handleSuccess" />
-    </div>
-    <div v-show="type === 'codeLogin'" :class="`${prefixCls}-form`">
-      <MiniCodelogin ref="codeRef" @go-back="goBack" @success="handleSuccess" />
-    </div>
-    <!-- 第三方登录相关弹框 -->
-    <ThirdModal ref="thirdModalRef"></ThirdModal>
-
     <!-- 图片验证码弹窗 -->
     <CaptchaModal @register="captchaRegisterModal" @ok="getLoginCode" />
   </div>
@@ -120,15 +110,13 @@
   import { useMessage } from '/@/hooks/web/useMessage';
   import { useI18n } from '/@/hooks/web/useI18n';
   import { SmsEnum } from '/@/views/sys/login/useLogin';
-  import ThirdModal from '/@/views/sys/login/ThirdModal.vue';
   import MiniForgotpad from './MiniForgotpad.vue';
-  import MiniRegister from './MiniRegister.vue';
-  import MiniCodelogin from './MiniCodelogin.vue';
-  import logoImg from '/@/assets/loginmini/icon/jeecg_logo.png';
+  import { AppDarkModeToggle } from '/@/components/Application';
   import { useDesign } from '/@/hooks/web/useDesign';
   import { useAppInject } from '/@/hooks/web/useAppInject';
+  import CaptchaModal from '@/components/jeecg/captcha/CaptchaModal.vue';
   import { useModal } from '@/components/Modal';
-  import { ExceptionEnum } from '/@/enums/exceptionEnum';
+  import { ExceptionEnum } from '@/enums/exceptionEnum';
 
   const { prefixCls } = useDesign('mini-login');
   const { notification, createMessage } = useMessage();
@@ -158,7 +146,6 @@
   //第三方登录弹窗
   const thirdModalRef = ref();
   //扫码登录
-  const codeRef = ref();
   //是否显示获取验证码
   const showInterval = ref<boolean>(true);
   //60s
@@ -168,7 +155,6 @@
   //忘记密码
   const forgotRef = ref();
   //注册
-  const registerRef = ref();
   const loginLoading = ref<boolean>(false);
   const { getIsMobile } = useAppInject();
   const [captchaRegisterModal, { openModal: openCaptchaModal }] = useModal();
@@ -320,6 +306,14 @@
     }
   }
 
+  /**
+   * 第三方登录
+   * @param type
+   */
+  function onThirdLogin(type) {
+    thirdModalRef.value.onThirdLogin(type);
+  }
+
   /**
    * 忘记密码
    */
@@ -363,6 +357,7 @@
   :deep(.ant-input:focus) {
     box-shadow: none;
   }
+
   .aui-get-code {
     float: right;
     position: relative;
@@ -389,6 +384,7 @@
     position: absolute;
     margin-right: 10px;
   }
+
   .aui-link-login {
     height: 42px;
     padding: 10px 15px;
@@ -399,6 +395,7 @@
     flex: 1;
     color: #fff;
   }
+
   .aui-phone-logo {
     position: absolute;
     margin-left: 10px;
@@ -406,6 +403,7 @@
     top: 2px;
     z-index: 4;
   }
+
   .top-3 {
     top: 0.45rem;
   }
@@ -423,9 +421,11 @@
       &::before {
         background-image: url(/@/assets/svg/login-bg-dark.svg);
       }
+
       .aui-inputClear {
         background-color: #232a3b !important;
       }
+
       .ant-input,
       .ant-input-password {
         background-color: #232a3b !important;
@@ -442,6 +442,7 @@
       .app-iconify {
         color: #fff !important;
       }
+
       .aui-inputClear input,
       .aui-input-line input,
       .aui-choice {
@@ -451,9 +452,11 @@
       .aui-formBox {
         background-color: @dark-bg !important;
       }
+
       .aui-third-text span {
         background-color: @dark-bg !important;
       }
+
       .aui-form-nav .aui-flex-box {
         color: #c9d1d9 !important;
       }
@@ -462,13 +465,16 @@
         background: @dark-bg !important;
         color: white !important;
       }
+
       .aui-code-line {
         border-left: none !important;
       }
+
       .ant-checkbox-inner,
       .aui-success h3 {
         border-color: #c9d1d9;
       }
+
       //update-begin---author:wangshuai ---date:20230828  for:【QQYUN-6363】这个样式代码有问题,不在里面,导致表达式有问题------------
       &-sign-in-way {
         .anticon {
@@ -481,6 +487,7 @@
           }
         }
       }
+
       //update-end---author:wangshuai ---date:20230828  for:【QQYUN-6363】这个样式代码有问题,不在里面,导致表达式有问题------------
     }
 
@@ -494,6 +501,7 @@
       font-size: 12px !important;
       color: @text-color-secondary !important;
     }
+
     .aui-third-login a {
       background: transparent;
     }

+ 0 - 288
src/views/system/loginmini/MiniRegister.vue

@@ -1,288 +0,0 @@
-<template>
-  <div class="aui-content">
-    <div class="aui-container">
-      <div class="aui-form">
-        <div class="aui-image">
-          <div class="aui-image-text">
-            <!-- <img :src="jeecgAdTextImg" alt="" /> -->
-          </div>
-        </div>
-        <div class="aui-formBox">
-          <div class="aui-formWell">
-            <a-form ref="formRef" :model="formData">
-              <div class="aui-flex aui-form-nav aui-clear-left" style="padding-bottom: 21px">
-                <div class="aui-flex-box activeNav on">{{ t('sys.login.signUpFormTitle') }}</div>
-              </div>
-              <div class="aui-form-box">
-                <div class="aui-account aui-account-line">
-                  <a-form-item>
-                    <div class="aui-input-line">
-                      <Icon class="aui-icon" icon="ant-design:user-outlined" />
-                      <a-input class="fix-auto-fill" type="text" :placeholder="t('sys.login.userName')" v-model:value="formData.username" />
-                    </div>
-                  </a-form-item>
-                  <a-form-item>
-                    <div class="aui-input-line">
-                      <Icon class="aui-icon" icon="ant-design:mobile-outlined" />
-                      <a-input class="fix-auto-fill" type="text" :placeholder="t('sys.login.mobile')" v-model:value="formData.mobile" />
-                    </div>
-                  </a-form-item>
-                  <a-form-item>
-                    <div class="aui-input-line">
-                      <Icon class="aui-icon" icon="ant-design:mail-outlined" />
-                      <a-input class="fix-auto-fill" type="text" :placeholder="t('sys.login.smsCode')" v-model:value="formData.smscode" />
-                      <div v-if="showInterval" class="aui-code-line" @click="getLoginCode">{{ t('component.countdown.normalText') }}</div>
-                      <div v-else class="aui-code-line">{{ t('component.countdown.sendText', [unref(timeRuning)]) }}</div>
-                    </div>
-                  </a-form-item>
-                  <a-form-item>
-                    <div class="aui-input-line">
-                      <Icon class="aui-icon" icon="ant-design:lock-outlined" />
-                      <a-input
-                        class="fix-auto-fill"
-                        :type="pwdIndex === 'close' ? 'password' : 'text'"
-                        :placeholder="t('sys.login.password')"
-                        v-model:value="formData.password"
-                      />
-                      <div class="aui-eye">
-                        <img :src="eyeKImg" alt="开启" v-if="pwdIndex === 'open'" @click="pwdClick('close')" />
-                        <img :src="eyeGImg" alt="关闭" v-else-if="pwdIndex === 'close'" @click="pwdClick('open')" />
-                      </div>
-                    </div>
-                  </a-form-item>
-                  <a-form-item>
-                    <div class="aui-input-line">
-                      <Icon class="aui-icon" icon="ant-design:lock-outlined" />
-                      <a-input
-                        class="fix-auto-fill"
-                        :type="confirmPwdIndex === 'close' ? 'password' : 'text'"
-                        :placeholder="t('sys.login.confirmPassword')"
-                        v-model:value="formData.confirmPassword"
-                      />
-                      <div class="aui-eye">
-                        <img :src="eyeKImg" alt="开启" v-if="confirmPwdIndex === 'open'" @click="confirmPwdClick('close')" />
-                        <img :src="eyeGImg" alt="关闭" v-else-if="confirmPwdIndex === 'close'" @click="confirmPwdClick('open')" />
-                      </div>
-                    </div>
-                  </a-form-item>
-                  <a-form-item name="policy">
-                    <div class="aui-flex">
-                      <div class="aui-flex-box">
-                        <div class="aui-choice">
-                          <a-checkbox v-model:checked="formData.policy" />
-                          <span style="color: #1b90ff; margin-left: 4px">{{ t('sys.login.policy') }}</span>
-                        </div>
-                      </div>
-                    </div>
-                  </a-form-item>
-                </div>
-              </div>
-              <div class="aui-formButton">
-                <div class="aui-flex">
-                  <a class="aui-link-login aui-flex-box" @click="registerHandleClick"> {{ t('sys.login.registerButton') }}</a>
-                </div>
-                <div class="aui-flex">
-                  <a class="aui-linek-code aui-flex-box" @click="goBackHandleClick">{{ t('sys.login.backSignIn') }}</a>
-                </div>
-              </div>
-            </a-form>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-  <!-- 图片验证码弹窗 -->
-  <CaptchaModal @register="captchaRegisterModal" @ok="getLoginCode" />
-</template>
-
-<script lang="ts" setup name="mini-register">
-  import { ref, reactive, unref, toRaw } from 'vue';
-  import { getCaptcha, register } from '/@/api/sys/user';
-  import { SmsEnum } from '/@/views/sys/login/useLogin';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import logoImg from '/@/assets/loginmini/icon/jeecg_logo.png';
-  import jeecgAdTextImg from '/@/assets/loginmini/icon/jeecg_ad_text.png';
-  import eyeKImg from '/@/assets/loginmini/icon/icon-eye-k.png';
-  import eyeGImg from '/@/assets/loginmini/icon/icon-eye-g.png';
-  import { useI18n } from '/@/hooks/web/useI18n';
-  import CaptchaModal from '@/components/jeecg/captcha/CaptchaModal.vue';
-  import { useModal } from '@/components/Modal';
-  import { ExceptionEnum } from '@/enums/exceptionEnum';
-
-  const { t } = useI18n();
-  const { notification, createErrorModal, createMessage } = useMessage();
-  const emit = defineEmits(['go-back', 'success', 'register']);
-  const formRef = ref();
-  const formData = reactive<any>({
-    username: '',
-    mobile: '',
-    smscode: '',
-    password: '',
-    confirmPassword: '',
-    policy: false,
-  });
-  //是否显示获取验证码
-  const showInterval = ref<boolean>(true);
-  //60s
-  const timeRuning = ref<number>(60);
-  //定时器
-  const timer = ref<any>(null);
-  //密码眼睛打开关闭
-  const pwdIndex = ref<string>('close');
-  //确认密码眼睛打开关闭
-  const confirmPwdIndex = ref<string>('close');
-  const [captchaRegisterModal, { openModal: openCaptchaModal }] = useModal();
-
-  /**
-   * 返回
-   */
-  function goBackHandleClick() {
-    emit('go-back');
-    initForm();
-  }
-
-  /**
-   * 获取手机验证码
-   */
-  async function getLoginCode() {
-    if (!formData.mobile) {
-      createMessage.warn(t('sys.login.mobilePlaceholder'));
-      return;
-    }
-    //update-begin---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP,1分钟超过5次短信,则提示需要验证码---
-    const result = await getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.REGISTER }).catch((res) => {
-      if (res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE) {
-        openCaptchaModal(true, {});
-      }
-    });
-    //update-end---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP,1分钟超过5次短信,则提示需要验证码---
-    if (result) {
-      const TIME_COUNT = 60;
-      if (!unref(timer)) {
-        timeRuning.value = TIME_COUNT;
-        showInterval.value = false;
-        timer.value = setInterval(() => {
-          if (unref(timeRuning) > 0 && unref(timeRuning) <= TIME_COUNT) {
-            timeRuning.value = timeRuning.value - 1;
-          } else {
-            showInterval.value = true;
-            clearInterval(unref(timer));
-            timer.value = null;
-          }
-        }, 1000);
-      }
-    }
-  }
-
-  function registerHandleClick() {
-    if (!formData.username) {
-      createMessage.warn(t('sys.login.accountPlaceholder'));
-      return;
-    }
-    if (!formData.mobile) {
-      createMessage.warn(t('sys.login.mobilePlaceholder'));
-      return;
-    }
-    if (!formData.smscode) {
-      createMessage.warn(t('sys.login.smsPlaceholder'));
-      return;
-    }
-    if (!formData.password) {
-      createMessage.warn(t('sys.login.passwordPlaceholder'));
-      return;
-    }
-    if (!formData.confirmPassword) {
-      createMessage.warn(t('sys.login.confirmPassword'));
-      return;
-    }
-    if (formData.password !== formData.confirmPassword) {
-      createMessage.warn(t('sys.login.diffPwd'));
-      return;
-    }
-    if (!formData.policy) {
-      createMessage.warn(t('sys.login.policyPlaceholder'));
-      return;
-    }
-    registerAccount();
-  }
-
-  /**
-   * 注册账号
-   */
-  async function registerAccount() {
-    try {
-      const resultInfo = await register(
-        toRaw({
-          username: formData.username,
-          password: formData.password,
-          phone: formData.mobile,
-          smscode: formData.smscode,
-        })
-      );
-      if (resultInfo && resultInfo.data.success) {
-        notification.success({
-          description: resultInfo.data.message || t('sys.api.registerMsg'),
-          duration: 3,
-        });
-        emit('success', { username: formData.username, password: formData.password });
-        initForm();
-      } else {
-        notification.warning({
-          message: t('sys.api.errorTip'),
-          description: resultInfo.data.message || t('sys.api.networkExceptionMsg'),
-          duration: 3,
-        });
-      }
-    } catch (error) {
-      notification.error({
-        message: t('sys.api.errorTip'),
-        description: error.message || t('sys.api.networkExceptionMsg'),
-        duration: 3,
-      });
-    }
-  }
-
-  /**
-   * 初始化表单
-   */
-  function initForm() {
-    Object.assign(formData, { username: '', mobile: '', smscode: '', password: '', confirmPassword: '', policy: false });
-    if (!unref(timer)) {
-      showInterval.value = true;
-      clearInterval(unref(timer));
-      timer.value = null;
-    }
-    formRef.value.resetFields();
-  }
-
-  /**
-   * 密码打开或关闭
-   * @param value
-   */
-  function pwdClick(value) {
-    pwdIndex.value = value;
-  }
-
-  /**
-   * 确认密码打开或关闭
-   * @param value
-   */
-  function confirmPwdClick(value) {
-    confirmPwdIndex.value = value;
-  }
-
-  defineExpose({
-    initForm,
-  });
-</script>
-<style lang="less" scoped>
-  @import '/@/assets/loginmini/style/home.less';
-  @import '/@/assets/loginmini/style/base.less';
-  .aui-input-line .aui-icon {
-    position: absolute;
-    z-index: 2;
-    top: 10px;
-    left: 10px;
-    font-size: 20px !important;
-  }
-</style>

+ 128 - 50
src/views/system/menu/MenuDrawer.vue

@@ -1,63 +1,141 @@
 <template>
-  <BasicDrawer v-bind="$attrs" @register="registerDrawer" showFooter :title="getTitle" width="50%" @ok="handleSubmit">
-    <BasicForm @register="registerForm" />
+  <BasicDrawer v-bind="$attrs" @register="registerDrawer" showFooter :width="adaptiveWidth" :title="getTitle" @ok="handleSubmit">
+    <BasicForm @register="registerForm" class="menuForm" />
   </BasicDrawer>
 </template>
-<script lang="ts">
-  import { defineComponent, ref, computed, unref } from 'vue';
+<script lang="ts" setup>
+  import { ref, computed, unref, useAttrs } from 'vue';
   import { BasicForm, useForm } from '/@/components/Form/index';
-  import { formSchema } from './menu.data';
+  import { formSchema, ComponentTypes } from './menu.data';
   import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+  import { list, saveOrUpdateMenu } from './menu.api';
+  import { useDrawerAdaptiveWidth } from '/@/hooks/jeecg/useAdaptiveWidth';
+  import { useI18n } from "/@/hooks/web/useI18n";
+  // 声明Emits
+  const emit = defineEmits(['success', 'register']);
+  const { adaptiveWidth } = useDrawerAdaptiveWidth();
+  const attrs = useAttrs();
+  const isUpdate = ref(true);
+  const menuType = ref(0);
+  const isButton = (type) => type === 2;
+  const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate }] = useForm({
+    labelCol: {
+      md: { span: 4 },
+      sm: { span: 6 },
+    },
+    wrapperCol: {
+      md: { span: 20 },
+      sm: { span: 18 },
+    },
+    schemas: formSchema,
+    showActionButtonGroup: false,
+  });
 
-  import { getMenuList } from '/@/api/demo/system';
+  const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
+    await resetFields();
+    setDrawerProps({ confirmLoading: false });
+    isUpdate.value = !!data?.isUpdate;
+    menuType.value = data?.record?.menuType;
 
-  export default defineComponent({
-    name: 'MenuDrawer',
-    components: { BasicDrawer, BasicForm },
-    emits: ['success', 'register'],
-    setup(_, { emit }) {
-      const isUpdate = ref(true);
+    //获取下拉树信息
+    const treeData = await list();
+    updateSchema([
+      {
+        field: 'parentId',
+        // update-begin--author:liaozhiyang---date:20240306---for:【QQYUN-8379】菜单管理页菜单国际化
+        componentProps: { treeData: translateMenu(treeData, 'name') },
+        // update-end--author:liaozhiyang---date:20240306---for:【QQYUN-8379】菜单管理页菜单国际化
+      },
+      {
+        field: 'name',
+        label: isButton(unref(menuType)) ? '按钮/权限' : '菜单名称',
+      },
+      {
+        field: 'url',
+        required: !isButton(unref(menuType)),
+        componentProps: {
+          onChange: (e) => onUrlChange(e.target.value),
+        },
+      },
+    ]);
 
-      const [registerForm, { resetFields, setFieldsValue, updateSchema, validate }] = useForm({
-        labelWidth: 100,
-        schemas: formSchema,
-        showActionButtonGroup: false,
-        baseColProps: { lg: 12, md: 24 },
-      });
+    // 无论新增还是编辑,都可以设置表单值
+    if (typeof data.record === 'object') {
+      let values = { ...data.record };
+      setFieldsValue(values);
+      onUrlChange(values.url);
+    }
+    //按钮类型情况下,编辑时候清除一下地址的校验
+    if (menuType.value == 2) {
+      clearValidate();
+    }
+    //禁用表单
+    setProps({ disabled: !attrs.showFooter });
+  });
+  //获取弹窗标题
+  const getTitle = computed(() => (!unref(isUpdate) ? '新增菜单' : '编辑菜单'));
+  //提交事件
+  async function handleSubmit() {
+    try {
+      const values = await validate();
+      // iframe兼容
+      if (ComponentTypes.IFrame === values.component) {
+        values.component = values.frameSrc;
+      }
+      setDrawerProps({ confirmLoading: true });
+      //提交表单
+      await saveOrUpdateMenu(values, unref(isUpdate));
+      closeDrawer();
+      emit('success');
+    } finally {
+      setDrawerProps({ confirmLoading: false });
+    }
+  }
 
-      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
-        resetFields();
-        setDrawerProps({ confirmLoading: false });
-        isUpdate.value = !!data?.isUpdate;
+  /** url 变化时,动态设置组件名称placeholder */
+  function onUrlChange(url) {
+    let placeholder = '';
+    let httpUrl = url;
+    if (url != null && url != '') {
+      if (url.startsWith('/')) {
+        url = url.substring(1);
+      }
+      url = url.replaceAll('/', '-');
+      // 特殊标记
+      url = url.replaceAll(':', '@');
+      placeholder = `${url}`;
+    } else {
+      placeholder = '请输入组件名称';
+    }
+    updateSchema([{ field: 'componentName', componentProps: { placeholder } }]);
+    //update-begin---author:wangshuai ---date:20230204  for:[QQYUN-4058]菜单添加智能化处理------------
+    if (httpUrl != null && httpUrl != '') {
+      if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
+        setFieldsValue({ component: httpUrl });
+      }
+    }
+    //update-end---author:wangshuai ---date:20230204  for:[QQYUN-4058]菜单添加智能化处理------------
+  }
 
-        if (unref(isUpdate)) {
-          setFieldsValue({
-            ...data.record,
-          });
+  /**
+  * 2024-03-06
+  * liaozhiyang
+  * 翻译菜单名称
+  */
+  function translateMenu(data, key) {
+    if (data?.length) {
+      const { t } = useI18n();
+      data.forEach((item) => {
+        if (item[key]) {
+          if (item[key].includes("t('") && t) {
+            item[key] = new Function('t', `return ${item[key]}`)(t);
+          }
         }
-        const treeData = await getMenuList();
-        updateSchema({
-          field: 'parentMenu',
-          componentProps: { treeData },
-        });
-      });
-
-      const getTitle = computed(() => (!unref(isUpdate) ? '新增菜单' : '编辑菜单'));
-
-      async function handleSubmit() {
-        try {
-          const values = await validate();
-          setDrawerProps({ confirmLoading: true });
-          // TODO custom api
-          console.log(values);
-          closeDrawer();
-          emit('success');
-        } finally {
-          setDrawerProps({ confirmLoading: false });
+        if (item.children?.length) {
+          translateMenu(item.children, key);
         }
-      }
-
-      return { registerDrawer, registerForm, getTitle, handleSubmit };
-    },
-  });
+      });
+    }
+    return data;
+  }
 </script>

+ 249 - 92
src/views/system/menu/index.vue

@@ -1,107 +1,264 @@
 <template>
-  <div>
-    <BasicTable @register="registerTable" @fetch-success="onFetchSuccess">
+  <div class="p-4">
+    <BasicTable @register="registerTable" :rowSelection="rowSelection">
       <template #tableTitle>
-        <a-button type="primary" @click="handleCreate"> 新增菜单 </a-button>
+        <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增菜单</a-button>
+        <a-button type="primary" preIcon="ic:round-expand" @click="expandAll">展开全部</a-button>
+        <a-button type="primary" preIcon="ic:round-compress" @click="collapseAll">折叠全部</a-button>
+
+        <a-dropdown v-if="checkedKeys.length > 0">
+          <template #overlay>
+            <a-menu>
+              <a-menu-item key="1" @click="batchHandleDelete">
+                <Icon icon="ant-design:delete-outlined" />
+                删除
+              </a-menu-item>
+            </a-menu>
+          </template>
+          <a-button
+            >批量操作
+            <Icon icon="ant-design:down-outlined" />
+          </a-button>
+        </a-dropdown>
       </template>
       <template #action="{ record }">
-        <TableAction
-          :actions="[
-            {
-              icon: 'clarity:note-edit-line',
-              onClick: handleEdit.bind(null, record),
-            },
-            {
-              icon: 'ant-design:delete-outlined',
-              color: 'error',
-              popConfirm: {
-                title: '是否确认删除',
-                confirm: handleDelete.bind(null, record),
-              },
-            },
-          ]"
-        />
+        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
       </template>
     </BasicTable>
-    <MenuDrawer @register="registerDrawer" @success="handleSuccess" />
+    <MenuDrawer @register="registerDrawer" @success="handleSuccess" :showFooter="showFooter" />
+    <DataRuleList @register="registerDrawer1" />
   </div>
 </template>
-<script lang="ts">
-  import { defineComponent, nextTick } from 'vue';
-
+<script lang="ts" name="system-menu" setup>
+  import { nextTick, ref } from 'vue';
   import { BasicTable, useTable, TableAction } from '/@/components/Table';
-  import { getMenuList } from '/@/api/demo/system';
-
+  import { useListPage } from '/@/hooks/system/useListPage';
   import { useDrawer } from '/@/components/Drawer';
   import MenuDrawer from './MenuDrawer.vue';
+  import DataRuleList from './DataRuleList.vue';
+  import { columns,searchFormSchema } from './menu.data';
+  import { list, deleteMenu, batchDeleteMenu } from './menu.api';
+  import { useDefIndexStore } from "@/store/modules/defIndex";
+  import { useI18n } from "/@/hooks/web/useI18n";
 
-  import { columns, searchFormSchema } from './menu.data';
-
-  export default defineComponent({
-    name: 'MenuManagement',
-    components: { BasicTable, MenuDrawer, TableAction },
-    setup() {
-      const [registerDrawer, { openDrawer }] = useDrawer();
-      const [registerTable, { reload, expandAll }] = useTable({
-        title: '菜单列表',
-        api: getMenuList,
-        columns,
-        formConfig: {
-          labelWidth: 120,
-          schemas: searchFormSchema,
-        },
-        isTreeTable: true,
-        pagination: false,
-        striped: false,
-        useSearchForm: true,
-        showTableSetting: true,
-        bordered: true,
-        showIndexColumn: false,
-        canResize: false,
-        actionColumn: {
-          width: 80,
-          title: '操作',
-          dataIndex: 'action',
-          slots: { customRender: 'action' },
-          fixed: undefined,
-        },
-      });
-
-      function handleCreate() {
-        openDrawer(true, {
-          isUpdate: false,
-        });
-      }
-
-      function handleEdit(record: Recordable) {
-        openDrawer(true, {
-          record,
-          isUpdate: true,
-        });
-      }
-
-      function handleDelete(record: Recordable) {
-        console.log(record);
-      }
-
-      function handleSuccess() {
-        reload();
-      }
-
-      function onFetchSuccess() {
-        // 演示默认展开所有表项
-        nextTick(expandAll);
-      }
-
-      return {
-        registerTable,
-        registerDrawer,
-        handleCreate,
-        handleEdit,
-        handleDelete,
-        handleSuccess,
-        onFetchSuccess,
-      };
+  const checkedKeys = ref<Array<string | number>>([]);
+  const showFooter = ref(true);
+  const [registerDrawer, { openDrawer }] = useDrawer();
+  const [registerDrawer1, { openDrawer: openDataRule }] = useDrawer();
+  const { t } = useI18n();
+
+  // 自定义菜单名称列渲染
+  columns[0].customRender = function ({text, record}) {
+    const isDefIndex = checkDefIndex(record)
+    if (isDefIndex) {
+      text += '(默认首页)'
+    }
+    // update-begin--author:liaozhiyang---date:20240306---for:【QQYUN-8379】菜单管理页菜单国际化
+    if (text.includes("t('") && t) {
+      return new Function('t', `return ${text}`)(t);
+    }
+    // update-end--author:liaozhiyang---date:20240306---for:【QQYUN-8379】菜单管理页菜单国际化
+    return text
+  }
+
+  // 列表页面公共参数、方法
+  const { prefixCls, tableContext } = useListPage({
+    tableProps: {
+      title: '菜单列表',
+      api: list,
+      columns: columns,
+      size: 'small',
+      pagination: false,
+      isTreeTable: true,
+      striped: true,
+      useSearchForm: true,
+      showTableSetting: true,
+      bordered: true,
+      showIndexColumn: false,
+      tableSetting: { fullScreen: true },
+      formConfig: {
+        // update-begin--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
+        labelWidth: 74,
+        rowProps: { gutter: 24 },
+        // update-end--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
+        schemas: searchFormSchema,
+        autoAdvancedCol: 4,
+        baseColProps: { xs: 24, sm: 12, md: 6, lg: 6, xl: 6, xxl: 6 },
+        actionColOptions: { xs: 24, sm: 12, md: 6, lg: 6, xl: 6, xxl: 6 },
+      },
+      actionColumn: {
+        width: 120,
+      },
     },
   });
+  //注册table数据
+  const [registerTable, { reload, expandAll, collapseAll }] = tableContext;
+
+  /**
+   * 选择列配置
+   */
+  const rowSelection = {
+    type: 'checkbox',
+    columnWidth: 30,
+    selectedRowKeys: checkedKeys,
+    onChange: onSelectChange,
+  };
+
+  /**
+   * 选择事件
+   */
+  function onSelectChange(selectedRowKeys: (string | number)[]) {
+    checkedKeys.value = selectedRowKeys;
+  }
+
+  /**
+   * 新增
+   */
+  function handleCreate() {
+    showFooter.value = true;
+    openDrawer(true, {
+      isUpdate: false,
+    });
+  }
+
+  /**
+   * 编辑
+   */
+  function handleEdit(record) {
+    showFooter.value = true;
+    openDrawer(true, {
+      record,
+      isUpdate: true,
+    });
+  }
+  /**
+   * 详情
+   */
+  function handleDetail(record) {
+    showFooter.value = false;
+    openDrawer(true, {
+      record,
+      isUpdate: true,
+    });
+  }
+  /**
+   * 添加下级
+   */
+  function handleAddSub(record) {
+    openDrawer(true, {
+      record: { parentId: record.id, menuType: 1 },
+      isUpdate: false,
+    });
+  }
+  /**
+   * 数据权限弹窗
+   */
+  function handleDataRule(record) {
+    openDataRule(true, { id: record.id });
+  }
+
+  /**
+   * 删除
+   */
+  async function handleDelete(record) {
+    await deleteMenu({ id: record.id }, reload);
+  }
+  /**
+   * 批量删除事件
+   */
+  async function batchHandleDelete() {
+    await batchDeleteMenu({ ids: checkedKeys.value }, () => {
+      // -update-begin--author:liaozhiyang---date:20240702---for:【TV360X-1662】菜单管理、定时任务批量删除清空选中
+      reload();
+      checkedKeys.value = [];
+      // -update-end--author:liaozhiyang---date:20240702---for:【TV360X-1662】菜单管理、定时任务批量删除清空选中
+    });
+  }
+  /**
+   * 成功回调
+   */
+  function handleSuccess() {
+    reload();
+    reloadDefIndex();
+  }
+
+  function onFetchSuccess() {
+    // 演示默认展开所有表项
+    nextTick(expandAll);
+  }
+
+  // --------------- begin 默认首页配置 ------------
+
+  const defIndexStore = useDefIndexStore()
+
+  // 设置默认主页
+  async function handleSetDefIndex(record: Recordable) {
+    defIndexStore.update(record.url, record.component, record.route)
+  }
+
+  /**
+   * 检查是否为默认主页
+   * @param record
+   */
+  function checkDefIndex(record: Recordable) {
+    return defIndexStore.check(record.url)
+  }
+
+  // 重新加载默认首页配置
+  function reloadDefIndex() {
+    try {
+      defIndexStore.query();
+    } catch (e) {
+      console.error(e)
+    }
+  }
+
+  reloadDefIndex()
+
+  // --------------- end 默认首页配置 ------------
+
+  /**
+   * 操作栏
+   */
+  function getTableAction(record) {
+    return [
+      {
+        label: '编辑',
+        onClick: handleEdit.bind(null, record),
+      },
+    ];
+  }
+
+  /**
+   * 下拉操作栏
+   */
+  function getDropDownAction(record) {
+    return [
+      // {
+      //   label: '详情',
+      //   onClick: handleDetail.bind(null, record),
+      // },
+      {
+        label: '添加下级',
+        onClick: handleAddSub.bind(null, record),
+      },
+      {
+        label: '数据规则',
+        onClick: handleDataRule.bind(null, record),
+      },
+      {
+        label: '设为默认首页',
+        onClick: handleSetDefIndex.bind(null, record),
+        ifShow: () => !record.internalOrExternal && record.component && !checkDefIndex(record),
+      },
+      {
+        label: '删除',
+        color: 'error',
+        popConfirm: {
+          title: '是否确认删除',
+          confirm: handleDelete.bind(null, record),
+        },
+      },
+    ];
+  }
 </script>

+ 362 - 103
src/views/system/menu/menu.data.ts

@@ -1,16 +1,36 @@
 import { BasicColumn } from '/@/components/Table';
 import { FormSchema } from '/@/components/Table';
 import { h } from 'vue';
-import { Tag } from 'ant-design-vue';
 import { Icon } from '/@/components/Icon';
+import { duplicateCheck } from '../user/user.api';
+import { ajaxGetDictItems ,checkPermDuplication } from './menu.api';
+import { render } from '/@/utils/common/renderUtils';
+
+const isDir = (type) => type === 0;
+const isMenu = (type) => type === 1;
+const isButton = (type) => type === 2;
+
+// 定义可选择的组件类型
+export enum ComponentTypes {
+  Default = 'layouts/default/index',
+  IFrame = 'sys/iframe/FrameBlank',
+}
 
 export const columns: BasicColumn[] = [
   {
     title: '菜单名称',
-    dataIndex: 'menuName',
+    dataIndex: 'name',
     width: 200,
     align: 'left',
   },
+  {
+    title: '菜单类型',
+    dataIndex: 'menuType',
+    width: 150,
+    customRender: ({ text }) => {
+      return render.renderDict(text, 'menu_type');
+    },
+  },
   {
     title: '图标',
     dataIndex: 'icon',
@@ -19,184 +39,423 @@ export const columns: BasicColumn[] = [
       return h(Icon, { icon: record.icon });
     },
   },
-  {
-    title: '权限标识',
-    dataIndex: 'permission',
-    width: 180,
-  },
   {
     title: '组件',
     dataIndex: 'component',
+    align: 'left',
+    width: 150,
   },
   {
-    title: '排序',
-    dataIndex: 'orderNo',
-    width: 50,
-  },
-  {
-    title: '状态',
-    dataIndex: 'status',
-    width: 80,
-    customRender: ({ record }) => {
-      const status = record.status;
-      const enable = ~~status === 0;
-      const color = enable ? 'green' : 'red';
-      const text = enable ? '启用' : '停用';
-      return h(Tag, { color: color }, () => text);
-    },
+    title: '路径',
+    dataIndex: 'url',
+    align: 'left',
+    width: 150,
   },
   {
-    title: '创建时间',
-    dataIndex: 'createTime',
-    width: 180,
+    title: '排序',
+    dataIndex: 'sortNo',
+    width: 50,
   },
 ];
 
-const isDir = (type: string) => type === '0';
-const isMenu = (type: string) => type === '1';
-const isButton = (type: string) => type === '2';
-
 export const searchFormSchema: FormSchema[] = [
   {
-    field: 'menuName',
+    field: 'name',
     label: '菜单名称',
     component: 'Input',
     colProps: { span: 8 },
   },
-  {
-    field: 'status',
-    label: '状态',
-    component: 'Select',
-    componentProps: {
-      options: [
-        { label: '启用', value: '0' },
-        { label: '停用', value: '1' },
-      ],
-    },
-    colProps: { span: 8 },
-  },
 ];
 
 export const formSchema: FormSchema[] = [
   {
-    field: 'type',
+    label: 'id',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'menuType',
     label: '菜单类型',
     component: 'RadioButtonGroup',
-    defaultValue: '0',
-    componentProps: {
-      options: [
-        { label: '目录', value: '0' },
-        { label: '菜单', value: '1' },
-        { label: '按钮', value: '2' },
-      ],
+    defaultValue: 0,
+    componentProps: ({ formActionType, formModel }) => {
+      return {
+        options: [
+          { label: '一级菜单', value: 0 },
+          { label: '子菜单', value: 1 },
+          { label: '按钮/权限', value: 2 },
+        ],
+        onChange: (e) => {
+          const { updateSchema, clearValidate } = formActionType;
+          const label = isButton(e) ? '按钮/权限' : '菜单名称';
+          //清除校验
+          clearValidate();
+          updateSchema([
+            {
+              field: 'name',
+              label: label,
+            },
+            {
+              field: 'url',
+              required: !isButton(e),
+            },
+          ]);
+          //update-begin---author:wangshuai ---date:20220729  for:[VUEN-1834]只有一级菜单,才默认值,子菜单的时候,清空------------
+          if (isMenu(e) && !formModel.id && (formModel.component=='layouts/default/index' || formModel.component=='layouts/RouteView')) {
+            formModel.component = '';
+          }
+          //update-end---author:wangshuai ---date:20220729  for:[VUEN-1834]只有一级菜单,才默认值,子菜单的时候,清空------------
+        },
+      };
     },
-    colProps: { lg: 24, md: 24 },
   },
   {
-    field: 'menuName',
+    field: 'name',
     label: '菜单名称',
     component: 'Input',
     required: true,
   },
-
   {
-    field: 'parentMenu',
+    field: 'parentId',
     label: '上级菜单',
     component: 'TreeSelect',
+    required: true,
     componentProps: {
-      replaceFields: {
-        title: 'menuName',
+      //update-begin---author:wangshuai ---date:20230829  for:replaceFields已过期,使用fieldNames代替------------
+      fieldNames: {
+        label: 'name',
         key: 'id',
         value: 'id',
       },
-      getPopupContainer: () => document.body,
+      //update-end---author:wangshuai ---date:20230829  for:replaceFields已过期,使用fieldNames代替------------
+      dropdownStyle: {
+        maxHeight: '50vh',
+      },
+      getPopupContainer: (node) => node?.parentNode,
     },
+    ifShow: ({ values }) => !isDir(values.menuType),
   },
-
   {
-    field: 'orderNo',
-    label: '排序',
-    component: 'InputNumber',
+    field: 'url',
+    label: '访问路径',
+    component: 'Input',
     required: true,
+    //update-begin-author:liusq date:2023-06-06 for: [issues/5008]子表数据权限设置不生效
+    ifShow: ({ values }) => !(values.component === ComponentTypes.IFrame && values.internalOrExternal),
+    //update-begin-author:zyf date:2022-11-02 for: 聚合路由允许路径重复
+     dynamicRules: ({ model, schema,values }) => {
+       return checkPermDuplication(model, schema,  values.menuType !== 2?true:false);
+    },
+    //update-end-author:zyf date:2022-11-02 for: 聚合路由允许路径重复
+    //update-end-author:liusq date:2022-06-06 for:  [issues/5008]子表数据权限设置不生效
   },
   {
-    field: 'icon',
-    label: '图标',
-    component: 'IconPicker',
+    field: 'component',
+    label: '前端组件',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入前端组件',
+    },
+    defaultValue:'layouts/default/index',
     required: true,
-    ifShow: ({ values }) => !isButton(values.type),
+    ifShow: ({ values }) => !isButton(values.menuType),
   },
-
   {
-    field: 'routePath',
-    label: '路由地址',
+    field: 'componentName',
+    label: '组件名称',
     component: 'Input',
-    required: true,
-    ifShow: ({ values }) => !isButton(values.type),
+    componentProps: {
+      placeholder: '请输入组件名称',
+    },
+    helpMessage: [
+      '此处名称应和vue组件的name属性保持一致。',
+      '组件名称不能重复,主要用于路由缓存功能。',
+      '如果组件名称和vue组件的name属性不一致,则会导致路由缓存失效。',
+      '非必填,留空则会根据访问路径自动生成。',
+    ],
+    defaultValue: '',
+    ifShow: ({ values }) => !isButton(values.menuType),
   },
   {
-    field: 'component',
-    label: '组件路径',
+    field: 'frameSrc',
+    label: 'Iframe地址',
     component: 'Input',
-    ifShow: ({ values }) => isMenu(values.type),
+    rules: [
+      { required: true, message: '请输入Iframe地址' },
+      { type: 'url', message: '请输入正确的url地址' },
+    ],
+    ifShow: ({ values }) => !isButton(values.menuType) && values.component === ComponentTypes.IFrame,
   },
   {
-    field: 'permission',
-    label: '权限标识',
+    field: 'redirect',
+    label: '默认跳转地址',
     component: 'Input',
-    ifShow: ({ values }) => !isDir(values.type),
+    ifShow: ({ values }) => isDir(values.menuType),
   },
   {
-    field: 'status',
-    label: '状态',
-    component: 'RadioButtonGroup',
-    defaultValue: '0',
+    field: 'perms',
+    label: '授权标识',
+    component: 'Input',
+    ifShow: ({ values }) => isButton(values.menuType),
+    // dynamicRules: ({ model }) => {
+    //   return [
+    //     {
+    //       required: false,
+    //       validator: (_, value) => {
+    //         return new Promise((resolve, reject) => {
+    //           let params = {
+    //             tableName: 'sys_permission',
+    //             fieldName: 'perms',
+    //             fieldVal: value,
+    //             dataId: model.id,
+    //           };
+    //           duplicateCheck(params)
+    //             .then((res) => {
+    //               res.success ? resolve() : reject(res.message || '校验失败');
+    //             })
+    //             .catch((err) => {
+    //               reject(err.message || '校验失败');
+    //             });
+    //         });
+    //       },
+    //     },
+    //   ];
+    // },
+  },
+  {
+    field: 'permsType',
+    label: '授权策略',
+    component: 'RadioGroup',
+    defaultValue: '1',
+    helpMessage: ['可见/可访问(授权后可见/可访问)', '可编辑(未授权时禁用)'],
     componentProps: {
       options: [
-        { label: '启用', value: '0' },
-        { label: '禁用', value: '1' },
+        { label: '可见/可访问', value: '1' },
+        { label: '可编辑', value: '2' },
       ],
     },
+    ifShow: ({ values }) => isButton(values.menuType),
   },
   {
-    field: 'isExt',
-    label: '是否外链',
-    component: 'RadioButtonGroup',
-    defaultValue: '0',
+    field: 'status',
+    label: '状态',
+    component: 'RadioGroup',
+    defaultValue: '1',
     componentProps: {
       options: [
-        { label: '否', value: '0' },
-        { label: '是', value: '1' },
+        { label: '有效', value: '1' },
+        { label: '无效', value: '0' },
       ],
     },
-    ifShow: ({ values }) => !isButton(values.type),
+    ifShow: ({ values }) => isButton(values.menuType),
+  },
+  {
+    field: 'icon',
+    label: '菜单图标',
+    component: 'IconPicker',
+    ifShow: ({ values }) => !isButton(values.menuType),
+    componentProps: {
+      allowClear: true
+    },
   },
+  {
+    field: 'sortNo',
+    label: '排序',
+    component: 'InputNumber',
+    defaultValue: 1,
+    ifShow: ({ values }) => !isButton(values.menuType),
+  },
+  {
+    field: 'route',
+    label: '是否路由菜单',
+    component: 'Switch',
+    defaultValue: true,
+    componentProps: {
+      checkedChildren: '是',
+      unCheckedChildren: '否',
+    },
+    ifShow: ({ values }) => !isButton(values.menuType),
+  },
+  {
+    field: 'hidden',
+    label: '隐藏路由',
+    component: 'Switch',
+    defaultValue: 0,
+    componentProps: {
+      checkedChildren: '是',
+      unCheckedChildren: '否',
+    },
+    ifShow: ({ values }) => !isButton(values.menuType),
+  },
+  {
+    field: 'hideTab',
+    label: '隐藏Tab',
+    component: 'Switch',
+    defaultValue: 0,
+    componentProps: {
+      checkedChildren: '是',
+      unCheckedChildren: '否',
+    },
+    ifShow: ({ values }) => !isButton(values.menuType),
+  },
+  {
+    field: 'keepAlive',
+    label: '是否缓存路由',
+    component: 'Switch',
+    defaultValue: false,
+    componentProps: {
+      checkedChildren: '是',
+      unCheckedChildren: '否',
+    },
+    ifShow: ({ values }) => !isButton(values.menuType),
+  },
+  {
+    field: 'alwaysShow',
+    label: '聚合路由',
+    component: 'Switch',
+    defaultValue: false,
+    componentProps: {
+      checkedChildren: '是',
+      unCheckedChildren: '否',
+    },
+    ifShow: ({ values }) => !isButton(values.menuType),
+  },
+  {
+    field: 'internalOrExternal',
+    label: '打开方式',
+    component: 'Switch',
+    defaultValue: false,
+    componentProps: {
+      checkedChildren: '外部',
+      unCheckedChildren: '内部',
+    },
+    ifShow: ({ values }) => !isButton(values.menuType),
+  },
+];
 
+export const dataRuleColumns: BasicColumn[] = [
   {
-    field: 'keepalive',
-    label: '是否缓存',
-    component: 'RadioButtonGroup',
-    defaultValue: '0',
+    title: '规则名称',
+    dataIndex: 'ruleName',
+    width: 150,
+  },
+  {
+    title: '规则字段',
+    dataIndex: 'ruleColumn',
+    width: 100,
+  },
+  {
+    title: '规则值',
+    dataIndex: 'ruleValue',
+    width: 100,
+  },
+];
+
+export const dataRuleSearchFormSchema: FormSchema[] = [
+  {
+    field: 'ruleName',
+    label: '规则名称',
+    component: 'Input',
+    // colProps: { span: 6 },
+  },
+  {
+    field: 'ruleValue',
+    label: '规则值',
+    component: 'Input',
+    // colProps: { span: 6 },
+  },
+];
+
+export const dataRuleFormSchema: FormSchema[] = [
+  {
+    label: 'id',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'ruleName',
+    label: '规则名称',
+    component: 'Input',
+    required: true,
+  },
+  {
+    field: 'ruleColumn',
+    label: '规则字段',
+    component: 'Input',
+    ifShow: ({ values }) => {
+      const ruleConditions = Array.isArray(values.ruleConditions) ? values.ruleConditions[0] : values.ruleConditions;
+      return ruleConditions !== 'USE_SQL_RULES';
+    },
+  },
+  {
+    field: 'ruleConditions',
+    label: '条件规则',
+    required: true,
+    component: 'ApiSelect',
+    componentProps: {
+      api: ajaxGetDictItems,
+      params: { code: 'rule_conditions' },
+      labelField: 'text',
+      valueField: 'value',
+      getPopupContainer: (node) => document.body,
+    },
+  },
+  // update-begin--author:liaozhiyang---date:20240724---for:【TV360X-1864】添加系统变量
+  {
+    field: 'ruleValue',
+    component: 'JInputSelect',
+    label: '规则值',
+    required: true,
     componentProps: {
+      selectPlaceholder: '可选择系统变量',
+      inputPlaceholder: '请输入',
+      getPopupContainer: () => document.body,
+      selectWidth: '200px',
       options: [
-        { label: '否', value: '0' },
-        { label: '是', value: '1' },
+        {
+          label: '登录用户账号',
+          value: '#{sys_user_code}',
+        },
+        {
+          label: '登录用户名称',
+          value: '#{sys_user_name}',
+        },
+        {
+          label: '当前日期',
+          value: '#{sys_date}',
+        },
+        {
+          label: '当前时间',
+          value: '#{sys_time}',
+        },
+        {
+          label: '登录用户部门',
+          value: '#{sys_org_code}',
+        },
+        {
+          label: '用户拥有部门',
+          value: '#{sys_multi_org_code}',
+        },
+        {
+          label: '登录用户租户',
+          value: '#{tenant_id}',
+        },
       ],
     },
-    ifShow: ({ values }) => isMenu(values.type),
   },
-
+  // update-end--author:liaozhiyang---date:20240724---for:【TV360X-1864】添加系统变量
   {
-    field: 'show',
-    label: '是否显示',
+    field: 'status',
+    label: '状态',
     component: 'RadioButtonGroup',
-    defaultValue: '0',
+    defaultValue: '1',
     componentProps: {
       options: [
-        { label: '是', value: '0' },
-        { label: '否', value: '1' },
+        { label: '无效', value: '0' },
+        { label: '有效', value: '1' },
       ],
     },
-    ifShow: ({ values }) => !isButton(values.type),
   },
 ];

+ 0 - 80
src/views/system/role/RoleDrawer.vue

@@ -1,80 +0,0 @@
-<template>
-  <BasicDrawer v-bind="$attrs" @register="registerDrawer" showFooter :title="getTitle" width="500px" @ok="handleSubmit">
-    <BasicForm @register="registerForm">
-      <template #menu="{ model, field }">
-        <BasicTree
-          v-model:value="model[field]"
-          :treeData="treeData"
-          :replaceFields="{ title: 'menuName', key: 'id' }"
-          checkable
-          toolbar
-          title="菜单分配"
-        />
-      </template>
-    </BasicForm>
-  </BasicDrawer>
-</template>
-<script lang="ts">
-  import { defineComponent, ref, computed, unref } from 'vue';
-  import { BasicForm, useForm } from '/@/components/Form/index';
-  import { formSchema } from './role.data';
-  import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
-  import { BasicTree, TreeItem } from '/@/components/Tree';
-
-  import { getMenuList } from '/@/api/demo/system';
-
-  export default defineComponent({
-    name: 'RoleDrawer',
-    components: { BasicDrawer, BasicForm, BasicTree },
-    emits: ['success', 'register'],
-    setup(_, { emit }) {
-      const isUpdate = ref(true);
-      const treeData = ref<TreeItem[]>([]);
-
-      const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
-        labelWidth: 90,
-        schemas: formSchema,
-        showActionButtonGroup: false,
-      });
-
-      const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
-        resetFields();
-        setDrawerProps({ confirmLoading: false });
-        // 需要在setFieldsValue之前先填充treeData,否则Tree组件可能会报key not exist警告
-        if (unref(treeData).length === 0) {
-          treeData.value = (await getMenuList()) as any as TreeItem[];
-        }
-        isUpdate.value = !!data?.isUpdate;
-
-        if (unref(isUpdate)) {
-          setFieldsValue({
-            ...data.record,
-          });
-        }
-      });
-
-      const getTitle = computed(() => (!unref(isUpdate) ? '新增角色' : '编辑角色'));
-
-      async function handleSubmit() {
-        try {
-          const values = await validate();
-          setDrawerProps({ confirmLoading: true });
-          // TODO custom api
-          console.log(values);
-          closeDrawer();
-          emit('success');
-        } finally {
-          setDrawerProps({ confirmLoading: false });
-        }
-      }
-
-      return {
-        registerDrawer,
-        registerForm,
-        getTitle,
-        handleSubmit,
-        treeData,
-      };
-    },
-  });
-</script>

+ 185 - 89
src/views/system/role/index.vue

@@ -1,97 +1,193 @@
 <template>
-  <div>
-    <BasicTable @register="registerTable">
-      <template #tableTitle>
-        <a-button type="primary" @click="handleCreate"> 新增角色 </a-button>
-      </template>
-      <template #action="{ record }">
-        <TableAction
-          :actions="[
-            {
-              icon: 'clarity:note-edit-line',
-              onClick: handleEdit.bind(null, record),
-            },
-            {
-              icon: 'ant-design:delete-outlined',
-              color: 'error',
-              popConfirm: {
-                title: '是否确认删除',
-                confirm: handleDelete.bind(null, record),
-              },
-            },
-          ]"
-        />
-      </template>
-    </BasicTable>
-    <RoleDrawer @register="registerDrawer" @success="handleSuccess" />
-  </div>
+  <BasicTable @register="registerTable">
+    <template #tableTitle>
+      <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增</a-button>
+      <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
+      <j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
+      <a-dropdown v-if="selectedRowKeys.length > 0">
+        <template #overlay>
+          <a-menu>
+            <a-menu-item key="1" @click="batchHandleDelete">
+              <Icon icon="ant-design:delete-outlined"></Icon>
+              删除
+            </a-menu-item>
+          </a-menu>
+        </template>
+        <a-button
+          >批量操作
+          <Icon icon="mdi:chevron-down"></Icon>
+        </a-button>
+      </a-dropdown>
+    </template>
+    <template #action="{ record }">
+      <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
+    </template>
+  </BasicTable>
+  <!--角色用户表格-->
+  <RoleUserTable @register="roleUserDrawer" />
+  <!--角色编辑抽屉-->
+  <RoleDrawer @register="registerDrawer" @success="reload" :showFooter="showFooter" />
+  <!--角色详情-->
+  <RoleDesc @register="registerDesc"></RoleDesc>
+  <!--角色菜单授权抽屉-->
+  <RolePermissionDrawer @register="rolePermissionDrawer" />
+  <!--角色首页配置-->
+  <RoleIndexModal @register="registerIndexModal" />
 </template>
-<script lang="ts">
-  import { defineComponent } from 'vue';
-
-  import { BasicTable, useTable, TableAction } from '/@/components/Table';
-  import { getRoleListByPage } from '/@/api/demo/system';
-
+<script lang="ts" name="system-role" setup>
+  import { ref } from 'vue';
+  import { BasicTable, TableAction } from '/@/components/Table';
   import { useDrawer } from '/@/components/Drawer';
-  import RoleDrawer from './RoleDrawer.vue';
-
+  import { useModal } from '/@/components/Modal';
+  import RoleDrawer from './components/RoleDrawer.vue';
+  import RoleDesc from './components/RoleDesc.vue';
+  import RolePermissionDrawer from './components/RolePermissionDrawer.vue';
+  import RoleIndexModal from './components/RoleIndexModal.vue';
+  import RoleUserTable from './components/RoleUserTable.vue';
   import { columns, searchFormSchema } from './role.data';
+  import { list, deleteRole, batchDeleteRole, getExportUrl, getImportUrl } from './role.api';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  const showFooter = ref(true);
+  const [roleUserDrawer, { openDrawer: openRoleUserDrawer }] = useDrawer();
+  const [registerDrawer, { openDrawer }] = useDrawer();
+  const [registerIndexModal, { openModal: openIndexModal }] = useModal();
+  const [rolePermissionDrawer, { openDrawer: openRolePermissionDrawer }] = useDrawer();
+  const [registerDesc, { openDrawer: openRoleDesc }] = useDrawer();
 
-  export default defineComponent({
-    name: 'system-demorole',
-    components: { BasicTable, RoleDrawer, TableAction },
-    setup() {
-      const [registerDrawer, { openDrawer }] = useDrawer();
-      const [registerTable, { reload }] = useTable({
-        title: '角色列表',
-        api: getRoleListByPage,
-        columns,
-        formConfig: {
-          labelWidth: 120,
-          schemas: searchFormSchema,
-        },
-        useSearchForm: true,
-        showTableSetting: true,
-        bordered: true,
-        showIndexColumn: false,
-        actionColumn: {
-          width: 80,
-          title: '操作',
-          dataIndex: 'action',
-          slots: { customRender: 'action' },
-          fixed: undefined,
-        },
-      });
-
-      function handleCreate() {
-        openDrawer(true, {
-          isUpdate: false,
-        });
-      }
-
-      function handleEdit(record: Recordable) {
-        openDrawer(true, {
-          record,
-          isUpdate: true,
-        });
-      }
-
-      function handleDelete(record: Recordable) {
-        console.log(record);
-      }
-
-      function handleSuccess() {
-        reload();
-      }
-
-      return {
-        registerTable,
-        registerDrawer,
-        handleCreate,
-        handleEdit,
-        handleDelete,
-        handleSuccess,
-      };
+  // 列表页面公共参数、方法
+  const { prefixCls, tableContext, onImportXls, onExportXls } = useListPage({
+    designScope: 'role-template',
+    tableProps: {
+      title: '系统角色列表',
+      api: list,
+      columns: columns,
+      formConfig: {
+        // update-begin--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
+        labelWidth:65,
+        rowProps: { gutter: 24 },
+        // update-end--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
+        schemas: searchFormSchema,
+      },
+      actionColumn: {
+        width: 120,
+      },
+      rowSelection: null,
+      //自定义默认排序
+      defSort: {
+        column: 'id',
+        order: 'desc',
+      },
+    },
+    exportConfig: {
+      name: '角色列表',
+      url: getExportUrl,
+    },
+    importConfig: {
+      url: getImportUrl,
     },
   });
+  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+
+  /**
+   * 新增
+   */
+  function handleCreate() {
+    showFooter.value = true;
+    openDrawer(true, {
+      isUpdate: false,
+    });
+  }
+  /**
+   * 编辑
+   */
+  function handleEdit(record: Recordable) {
+    showFooter.value = true;
+    openDrawer(true, {
+      record,
+      isUpdate: true,
+    });
+  }
+  /**
+   * 详情
+   */
+  function handleDetail(record) {
+    showFooter.value = false;
+    openRoleDesc(true, {
+      record,
+      isUpdate: true,
+    });
+  }
+  /**
+   * 删除事件
+   */
+  async function handleDelete(record) {
+    await deleteRole({ id: record.id }, reload);
+  }
+  /**
+   * 批量删除事件
+   */
+  async function batchHandleDelete() {
+    await batchDeleteRole({ ids: selectedRowKeys.value }, reload);
+  }
+  /**
+   * 角色授权弹窗
+   */
+  function handlePerssion(record) {
+    openRolePermissionDrawer(true, { roleId: record.id });
+  }
+  /**
+   * 首页配置弹窗
+   */
+  function handleIndexConfig(roleCode) {
+    openIndexModal(true, { roleCode });
+  }
+  /**
+   * 角色用户
+   */
+  function handleUser(record) {
+    //onSelectChange(selectedRowKeys)
+    openRoleUserDrawer(true, record);
+  }
+  /**
+   * 操作栏
+   */
+  function getTableAction(record) {
+    return [
+      {
+        label: '用户',
+        onClick: handleUser.bind(null, record),
+      },
+      {
+        label: '授权',
+        onClick: handlePerssion.bind(null, record),
+      },
+    ];
+  }
+
+  /**
+   * 下拉操作栏
+   */
+  function getDropDownAction(record) {
+    return [
+      {
+        label: '编辑',
+        onClick: handleEdit.bind(null, record),
+      },
+      {
+        label: '详情',
+        onClick: handleDetail.bind(null, record),
+      },
+      {
+        label: '删除',
+        popConfirm: {
+          title: '是否确认删除',
+          confirm: handleDelete.bind(null, record),
+        },
+      },
+      {
+        label: '首页配置',
+        onClick: handleIndexConfig.bind(null, record.roleCode),
+      },
+    ];
+  }
 </script>

+ 139 - 79
src/views/system/role/role.data.ts

@@ -1,91 +1,74 @@
-import { BasicColumn } from '/@/components/Table';
 import { FormSchema } from '/@/components/Table';
-import { h } from 'vue';
-import { Switch } from 'ant-design-vue';
-import { setRoleStatus } from '/@/api/demo/system';
-import { useMessage } from '/@/hooks/web/useMessage';
-
-export const columns: BasicColumn[] = [
+import { isRoleExist } from './role.api';
+export const columns = [
   {
     title: '角色名称',
     dataIndex: 'roleName',
-    width: 200,
+    width: 100,
   },
   {
-    title: '角色',
-    dataIndex: 'roleValue',
-    width: 180,
+    title: '角色编码',
+    dataIndex: 'roleCode',
+    width: 100,
   },
   {
-    title: '排序',
-    dataIndex: 'orderNo',
-    width: 50,
+    title: '创建时间',
+    dataIndex: 'createTime',
+    width: 100,
   },
+];
+/**
+ * 角色用户Columns
+ */
+export const userColumns = [
   {
-    title: '状态',
-    dataIndex: 'status',
-    width: 120,
-    customRender: ({ record }) => {
-      if (!Reflect.has(record, 'pendingStatus')) {
-        record.pendingStatus = false;
-      }
-      return h(Switch, {
-        checked: record.status === '1',
-        checkedChildren: '已启用',
-        unCheckedChildren: '已禁用',
-        loading: record.pendingStatus,
-        onChange(checked: boolean) {
-          record.pendingStatus = true;
-          const newStatus = checked ? '1' : '0';
-          const { createMessage } = useMessage();
-          setRoleStatus(record.id, newStatus)
-            .then(() => {
-              record.status = newStatus;
-              createMessage.success(`已成功修改角色状态`);
-            })
-            .catch(() => {
-              createMessage.error('修改角色状态失败');
-            })
-            .finally(() => {
-              record.pendingStatus = false;
-            });
-        },
-      });
-    },
+    title: '用户账号',
+    dataIndex: 'username',
   },
   {
-    title: '创建时间',
-    dataIndex: 'createTime',
-    width: 180,
+    title: '用户姓名',
+    dataIndex: 'realname',
   },
   {
-    title: '备注',
-    dataIndex: 'remark',
+    title: '状态',
+    dataIndex: 'status_dictText',
+    width: 80,
   },
 ];
-
 export const searchFormSchema: FormSchema[] = [
   {
-    field: 'roleNme',
+    field: 'roleName',
     label: '角色名称',
     component: 'Input',
-    colProps: { span: 8 },
+    colProps: { span: 6 },
   },
   {
-    field: 'status',
-    label: '状态',
-    component: 'Select',
-    componentProps: {
-      options: [
-        { label: '启用', value: '0' },
-        { label: '停用', value: '1' },
-      ],
-    },
-    colProps: { span: 8 },
+    field: 'roleCode',
+    label: '角色编码',
+    component: 'Input',
+    colProps: { span: 6 },
+  },
+];
+/**
+ * 角色用户搜索form
+ */
+export const searchUserFormSchema: FormSchema[] = [
+  {
+    field: 'username',
+    label: '用户账号',
+    component: 'Input',
+    colProps: { span: 12 },
+    labelWidth: 74,
   },
 ];
 
 export const formSchema: FormSchema[] = [
+  {
+    field: 'id',
+    label: '',
+    component: 'Input',
+    show: false,
+  },
   {
     field: 'roleName',
     label: '角色名称',
@@ -93,32 +76,109 @@ export const formSchema: FormSchema[] = [
     component: 'Input',
   },
   {
-    field: 'roleValue',
-    label: '角色',
+    field: 'roleCode',
+    label: '角色编码',
     required: true,
     component: 'Input',
-  },
-  {
-    field: 'status',
-    label: '状态',
-    component: 'RadioButtonGroup',
-    defaultValue: '0',
-    componentProps: {
-      options: [
-        { label: '启用', value: '0' },
-        { label: '停用', value: '1' },
-      ],
+    dynamicDisabled: ({ values }) => {
+      return !!values.id;
+    },
+    dynamicRules: ({ values, model }) => {
+      console.log('values:', values);
+      return [
+        {
+          required: true,
+          validator: (_, value) => {
+            if (!value) {
+              return Promise.reject('请输入角色编码');
+            }
+            if (values) {
+              return new Promise((resolve, reject) => {
+                isRoleExist({ id: model.id, roleCode: value })
+                  .then((res) => {
+                    res.success ? resolve() : reject(res.message || '校验失败');
+                  })
+                  .catch((err) => {
+                    reject(err.message || '验证失败');
+                  });
+              });
+            }
+            return Promise.resolve();
+          },
+        },
+      ];
     },
   },
   {
     label: '备注',
-    field: 'remark',
+    field: 'description',
     component: 'InputTextArea',
   },
+];
+
+export const formDescSchema = [
+  {
+    field: 'roleName',
+    label: '角色名称',
+  },
+  {
+    field: 'roleCode',
+    label: '角色编码',
+  },
+  {
+    label: '备注',
+    field: 'description',
+  },
+];
+
+export const roleIndexFormSchema: FormSchema[] = [
+  {
+    field: 'id',
+    label: '',
+    component: 'Input',
+    show: false,
+  },
+  {
+    label: '角色编码',
+    field: 'roleCode',
+    component: 'Input',
+    dynamicDisabled: true,
+  },
   {
-    label: ' ',
-    field: 'menu',
-    slot: 'menu',
+    label: '首页路由',
+    field: 'url',
     component: 'Input',
+    required: true,
+    helpMessage: '首页路由的访问地址',
+  },
+  {
+    label: '组件地址',
+    field: 'component',
+    component: 'Input',
+    helpMessage: '首页路由的组件地址',
+    componentProps: {
+      placeholder: '请输入前端组件',
+    },
+    required: true,
+  },
+  {
+    field: 'route',
+    label: '是否路由菜单',
+    helpMessage: '非路由菜单设置成首页,需开启',
+    component: 'Switch',
+    defaultValue: true
+  },
+  {
+    label: '优先级',
+    field: 'priority',
+    component: 'InputNumber',
+  },
+  {
+    label: '是否开启',
+    field: 'status',
+    component: 'JSwitch',
+    componentProps: {
+      options: ['1', '0'],
+    },
   },
 ];

+ 143 - 145
src/views/system/usersetting/BaseSetting.vue

@@ -14,14 +14,15 @@
           <div v-if="!isEdit">
             <span class="font-size-17 account-name">{{ userInfo.realname }}</span>
             <a-tooltip content="编辑姓名">
-              <Icon class="pointer font-size-17 gray-bd account-icon" icon="ant-design:edit-outlined" @click="editHandleClick" />
+              <Icon class="pointer font-size-17 gray-bd account-icon" icon="ant-design:edit-outlined"
+                    @click="editHandleClick" />
             </a-tooltip>
           </div>
           <div v-else>
             <a-input ref="accountNameEdit" :maxlength="100" v-model:value="userInfo.realname" @blur="editRealName" />
           </div>
           <div class="use-day">
-            使用:<span>{{ userInfo.createTimeText }}</span>
+            使用:<span>{{userInfo.createTimeText}}</span>
           </div>
         </div>
       </div>
@@ -40,11 +41,11 @@
         </div>
         <div class="margin-bottom-10 nowarp font-size-13">
           <span class="gray-75 item-label">职位</span>
-          <span class="gray-3">{{ userInfo.postText ? userInfo.postText : '未填写' }}</span>
+          <span class="gray-3">{{ userInfo.postText ? userInfo.postText : "未填写" }}</span>
         </div>
         <div class="font-size-13">
           <span class="item-label"></span>
-          <span class="item-label pointer" style="color: #1e88e5" @click="openEditModal">编辑</span>
+          <span class="item-label pointer" style="color:#1e88e5" @click="openEditModal">编辑</span>
         </div>
       </div>
       <!-- 联系信息 -->
@@ -52,11 +53,11 @@
         <div class="font-size-15 font-bold font-color-gray" style="margin-bottom: 16px">联系信息</div>
         <div class="margin-bottom-10 font-size-13">
           <span class="gray-75 item-label">邮箱</span>
-          <span class="gray-3">{{ userInfo.email ? userInfo.email : '未填写' }}</span>
+          <span class="gray-3">{{ userInfo.email ? userInfo.email : "未填写" }}</span>
         </div>
         <div class="margin-bottom-10 font-size-13">
           <span class="gray-75 item-label">手机</span>
-          <span class="gray-3">{{ userInfo.phone ? userInfo.phone : '未填写' }}</span>
+          <span class="gray-3">{{ userInfo.phone ? userInfo.phone : "未填写" }}</span>
         </div>
       </div>
     </div>
@@ -64,165 +65,162 @@
   <UserAccountModal @register="registerModal" @success="getUserDetail"></UserAccountModal>
 </template>
 <script lang="ts" setup>
-  import { computed, onMounted, ref } from 'vue';
-  import { CollapseContainer } from '/@/components/Container';
-  import { CropperAvatar } from '/@/components/Cropper';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import headerImg from '/@/assets/images/header.jpg';
-  import { defHttp } from '/@/utils/http/axios';
-  import { useUserStore } from '/@/store/modules/user';
-  import { uploadImg } from '/@/api/sys/upload';
-  import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
-  import dayjs from 'dayjs';
-  import { ajaxGetDictItems, getDictItemsByCode, initDictOptions } from '/@/utils/dict';
-  import { userEdit, getUserData, queryNameByCodes } from './UserSetting.api';
-  import UserAccountModal from './commponents/UserAccountModal.vue';
-  import { useModal } from '/@/components/Modal';
-  import { cloneDeep } from 'lodash-es';
-  import { useDesign } from '/@/hooks/web/useDesign';
-  //TODO 当字典租户隔离时,数据会查不到,默认一个
-  const sexOption = getDictItemsByCode('sex') || [
-    { text: '男', value: '1' },
-    { text: '女', value: '2' },
-  ];
-  const { createMessage } = useMessage();
-  const userStore = useUserStore();
+import { computed, onMounted, ref } from 'vue';
+import { CollapseContainer } from '/@/components/Container';
+import { CropperAvatar } from '/@/components/Cropper';
+import { useMessage } from '/@/hooks/web/useMessage';
+import headerImg from '/@/assets/images/header.jpg';
+import { defHttp } from '/@/utils/http/axios';
+import { useUserStore } from '/@/store/modules/user';
+import { uploadImg } from '/@/api/sys/upload';
+import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
+import dayjs from 'dayjs';
+import { ajaxGetDictItems, getDictItemsByCode, initDictOptions } from '/@/utils/dict';
+import { userEdit, getUserData, queryNameByCodes } from './UserSetting.api';
+import UserAccountModal from './commponents/UserAccountModal.vue';
+import { useModal } from '/@/components/Modal';
+import { cloneDeep } from 'lodash-es';
+import { useDesign } from '/@/hooks/web/useDesign';
+//TODO 当字典租户隔离时,数据会查不到,默认一个
+const sexOption = getDictItemsByCode("sex") || [{text:'男',value:'1'},{text:'女',value:'2'}];
+const { createMessage } = useMessage();
+const userStore = useUserStore();
   const { prefixCls } = useDesign('j-base-setting-container');
-  //是否编辑
-  const isEdit = ref<boolean>(false);
-  //用户信息
-  const userInfo = ref<any>({});
-  //编辑时input触发事件
-  const accountNameEdit = ref();
-  const [registerModal, { openModal }] = useModal();
-  //头像动态计算
-  const avatar = computed(() => {
-    return getFileAccessHttpUrl(userInfo.value.avatar) || headerImg;
-  });
+//是否编辑
+const isEdit = ref<boolean>(false);
+//用户信息
+const userInfo = ref<any>({});
+//编辑时input触发事件
+const accountNameEdit = ref();
+const [registerModal, { openModal }] = useModal();
+//头像动态计算
+const avatar = computed(() => {
+  return getFileAccessHttpUrl(userInfo.value.avatar) || headerImg;
+});
 
-  /**
-   * 更新用户头像
-   */
-  function updateAvatar(src: string, data: string) {
-    const userinfo = userStore.getUserInfo;
-    userinfo.avatar = data;
-    userStore.setUserInfo(userinfo);
-    if (data) {
-      updateUserInfo({ avatar: data, id: userinfo.id });
-    }
+/**
+ * 更新用户头像
+ */
+function updateAvatar(src: string, data: string) {
+  const userinfo = userStore.getUserInfo;
+  userinfo.avatar = data;
+  userStore.setUserInfo(userinfo);
+  if (data) {
+    updateUserInfo({ avatar: data, id: userinfo.id });
   }
+}
 
-  /**
-   * 更新用户信息
-   * @params 参数
-   */
-  function updateUserInfo(params) {
-    userEdit(params).then((res) => {
-      if (!res.success) {
-        createMessage.warn(res.message);
-      }
-    });
-  }
+/**
+ * 更新用户信息
+ * @params 参数
+ */
+function updateUserInfo(params) {
+  userEdit(params).then((res) => {
+    if (!res.success) {
+      createMessage.warn(res.message);
+    }
+  });
+}
 
-  /**
-   * 编辑按钮点击事件
-   */
-  function editHandleClick() {
-    isEdit.value = true;
-    setTimeout(() => {
-      accountNameEdit.value.focus();
-    }, 100);
-  }
+/**
+ * 编辑按钮点击事件
+ */
+function editHandleClick() {
+  isEdit.value = true;
+  setTimeout(() => {
+    accountNameEdit.value.focus();
+  }, 100);
+}
 
-  /**
-   * 修改真实姓名
-   */
-  function editRealName() {
-    if (userInfo.value.realname) {
-      updateUserInfo({ realname: userInfo.value.realname, id: userInfo.value.id });
-      userStore.setUserInfo(userInfo.value);
-    } else {
-      createMessage.warn('请输入姓名');
-    }
-    isEdit.value = false;
+/**
+ * 修改真实姓名
+ */
+function editRealName() {
+  if (userInfo.value.realname) {
+    updateUserInfo({ realname: userInfo.value.realname, id: userInfo.value.id });
+    userStore.setUserInfo(userInfo.value);
+  } else {
+    createMessage.warn("请输入姓名");
   }
+  isEdit.value = false;
+}
 
-  /**
-   * 获取生日信息
-   */
-  function getBirthDay(val) {
-    if (val) {
-      return dayjs(val).format('YYYY-MM-DD');
-    } else {
-      return '未填写';
-    }
+/**
+ * 获取生日信息
+ */
+function getBirthDay(val) {
+  if (val) {
+    return dayjs(val).format("YYYY-MM-DD");
+  } else {
+    return "未填写";
   }
+}
 
-  /**
-   * 获取性别
-   * @param val
-   */
-  function getSex(val) {
-    let findOption = sexOption.find((item) => parseInt(item.value) === val);
-    let sex = '未填写';
-    if (findOption) {
-      sex = findOption.text;
-    }
-    return sex;
+/**
+ * 获取性别
+ * @param val
+ */
+function getSex(val) {
+  let findOption = sexOption.find(item => parseInt(item.value) === val);
+  let sex = "未填写";
+  if (findOption) {
+    sex = findOption.text;
   }
+  return sex;
+}
 
-  /**
-   * 打开编辑弹窗
-   */
-  function openEditModal() {
-    let value = cloneDeep(userInfo.value);
-    openModal(true, {
-      record: value,
-    });
-  }
+/**
+ * 打开编辑弹窗
+ */
+function openEditModal() {
+  let value = cloneDeep(userInfo.value);
+  openModal(true, {
+    record: value
+  });
+}
 
-  /**
-   * 获取用户信息
-   */
-  function getUserDetail() {
-    getUserData().then(async (res) => {
-      if (res.success) {
-        if (res.result) {
-          res.result.sexText = getSex(res.result.sex);
-          res.result.birthday = getBirthDay(res.result.birthday);
-          res.result.createTimeText = getDiffDay(res.result.createTime);
-          userInfo.value = res.result;
-        } else {
-          userInfo.value = {};
-        }
+/**
+ * 获取用户信息
+ */
+function getUserDetail() {
+  getUserData().then((async res => {
+    if (res.success) {
+      if (res.result) {
+        res.result.sexText = getSex(res.result.sex);
+        res.result.birthday = getBirthDay(res.result.birthday);
+        res.result.createTimeText = getDiffDay(res.result.createTime);
+        userInfo.value = res.result;
+      } else {
+        userInfo.value = {};
       }
-    });
-  }
+    }
+  }));
+}
 
-  /**
-   * 获取使用时间
-   * @param date
-   */
-  function getDiffDay(date) {
-    // 计算两个日期之间的天数差值
-    let totalDays, diffDate;
-    let createDate = Date.parse(date);
-    let nowDate = new Date().getTime();
-    // 将两个日期都转换为毫秒格式,然后做差
-    diffDate = Math.abs(nowDate - createDate); // 取相差毫秒数的绝对值
-    totalDays = Math.floor(diffDate / (1000 * 3600 * 24)); // 向下取整
-    return totalDays + ' 天';
-  }
-  onMounted(async () => {
-    getUserDetail();
-  });
+/**
+ * 获取使用时间
+ * @param date
+ */
+function getDiffDay(date) {
+  // 计算两个日期之间的天数差值
+  let totalDays, diffDate
+  let createDate = Date.parse(date);
+  let nowDate = new Date().getTime();
+  // 将两个日期都转换为毫秒格式,然后做差
+  diffDate = Math.abs(nowDate - createDate) // 取相差毫秒数的绝对值
+  totalDays = Math.floor(diffDate / (1000 * 3600 * 24)) // 向下取整
+  return totalDays+" 天";
+}
+onMounted(async () => {
+  getUserDetail();
+});
 </script>
 
 <style lang="less">
-  // update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
+    // update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
   @prefix-cls: ~'@{namespace}-j-base-setting-container';
 
-  .@{prefix-cls} {
+  .@{prefix-cls}{
     .user-setting-top {
       padding-top: 40px;
       width: 100%;

+ 395 - 382
src/views/system/usersetting/TenantSetting.vue

@@ -2,11 +2,9 @@
   <div class="tenant-padding" :class="[`${prefixCls}`]">
     <div class="my-tenant">
       <span style="flex: 1">我的组织</span>
-      <span class="invited" @click="invitedClick"
-        >我的受邀信息<span class="approved-count" v-if="invitedCount > 0">{{ invitedCount }}</span></span
-      >
+      <span class="invited" @click="invitedClick">我的受邀信息<span class="approved-count" v-if="invitedCount>0">{{invitedCount}}</span></span>
     </div>
-    <div class="tenant-list" v-if="dataSource.length > 0">
+    <div class="tenant-list" v-if="dataSource.length>0">
       <div v-for="item in dataSource" class="tenant-list-item" @click="drownClick(item)">
         <div class="tenant-title">
           <div class="item-left">
@@ -26,8 +24,8 @@
               <span class="pointer cancel-apply" @click.stop="cancelApplyClick(item.tenantUserId)">取消申请</span>
             </span>
             <span v-else-if="item.userTenantStatus === '5'">
-              <span class="pointer examine" @click="joinOrRefuseClick(item.tenantUserId, '1')">加入</span>
-              <span class="pointer cancel-apply" @click.stop="joinOrRefuseClick(item.tenantUserId, '4')">拒绝</span>
+              <span class="pointer examine" @click="joinOrRefuseClick(item.tenantUserId,'1')">加入</span>
+              <span class="pointer cancel-apply" @click.stop="joinOrRefuseClick(item.tenantUserId,'4')">拒绝</span>
             </span>
             <div v-else style="width: 75px"></div>
             <span style="margin-left: 24px">
@@ -42,7 +40,7 @@
             <div class="content-desc">
               <div class="flex-flow">
                 <div class="content-des-text">姓名</div>
-                <div style="font-size: 13px; color: #000000">
+                <div style="font-size: 13px;color: #000000">
                   {{ userDetail.realname }}
                 </div>
               </div>
@@ -89,7 +87,7 @@
         </div>
       </div>
     </div>
-    <a-empty v-else description="暂无数据" style="position: relative; top: 50px" />
+    <a-empty v-else description="暂无数据" style="position: relative;top: 50px;"/>
   </div>
   <a-modal v-model:open="tenantVisible" width="400px" wrapClassName="edit-tenant-setting">
     <template #title>
@@ -114,22 +112,26 @@
   <a-modal v-model:open="cancelVisible" width="800" destroy-on-close>
     <template #title>
       <div class="cancellation">
-        <Icon icon="ant-design:warning-outlined" style="font-size: 20px; color: red" />
-        退出租户 {{ myTenantInfo.name }}
+        <Icon icon="ant-design:warning-outlined" style="font-size: 20px;color: red"/>
+        退出租户 {{myTenantInfo.name}}
       </div>
     </template>
     <a-form :model="formCancelState" ref="cancelTenantRef">
       <a-form-item name="tenantName">
-        <a-row :span="24" style="padding: 20px 20px 0; font-size: 13px">
-          <a-col :span="24"> 请输入租户名称 </a-col>
+        <a-row :span="24" style="padding: 20px 20px 0;font-size: 13px">
+          <a-col :span="24">
+            请输入租户名称
+          </a-col>
           <a-col :span="24" style="margin-top: 10px">
-            <a-input v-model:value="formCancelState.tenantName" @change="tenantNameChange" />
+            <a-input v-model:value="formCancelState.tenantName" @change="tenantNameChange"/>
           </a-col>
         </a-row>
       </a-form-item>
       <a-form-item name="loginPassword">
-        <a-row :span="24" style="padding: 0 20px; font-size: 13px">
-          <a-col :span="24"> 请输入您的登录密码 </a-col>
+        <a-row :span="24" style="padding: 0 20px;font-size: 13px">
+          <a-col :span="24">
+            请输入您的登录密码
+          </a-col>
           <a-col :span="24" style="margin-top: 10px">
             <a-input-password v-model:value="formCancelState.loginPassword" />
           </a-col>
@@ -142,30 +144,42 @@
     </template>
   </a-modal>
 
-  <a-modal title="变更拥有者" v-model:open="owenVisible" width="800" destroy-on-close :cancelButtonProps="{ display: 'none' }" @ok="changeOwen">
-    <div style="padding: 20px">
-      <a-row :span="24">
-        <div class="change-owen"> 只有变更拥有着之后,才能退出 </div>
-      </a-row>
-      <a-row :span="24" style="margin-top: 10px">
-        <UserSelect v-model:value="tenantOwen" izExcludeMy />
-      </a-row>
-    </div>
+  <a-modal
+    title="变更拥有者"
+    v-model:open="owenVisible"
+    width="800"
+    destroy-on-close
+    :cancelButtonProps="{display:'none'}"
+    @ok="changeOwen">
+      <div style="padding: 20px">
+        <a-row :span="24">
+          <div class="change-owen">
+            只有变更拥有着之后,才能退出
+          </div>
+        </a-row>
+        <a-row :span="24" style="margin-top: 10px">
+          <UserSelect v-model:value="tenantOwen" izExcludeMy/>
+        </a-row>
+      </div>
   </a-modal>
-
+  
   <!-- begin 我的受邀信息 -->
   <a-modal title="我的受邀信息" v-model:open="invitedVisible" :footer="null">
-    <a-row :span="24" class="invited-row">
-      <a-col :span="16"> 组织 </a-col>
-      <a-col :span="8"> 操作 </a-col>
-    </a-row>
+      <a-row :span="24" class="invited-row">
+        <a-col :span="16">
+          组织
+        </a-col>
+        <a-col :span="8">
+          操作
+        </a-col>
+      </a-row>
     <a-row :span="24" class="invited-row-list" v-for="item in invitedList">
       <a-col :span="16">
-        {{ item.name }}
+        {{item.name}}
       </a-col>
       <a-col :span="8">
-        <span class="common" @click="joinOrRefuseClick(item.tenantUserId, '1')">加入</span>
-        <span class="common refuse" @click="joinOrRefuseClick(item.tenantUserId, '4')">拒绝</span>
+        <span class="common" @click="joinOrRefuseClick(item.tenantUserId,'1')">加入</span>
+        <span class="common refuse" @click="joinOrRefuseClick(item.tenantUserId,'4')">拒绝</span>
       </a-col>
     </a-row>
     <div style="height: 20px"></div>
@@ -174,59 +188,59 @@
 </template>
 
 <script lang="ts" name="tenant-setting" setup>
-  import { onMounted, ref, unref } from 'vue';
-  import { getTenantListByUserId, cancelApplyTenant, exitUserTenant, changeOwenUserTenant, agreeOrRefuseJoinTenant } from './UserSetting.api';
-  import { useUserStore } from '/@/store/modules/user';
-  import { CollapseContainer } from '/@/components/Container';
-  import { getFileAccessHttpUrl, userExitChangeLoginTenantId } from '/@/utils/common/compUtils';
-  import headerImg from '/@/assets/images/header.jpg';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  import { initDictOptions } from '/@/utils/dict';
-  import { uniqWith } from 'lodash-es';
-  import { Modal } from 'ant-design-vue';
-  import UserSelect from '/@/components/Form/src/jeecg/components/userSelect/index.vue';
-  import { router } from '/@/router';
-  import { useDesign } from '/@/hooks/web/useDesign';
-
-  const { prefixCls } = useDesign('j-user-tenant-setting-container');
-  //数据源
-  const dataSource = ref<any>([]);
-  const userStore = useUserStore();
-
-  //数据源
-  const { createMessage } = useMessage();
-  //部门字典
-  const departOptions = ref<any>([]);
-  //租户编辑是或否隐藏
-  const tenantVisible = ref<boolean>(false);
-  //用户数据
-  const userData = ref<any>([]);
-  //用户
-  const userDetail = ref({
-    realname: userStore.getUserInfo.realname,
-    workNo: userStore.getUserInfo.workNo,
-    orgCodeTxt: userStore.getUserInfo.orgCodeTxt,
-    postText: userStore.getUserInfo.postText,
-  });
-  /**
-   * 初始化租户数据
-   */
+import { onMounted, ref, unref } from "vue";
+import { getTenantListByUserId, cancelApplyTenant, exitUserTenant, changeOwenUserTenant, agreeOrRefuseJoinTenant } from "./UserSetting.api";
+import { useUserStore } from "/@/store/modules/user";
+import { CollapseContainer } from "/@/components/Container";
+import { getFileAccessHttpUrl, userExitChangeLoginTenantId } from "/@/utils/common/compUtils";
+import headerImg from "/@/assets/images/header.jpg";
+import {useMessage} from "/@/hooks/web/useMessage";
+import { initDictOptions } from '/@/utils/dict';
+import { uniqWith } from 'lodash-es';
+import { Modal } from 'ant-design-vue';
+import UserSelect from '/@/components/Form/src/jeecg/components/userSelect/index.vue';
+import {router} from "/@/router";
+import { useDesign } from '/@/hooks/web/useDesign';
+
+const { prefixCls } = useDesign('j-user-tenant-setting-container');
+//数据源
+const dataSource = ref<any>([]);
+const userStore = useUserStore();
+
+//数据源
+const { createMessage } = useMessage();
+//部门字典
+const departOptions = ref<any>([]);
+//租户编辑是或否隐藏
+const tenantVisible = ref<boolean>(false);
+//用户数据
+const userData = ref<any>([]);
+//用户
+const userDetail = ref({
+  realname: userStore.getUserInfo.realname,
+  workNo: userStore.getUserInfo.workNo,
+  orgCodeTxt: userStore.getUserInfo.orgCodeTxt,
+  postText: userStore.getUserInfo.postText,
+});
+/**
+ * 初始化租户数据
+ */
   async function initDataSource() {
-    //获取用户数据
+  //获取用户数据
     //update-begin---author:wangshuai ---date:20230109  for: [QQYUN-3645]个人设置我的租户查询审核中和正常的------------
     //update-begin---author:wangshuai ---date:202307049  for:[QQYUN-5608]用户导入后,邀请后,被导入人同意即可,新增被邀信息-----------
     getTenantListByUserId({ userTenantStatus: '1,3,5' }).then((res) => {
       if (res.success) {
-        if (res.result && res.result.length > 0) {
+        if(res.result && res.result.length>0){
           let result = res.result;
           //存放正常和审核中的数组
-          let normal: any = [];
+          let normal:any = [];
           //存放受邀的信息
-          let invited: any = [];
+          let invited:any = [];
           for (let i = 0; i < result.length; i++) {
             let status = result[i].userTenantStatus;
             //状态为邀请的放入invited数组中
-            if (status === '5') {
+            if(status === '5'){
               invited.push(result[i]);
             }
             normal.push(result[i]);
@@ -234,12 +248,12 @@
           dataSource.value = normal;
           invitedList.value = invited;
           invitedCount.value = invited.length;
-        } else {
+        }else{
           setInitedValue();
         }
       } else {
         setInitedValue();
-        //update-end---author:wangshuai ---date:202307049  for:[QQYUN-5608]用户导入后,邀请后,被导入人同意即可,新增被邀信息------------
+    //update-end---author:wangshuai ---date:202307049  for:[QQYUN-5608]用户导入后,邀请后,被导入人同意即可,新增被邀信息------------
       }
     });
     //update-end---author:wangshuai ---date:20230109  for:[QQYUN-3645]个人设置我的租户查询审核中和正常的------------
@@ -247,7 +261,7 @@
   function setInitedValue() {
     dataSource.value = [];
     invitedList.value = [];
-    invitedCount.value = 0;
+    invitedCount.value = 0;  
   }
 
   /**
@@ -268,7 +282,7 @@
     // 删除input元素
     document.body.removeChild(el);
     createMessage.success('复制成功');
-  }
+  };
 
   /**
    * 取消申请
@@ -281,21 +295,19 @@
       okText: '确认',
       cancelText: '取消',
       onOk: () => {
-        cancelApplyTenant({ tenantId: id })
-          .then((res) => {
-            if (res.success) {
-              createMessage.success('取消申请成功');
-              initDataSource();
-            } else {
-              createMessage.warning(res.message);
-            }
-          })
-          .catch((e) => {
-            createMessage.warning(e.message);
-          });
+        cancelApplyTenant({ tenantId: id }).then((res) => {
+          if (res.success) {
+            createMessage.success('取消申请成功');
+            initDataSource();
+          }else{
+            createMessage.warning(res.message);
+          }
+        }).catch((e)=>{
+           createMessage.warning(e.message);
+        });
       },
     });
-  }
+  };
 
   /**
    * 展开关闭事件
@@ -306,7 +318,7 @@
     } else {
       value.show = false;
     }
-  }
+  };
 
   /**
    * 获取部门文本
@@ -320,7 +332,7 @@
       return arr[0].label;
     }
     return '未填写';
-  }
+  };
 
   /**
    * 底部文本点击事件
@@ -330,9 +342,9 @@
     //编辑组织名片
     if (type === 'editTenant') {
       tenantVisible.value = true;
-    } else if (type === 'exitTenant') {
+    }else if(type === 'exitTenant'){
       //退出租户
-      formCancelState.value = { loginPassword: '', tenantName: '' };
+      formCancelState.value = {loginPassword:'', tenantName:''};
       outBtnDisabled.value = true;
       cancelVisible.value = true;
       myTenantInfo.value = item;
@@ -358,9 +370,9 @@
   function tenantNameChange() {
     let name = unref(myTenantInfo).name;
     let tenantName = unref(formCancelState).tenantName;
-    if (name === tenantName) {
+    if(name === tenantName){
       outBtnDisabled.value = false;
-    } else {
+    }else{
       outBtnDisabled.value = true;
     }
   }
@@ -369,49 +381,47 @@
    * 退出确定点击事件
    */
   async function handleOutClick() {
-    if (!unref(formCancelState).loginPassword) {
-      createMessage.warning('请输入登录密码');
-      return;
+    if(!unref(formCancelState).loginPassword){
+        createMessage.warning("请输入登录密码");
+        return;
     }
-    console.log('myTenantInfo::::', myTenantInfo);
-    await exitUserTenant({ id: unref(myTenantInfo).tenantUserId, loginPassword: unref(formCancelState).loginPassword })
-      .then((res) => {
-        if (res.success) {
-          createMessage.success(res.message);
+    console.log("myTenantInfo::::",myTenantInfo);
+    await exitUserTenant({ id: unref(myTenantInfo).tenantUserId, loginPassword: unref(formCancelState).loginPassword }).then((res) => {
+      if (res.success) {
+        createMessage.success(res.message);
+        cancelVisible.value = false;
+        initDataSource();
+        userExitChangeLoginTenantId(unref(myTenantInfo).tenantUserId);
+      } else {
+        if (res.message === 'assignedOwen') {
+          //需要指定变更者
+          owenVisible.value = true;
+          cancelVisible.value = false;
+        //update-begin---author:wangshuai ---date:20230426  for:【QQYUN-5270】名下租户全部退出后,再次登录,提示租户全部冻结。拥有者提示前往注销------------
+        }else if(res.message === 'cancelTenant'){
           cancelVisible.value = false;
-          initDataSource();
-          userExitChangeLoginTenantId(unref(myTenantInfo).tenantUserId);
+          let fullPath = router.currentRoute.value.fullPath;
+          Modal.confirm({
+            title: '您是该组织的拥有者',
+            content: '该组织下没有其他成员,需要您前往注销',
+            okText: '前往注销',
+            okType: 'danger',
+            cancelText: '取消',
+            onOk: () => {
+              if(fullPath === '/system/usersetting'){
+                return;
+              }
+              router.push('/myapps/settings/organization/organMessage/'+unref(myTenantInfo).tenantUserId)
+            }
+          })
+        //update-end---author:wangshuai ---date:20230426  for:【QQYUN-5270】名下租户全部退出后,再次登录,提示租户全部冻结。拥有者提示前往注销------------
         } else {
-          if (res.message === 'assignedOwen') {
-            //需要指定变更者
-            owenVisible.value = true;
-            cancelVisible.value = false;
-            //update-begin---author:wangshuai ---date:20230426  for:【QQYUN-5270】名下租户全部退出后,再次登录,提示租户全部冻结。拥有者提示前往注销------------
-          } else if (res.message === 'cancelTenant') {
-            cancelVisible.value = false;
-            let fullPath = router.currentRoute.value.fullPath;
-            Modal.confirm({
-              title: '您是该组织的拥有者',
-              content: '该组织下没有其他成员,需要您前往注销',
-              okText: '前往注销',
-              okType: 'danger',
-              cancelText: '取消',
-              onOk: () => {
-                if (fullPath === '/system/usersetting') {
-                  return;
-                }
-                router.push('/myapps/settings/organization/organMessage/' + unref(myTenantInfo).tenantUserId);
-              },
-            });
-            //update-end---author:wangshuai ---date:20230426  for:【QQYUN-5270】名下租户全部退出后,再次登录,提示租户全部冻结。拥有者提示前往注销------------
-          } else {
-            createMessage.warning(res.message);
-          }
+          createMessage.warning(res.message);
         }
-      })
-      .catch((res) => {
-        createMessage.warning(res.message);
-      });
+      }
+    }).catch((res) => {
+      createMessage.warning(res.message);
+    })
   }
 
   /**
@@ -426,12 +436,12 @@
    * 变更拥有着
    */
   function changeOwen() {
-    if (!unref(tenantOwen)) {
-      createMessage.warning('请选择变更拥有者');
+    if(!unref(tenantOwen)){
+      createMessage.warning("请选择变更拥有者");
       return;
     }
-    changeOwenUserTenant({ userId: unref(tenantOwen), tenantId: unref(myTenantInfo).tenantUserId }).then((res) => {
-      if (res.success) {
+    changeOwenUserTenant({ userId:unref(tenantOwen), tenantId:unref(myTenantInfo).tenantUserId }).then((res) =>{
+      if(res.success){
         createMessage.success(res.message);
         initDataSource();
         //update-begin---author:wangshuai---date:2023-10-23---for:【QQYUN-6822】7、登录拥有多个租户身份的用户,退出租户,只剩下一个租户后显示为空---
@@ -440,9 +450,9 @@
       } else {
         createMessage.warning(res.message);
       }
-    });
+    })
   }
-
+  
   //邀请数量
   const invitedCount = ref<number>(0);
   //受邀信息
@@ -460,306 +470,309 @@
   /**
    * 加入组织点击事件
    */
-  async function joinOrRefuseClick(tenantId, status) {
-    await agreeOrRefuseJoinTenant({ tenantId: Number.parseInt(tenantId), status: status });
+  async function joinOrRefuseClick(tenantId,status) {
+    await agreeOrRefuseJoinTenant( { tenantId:Number.parseInt(tenantId), status:status });
     initDataSource();
   }
 
   onMounted(() => {
     initDataSource();
   });
+
 </script>
 
 <style lang="less" scoped>
-  .tenant-padding {
-    padding: 30px 40px 0 20px;
+.tenant-padding{
+  padding: 30px 40px 0 20px;
+}
+.my-tenant{
+  display: flex;
+  font-size: 17px;
+  font-weight: 700!important;
+  /*begin 兼容暗夜模式*/
+  color: @text-color;
+  /*end 兼容暗夜模式*/
+  margin-bottom: 20px;
+  .invited{
+    font-size: 14px;
+    text-align: right;
+    cursor: pointer;
   }
-  .my-tenant {
+}
+.tenant-list{
+  box-sizing: border-box;
+  flex: 1;
+  min-height: 0;
+  overflow-x: hidden;
+}
+.tenant-list-item{
+  /*begin 兼容暗夜模式*/
+  border: 1px solid @border-color-base;
+  /*end 兼容暗夜模式*/
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  margin-bottom: 20px;
+  overflow: hidden;
+  padding: 0 25px;
+  width: 100%;
+  .item-name{
+    align-items: center;
+    box-sizing: border-box;
     display: flex;
-    font-size: 17px;
-    font-weight: 700 !important;
+    justify-content: space-between;
+    padding: 14px 0;
+    cursor: pointer;
+    font-size:17px;
     /*begin 兼容暗夜模式*/
     color: @text-color;
     /*end 兼容暗夜模式*/
-    margin-bottom: 20px;
-    .invited {
-      font-size: 14px;
-      text-align: right;
-      cursor: pointer;
-    }
-  }
-  .tenant-list {
-    box-sizing: border-box;
-    flex: 1;
-    min-height: 0;
-    overflow-x: hidden;
-  }
-  .tenant-list-item {
+    font-weight: 700!important;
+  }
+}
+.tenant-list-item:hover{
+  box-shadow: 0 1px 2px 0 rgba(0,0,0,0.2);
+}
+.pointer {
+  cursor: pointer;
+}
+
+.examine {
+  color: #2c9cff;
+  font-size: 13px;
+}
+
+.cancel-apply {
+  margin-left: 24px;
+  color: red;
+  font-size: 13px;
+}
+
+.item-content {
+  transition: ease-in 2s;
+
+  .content-box {
     /*begin 兼容暗夜模式*/
-    border: 1px solid @border-color-base;
+    border-top: 1px solid @border-color-base;
     /*end 兼容暗夜模式*/
     box-sizing: border-box;
     display: flex;
-    flex-direction: column;
-    margin-bottom: 20px;
-    overflow: hidden;
-    padding: 0 25px;
-    width: 100%;
-    .item-name {
-      align-items: center;
-      box-sizing: border-box;
-      display: flex;
-      justify-content: space-between;
-      padding: 14px 0;
-      cursor: pointer;
-      font-size: 17px;
-      /*begin 兼容暗夜模式*/
-      color: @text-color;
-      /*end 兼容暗夜模式*/
-      font-weight: 700 !important;
-    }
-  }
-  .tenant-list-item:hover {
-    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
-  }
-  .pointer {
-    cursor: pointer;
-  }
-
-  .examine {
-    color: #2c9cff;
-    font-size: 13px;
+    padding: 24px 0;
   }
 
-  .cancel-apply {
-    margin-left: 24px;
-    color: red;
+  .content-name {
+    /*begin 兼容暗夜模式*/
+    color: @text-color;
+    /*end 兼容暗夜模式*/
+    text-align: center;
+    width: 100px;
     font-size: 13px;
   }
 
-  .item-content {
-    transition: ease-in 2s;
-
-    .content-box {
-      /*begin 兼容暗夜模式*/
-      border-top: 1px solid @border-color-base;
-      /*end 兼容暗夜模式*/
-      box-sizing: border-box;
-      display: flex;
-      padding: 24px 0;
-    }
-
-    .content-name {
-      /*begin 兼容暗夜模式*/
-      color: @text-color;
-      /*end 兼容暗夜模式*/
-      text-align: center;
-      width: 100px;
-      font-size: 13px;
-    }
-
-    .content-desc {
-      flex: 1;
-      min-width: 0;
-    }
-
-    .content-des-text {
-      /*begin 兼容暗夜模式*/
-      color: @text-color;
-      /*end 兼容暗夜模式*/
-      text-align: left;
-      width: 76px;
-      font-size: 13px;
-    }
-  }
-
-  .flex-flow {
-    display: flex;
-    min-width: 0;
-  }
-
-  .flex-center {
-    display: flex;
-    justify-content: center;
-    align-items: center;
+  .content-desc {
+    flex: 1;
     min-width: 0;
   }
 
-  .footer-box {
+  .content-des-text {
     /*begin 兼容暗夜模式*/
-    border-top: 1px solid @border-color-base;
+    color: @text-color;
     /*end 兼容暗夜模式*/
-    box-sizing: border-box;
-    display: flex;
-    padding: 24px 0;
-    color: #757575;
-  }
-
-  .margin-right40 {
-    margin-right: 40px;
+    text-align: left;
+    width: 76px;
+    font-size: 13px;
   }
+}
 
-  /*begin 兼容暗夜模式*/
-  .font-color333 {
-    color: @text-color;
-    font-weight: normal;
-  }
+.flex-flow {
+  display: flex;
+  min-width: 0;
+}
 
-  .font-color9e {
-    color: @text-color;
-  }
+.flex-center {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-width: 0;
+}
 
-  .font-color75 {
-    color: @text-color;
-  }
+.footer-box {
+  /*begin 兼容暗夜模式*/
+  border-top: 1px solid @border-color-base;
   /*end 兼容暗夜模式*/
-
-  .font-size13 {
-    font-size: 13px;
-  }
-
-  .footer-icon {
-    font-size: 13px !important;
-    margin-right: 13px;
-    position: relative;
-    top: 0px;
-  }
-  :deep(.edit-tenant-setting) {
-    color: #0a8fe9;
-  }
-  .margin-top6 {
-    margin-top: 6px;
-  }
-  .margin-bottom-16 {
-    margin-bottom: 16px;
-  }
-  .item-right {
-    align-items: center;
-    display: flex;
-    .buy-margin {
-      margin-left: 10px;
-      width: 66px;
-      border-radius: 20px;
-      background: rgba(255, 154, 0, 1);
-      height: 28px;
-      line-height: 28px;
-      cursor: pointer;
-      text-align: center;
-      span {
-        font-size: 14px;
-        font-weight: 400;
-        color: #ffffff;
-      }
+  box-sizing: border-box;
+  display: flex;
+  padding: 24px 0;
+  color: #757575;
+}
+
+.margin-right40 {
+  margin-right: 40px;
+}
+
+/*begin 兼容暗夜模式*/
+.font-color333 {
+  color: @text-color;
+  font-weight: normal;
+}
+
+.font-color9e {
+  color: @text-color;
+}
+
+.font-color75 {
+  color: @text-color;
+}
+/*end 兼容暗夜模式*/
+
+.font-size13 {
+  font-size: 13px;
+}
+
+.footer-icon {
+  font-size: 13px !important;
+  margin-right: 13px;
+  position: relative;
+  top: 0px;
+}
+:deep(.edit-tenant-setting) {
+  color: #0a8fe9;
+}
+.margin-top6 {
+  margin-top: 6px;
+}
+.margin-bottom-16 {
+  margin-bottom: 16px;
+}
+.item-right {
+  align-items: center;
+  display: flex;
+  .buy-margin{
+    margin-left: 10px;
+    width: 66px;
+    border-radius: 20px;
+    background: rgba(255, 154, 0, 1);
+    height: 28px;
+    line-height: 28px;
+    cursor: pointer;
+    text-align: center;
+    span{
+      font-size: 14px;
+      font-weight: 400;
+      color: #ffffff;
     }
-    .ordinary-user {
-      margin-left: 10px;
-      width: 66px;
-      span {
-        font-size: 14px;
-        font-weight: 400;
-        color: #9e9e9e;
-      }
+  }
+  .ordinary-user{
+    margin-left: 10px;
+    width: 66px;
+    span{
+      font-size: 14px;
+      font-weight: 400;
+      color: #9e9e9e;
     }
   }
-  .tenant-title {
-    align-items: center;
-    box-sizing: border-box;
+}
+.tenant-title {
+  align-items: center;
+  box-sizing: border-box;
+  display: flex;
+  justify-content: space-between;
+  padding: 24px 0;
+  .vip-message{
     display: flex;
-    justify-content: space-between;
-    padding: 24px 0;
-    .vip-message {
-      display: flex;
-      .vip-message-margin {
-        margin-right: 20px;
-      }
+    .vip-message-margin{
+      margin-right: 20px;
     }
   }
-  .change-owen {
-    font-size: 14px;
-    font-weight: 700;
+}
+.change-owen{
+  font-size: 14px;
+  font-weight: 700;
+}
+//update-begin---author:wangshuai ---date:20230704  for:被邀弹窗样式------------
+.approved-count{
+  background: #ffd2d2;
+  border-radius: 19px;
+  color: red;
+  display: inline-block;
+  font-weight: 500;
+  height: 19px;
+  line-height: 18px;
+  margin-left: 8px;
+  min-width: 19px;
+  padding: 0 6px;
+  text-align: center;
+}
+
+.invited-row{
+  padding: 10px 34px;
+}
+.invited-row-list{
+  padding: 0px 34px;
+  .common{
+    color: #1e88e5;
+    cursor: pointer;
   }
-  //update-begin---author:wangshuai ---date:20230704  for:被邀弹窗样式------------
-  .approved-count {
-    background: #ffd2d2;
-    border-radius: 19px;
+  .refuse{
     color: red;
-    display: inline-block;
-    font-weight: 500;
-    height: 19px;
-    line-height: 18px;
-    margin-left: 8px;
-    min-width: 19px;
-    padding: 0 6px;
-    text-align: center;
-  }
-
-  .invited-row {
-    padding: 10px 34px;
-  }
-  .invited-row-list {
-    padding: 0px 34px;
-    .common {
-      color: #1e88e5;
-      cursor: pointer;
-    }
-    .refuse {
-      color: red;
-      margin-left: 20px;
-    }
-  }
-  .pointer {
-    cursor: pointer;
+    margin-left: 20px;
   }
-  //update-end---author:wangshuai ---date:20230704  for:被邀弹窗样式------------
+}
+.pointer{
+  cursor: pointer;
+}
+//update-end---author:wangshuai ---date:20230704  for:被邀弹窗样式------------
 </style>
 
 <style lang="less">
   // update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
-  @prefix-cls: ~'@{namespace}-j-user-tenant-setting-container';
-  /*begin 兼容暗夜模式*/
-  .@{prefix-cls} {
-    .my-tenant {
-      color: @text-color;
-    }
+@prefix-cls: ~'@{namespace}-j-user-tenant-setting-container';
+/*begin 兼容暗夜模式*/
+.@{prefix-cls} {
 
-    .tenant-list-item {
-      border: 1px solid @border-color-base;
+  .my-tenant{
+    color: @text-color;
+  }
 
-      .item-name {
-        color: @text-color;
-      }
-    }
+  .tenant-list-item{
+    border: 1px solid @border-color-base;
 
-    .item-content {
-      .content-box {
-        border-top: 1px solid @border-color-base;
-      }
+    .item-name{
+      color: @text-color;
+    }
+  }
 
-      .content-name {
-        color: @text-color;
-      }
+  .item-content {
 
-      .content-des-text {
-        color: @text-color;
-      }
-    }
-    .footer-box {
+    .content-box {
       border-top: 1px solid @border-color-base;
     }
 
-    /*begin 兼容暗夜模式*/
-    .font-color333 {
+    .content-name {
       color: @text-color;
     }
 
-    .font-color9e {
+    .content-des-text {
       color: @text-color;
     }
+  }
+  .footer-box {
+    border-top: 1px solid @border-color-base;
+  }
 
-    .font-color75 {
-      color: @text-color;
-    }
+  /*begin 兼容暗夜模式*/
+  .font-color333 {
+    color: @text-color;
   }
-  /*end 兼容暗夜模式*/
+
+  .font-color9e {
+    color: @text-color;
+  }
+
+  .font-color75 {
+    color: @text-color;
+  }
+}
+/*end 兼容暗夜模式*/
   // update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
 </style>