Jelajahi Sumber

feat(架构): 增加切换租户功能

shizhongming 2 tahun lalu
induk
melakukan
b1574c7828

+ 26 - 0
src/api/sys/user.ts

@@ -15,6 +15,8 @@ enum Api {
   GetPermCode = '/getPermCode',
   TestRetry = '/testRetry',
   changePassword = 'sys/auth/changePassword',
+  listCurrentUserTenant = 'sys/tenant/manager/listCurrentUserTenant',
+  changeTenant = '/auth/tenant/change',
 }
 
 /**
@@ -72,3 +74,27 @@ export const changePasswordApi = (params: ChangePasswordParams) => {
     data: params,
   });
 };
+
+/**
+ * 查询当前用户租户列表
+ */
+export const listCurrentUserTenantApi = () => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.listCurrentUserTenant,
+  });
+};
+
+/**
+ * 切换租户API
+ * @param tenantId
+ */
+export const changeTenantApi = (tenantId: number): Promise<LoginResultModel> => {
+  return defHttp.postForm({
+    service: ApiServiceEnum.SMART_AUTH,
+    url: Api.changeTenant,
+    params: {
+      tenantId,
+    },
+  });
+};

+ 1 - 1
src/hooks/web/usePermission.ts

@@ -115,7 +115,7 @@ export function usePermission() {
    * refresh menu data
    */
   async function refreshMenu() {
-    resume();
+    return resume();
   }
 
   const getNoPermissionMode = computed(() => appStore.getProjectConfig.noPermissionMode);

+ 95 - 0
src/layouts/default/header/components/ChangeTenant/ChangeTenantModal.vue

@@ -0,0 +1,95 @@
+<template>
+  <BasicModal
+    v-bind="$attrs"
+    :title="t('layout.header.changeTenant')"
+    @register="registerModal"
+    @open-change="handleOpenChange"
+    @ok="handleOk"
+  >
+    <BasicForm @register="registerForm">
+      <template #tenant="{ model, field }">
+        <RadioGroup v-model:value="model[field]">
+          <Radio v-for="item in tenantListRef" :key="item.id" :value="item.id" :style="radioStyle">
+            {{ item.tenantCode }}: {{ item.tenantShortName || item.tenantName }}
+          </Radio>
+        </RadioGroup>
+      </template>
+    </BasicForm>
+  </BasicModal>
+</template>
+
+<script setup lang="ts">
+  import { nextTick, ref, unref } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { Radio } from 'ant-design-vue';
+  import { BasicModal, useModalInner } from '@/components/Modal';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { BasicForm, useForm } from '@/components/Form';
+  import { useUserStore } from '@/store/modules/user';
+
+  import { listCurrentUserTenantApi } from '@/api/sys/user';
+  import { successMessage } from '@/utils/message/SystemNotice';
+
+  const RadioGroup = Radio.Group;
+
+  const { t } = useI18n();
+  const userStore = useUserStore();
+  const { changeTenant } = userStore;
+  const { getUserTenant } = storeToRefs(userStore);
+
+  const tenantListRef = ref<Recordable[]>();
+  const handleOpenChange = async (visible: boolean) => {
+    if (visible) {
+      nextTick(() => {
+        setFieldsValue({
+          tenantId: unref(getUserTenant)?.tenantId,
+        });
+      });
+      try {
+        changeLoading(true);
+        tenantListRef.value = await listCurrentUserTenantApi();
+      } finally {
+        changeLoading(false);
+      }
+    } else {
+      tenantListRef.value = [];
+    }
+  };
+
+  const handleOk = async () => {
+    const { tenantId } = await validate();
+    if (tenantId === unref(getUserTenant)?.tenantId) {
+      closeModal();
+      return false;
+    }
+    try {
+      changeOkLoading(true);
+      await changeTenant(tenantId);
+      successMessage(t('layout.header.changeTenantSuccess'));
+      closeModal();
+    } finally {
+      changeOkLoading(false);
+    }
+  };
+
+  const [registerModal, { changeLoading, closeModal, changeOkLoading }] = useModalInner();
+  const [registerForm, { setFieldsValue, validate }] = useForm({
+    showActionButtonGroup: false,
+    schemas: [
+      {
+        field: 'tenantId',
+        label: t('layout.header.changeTenant'),
+        slot: 'tenant',
+        required: true,
+      },
+    ],
+  });
+
+  const radioStyle = ref({
+    display: 'flex',
+    height: '30px',
+    lineHeight: '30px',
+  });
+</script>
+
+<style scoped lang="less"></style>

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

@@ -4,7 +4,7 @@
       <img :class="`${prefixCls}__header`" :src="getUserInfo.avatar" />
       <span :class="`${prefixCls}__info hidden md:block`">
         <span :class="`${prefixCls}__name`" class="truncate">
-          {{ getUserInfo.realName }}
+          {{ computedUsername }}
         </span>
       </span>
     </span>
@@ -18,6 +18,11 @@
           v-if="getShowDoc"
         />
         <Menu.Divider v-if="getShowDoc" />
+        <MenuItem
+          key="changeTenant"
+          :text="t('layout.header.changeTenant')"
+          icon="ant-design:usergroup-add-outlined"
+        />
         <MenuItem
           v-if="getShowApi"
           key="api"
@@ -46,11 +51,12 @@
   <LockAction @register="register" />
   <ChangeApi @register="registerApi" />
   <ChangePasswordModal @register="registerChangePasswordModal" />
+  <ChangeTenantModal @register="registerTenantModal" />
 </template>
 <script lang="ts" setup>
   import { Dropdown, Menu } from 'ant-design-vue';
   import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
-  import { computed } from 'vue';
+  import { computed, unref } from 'vue';
   import { DOC_URL } from '@/settings/siteSetting';
   import { useUserStore } from '@/store/modules/user';
   import { useHeaderSetting } from '@/hooks/setting/useHeaderSetting';
@@ -62,8 +68,9 @@
   import { openWindow } from '@/utils';
   import { createAsyncComponent } from '@/utils/factory/createAsyncComponent';
   import ChangePasswordModal from './ChangePasswordModal.vue';
+  import ChangeTenantModal from '../ChangeTenant/ChangeTenantModal.vue';
 
-  type MenuEvent = 'logout' | 'doc' | 'lock' | 'api' | 'changePassword';
+  type MenuEvent = 'logout' | 'doc' | 'lock' | 'api' | 'changePassword' | 'changeTenant';
 
   const MenuItem = createAsyncComponent(() => import('./DropMenuItem.vue'));
   const LockAction = createAsyncComponent(() => import('../lock/LockModal.vue'));
@@ -81,12 +88,22 @@
   const userStore = useUserStore();
 
   const getUserInfo = computed(() => {
-    const { realName = '', avatar, desc } = userStore.getUserInfo || {};
-    return { realName, avatar: avatar || headerImg, desc };
+    const { realName = '', avatar, desc, userTenant } = userStore.getUserInfo || {};
+    return { realName, avatar: avatar || headerImg, desc, userTenant };
+  });
+
+  const computedUsername = computed(() => {
+    const { realName, userTenant } = unref(getUserInfo);
+    const list = [realName];
+    if (userTenant) {
+      list.push(userTenant.tenantShortName || userTenant.tenantName);
+    }
+    return list.join('/');
   });
 
   const [register, { openModal }] = useModal();
   const [registerApi, { openModal: openApiModal }] = useModal();
+  const [registerTenantModal, { openModal: openTenantModal }] = useModal();
 
   function handleLock() {
     openModal(true);
@@ -123,6 +140,9 @@
       case 'changePassword':
         handleChangePassword();
         break;
+      case 'changeTenant':
+        openTenantModal();
+        break;
     }
   }
 

+ 3 - 1
src/locales/lang/en/layout.json

@@ -16,7 +16,9 @@
     "lockScreen": "Lock screen",
     "lockScreenBtn": "Locking",
     "home": "Home",
-    "changePassword": "Change password"
+    "changePassword": "Change password",
+    "changeTenant": "Change tenant",
+    "changeTenantSuccess": "Change tenant success"
   },
   "multipleTab": {
     "reload": "Refresh current",

+ 3 - 1
src/locales/lang/zh-CN/layout.json

@@ -16,7 +16,9 @@
     "lockScreen": "锁定屏幕",
     "lockScreenBtn": "锁定",
     "home": "首页",
-    "changePassword": "修改密码"
+    "changePassword": "修改密码",
+    "changeTenant": "切换租户",
+    "changeTenantSuccess": "切换租户成功"
   },
   "multipleTab": {
     "reload": "重新加载",

+ 13 - 0
src/modules/smart-system/views/tenant/tenantManager/SysTenantListView.api.ts

@@ -17,6 +17,7 @@ enum Api {
   getSubscribeById = '/sys/tenant/subscribe/getById',
   batchSaveUpdateSubscribe = '/sys/tenant/subscribe/saveUpdateBatch',
   listNoBindPackageByTenantId = '/sys/tenant/manager/listNoBindPackageByTenantId',
+  createTenantUserAccount = '/sys/tenant/manager/createTenantUserAccount',
 }
 
 export const listApi = (params) => {
@@ -163,3 +164,15 @@ export const batchSaveUpdateSubscribeApi = (data: Recordable) => {
     data,
   });
 };
+
+/**
+ * 根据租户ID查询没有绑定的套餐
+ * @param data
+ */
+export const createTenantUserAccountApi = (data: Recordable) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.createTenantUserAccount,
+    data,
+  });
+};

+ 21 - 1
src/modules/smart-system/views/tenant/tenantManager/SysTenantListView.config.ts

@@ -346,7 +346,7 @@ export const getSearchFormSchemas = (t: Function): SmartSearchFormSchema[] => {
 /**
  * 绑定用户tab
  */
-export const getTabUserListColumns = (): SmartColumn[] => {
+export const getTabUserListColumns = (t: Function): SmartColumn[] => {
   return [
     {
       type: 'checkbox',
@@ -367,6 +367,26 @@ export const getTabUserListColumns = (): SmartColumn[] => {
       width: 120,
       fixed: 'left',
     },
+    {
+      field: 'accountId',
+      title: '{system.views.tenant.manager.title.user.hasAccount}',
+      formatter({ row }) {
+        const accountId = row.accountId;
+        if (accountId) {
+          return t('common.form.yes');
+        }
+        return t('common.form.no');
+      },
+      align: 'center',
+      className({ row }) {
+        const accountId = row.accountId;
+        if (accountId) {
+          return 'text-color--success-bold';
+        }
+        return 'text-color--danger-bold';
+      },
+      width: 100,
+    },
     {
       title: '{system.views.tenant.manager.title.user.email}',
       field: 'email',

+ 51 - 3
src/modules/smart-system/views/tenant/tenantManager/components/TenantUserList.vue

@@ -17,11 +17,18 @@
     getTabUserListSearchSchemas,
     Permission,
   } from '../SysTenantListView.config';
-  import { listTenantUserApi, removeBindUserApi } from '../SysTenantListView.api';
+  import {
+    listTenantUserApi,
+    removeBindUserApi,
+    createTenantUserAccountApi,
+  } from '../SysTenantListView.api';
   import { useDesign } from '@/hooks/web/useDesign';
   import { useI18n } from '@/hooks/web/useI18n';
   import TenantAddUserModal from './TenantAddUserModal.vue';
   import { hasPermission } from '@/utils/auth';
+  import { useUserStore } from '@/store/modules/user';
+  import { storeToRefs } from 'pinia';
+  import { createConfirm, successMessage, warnMessage } from '@/utils/message/SystemNotice';
 
   const props = defineProps({
     tenantId: propTypes.number,
@@ -31,6 +38,7 @@
     () => query(),
   );
   const computedChoseTenant = computed(() => props.tenantId !== undefined);
+  const { getIsPlatformTenant } = storeToRefs(useUserStore());
 
   const { tableSizeConfig } = useSizeSetting();
   const { prefixCls } = useDesign('system-tenant-manager-userTab');
@@ -38,9 +46,39 @@
 
   const [registerAddUserModal, { openModal: openAddUserModal }] = useModal();
 
-  const [register, { query }] = useSmartTable({
+  const handleCreateAccount = () => {
+    const selectRows = getCheckboxRecords();
+    if (selectRows.length === 0) {
+      warnMessage(t('system.views.tenant.manager.message.selectUser'));
+      return false;
+    }
+    // 验证是否已经存在创建账户的
+    const createAccountList = selectRows.filter((item) => item.accountId === null);
+    if (createAccountList.length === 0) {
+      warnMessage(t('system.views.tenant.manager.message.所选用户已全部创建账户'));
+      return false;
+    }
+    let i18nKey = 'system.views.tenant.manager.message.createAccountConfirm';
+    if (createAccountList.length < selectRows.length) {
+      i18nKey = 'system.views.tenant.manager.message.hasCreateAccount';
+    }
+    createConfirm({
+      content: t(i18nKey),
+      iconType: 'warning',
+      async onOk() {
+        await createTenantUserAccountApi({
+          tenantId: props.tenantId,
+          userIdList: createAccountList.map((item) => item.userId),
+        });
+        successMessage(t('system.views.tenant.manager.message.createAccountSuccess'));
+        query();
+      },
+    });
+  };
+
+  const [register, { query, getCheckboxRecords }] = useSmartTable({
     id: 'system-tenant-manager-userList',
-    columns: getTabUserListColumns(),
+    columns: getTabUserListColumns(t),
     border: true,
     height: 'auto',
     customConfig: { storage: true },
@@ -115,6 +153,16 @@
             };
           }),
         },
+        {
+          name: t('system.views.tenant.manager.button.user.createAccount'),
+          customRender: 'ant',
+          visible: unref(getIsPlatformTenant),
+          props: {
+            type: 'primary',
+            preIcon: 'ant-design:user-add-outlined',
+            onClick: handleCreateAccount,
+          },
+        },
       ],
     },
   });

+ 10 - 0
src/modules/smart-system/views/tenant/tenantManager/lang/zh_CN.ts

@@ -33,6 +33,7 @@ export default {
         mobile: '电话',
         bindTime: '绑定时间',
         bindBy: '绑定人',
+        hasAccount: '是否创建账户',
       },
       subscribe: {
         tenantId: '租户',
@@ -74,6 +75,15 @@ export default {
       selectOneRow: '请选择一条数据',
       bindUserSuccess: '绑定用户成功',
       selectUser: '请先选择用户',
+      hasCreateAccount: '选择用户存在已创建账户,是否跳过?',
+      createAccountConfirm: '确定要创建账户吗?',
+      noCreateAccountUser: '所选用户已全部创建账户',
+      createAccountSuccess: '创建账户成功',
+    },
+    button: {
+      user: {
+        createAccount: '创建账户',
+      },
     },
   },
 };

+ 23 - 3
src/store/modules/user.ts

@@ -1,4 +1,4 @@
-import type { UserInfo } from '#/store';
+import type { UserInfo, UserTenant } from '#/store';
 import type { ErrorMessageMode } from '#/axios';
 import { defineStore } from 'pinia';
 import { store } from '@/store';
@@ -12,11 +12,12 @@ import {
   LoginParams,
   LoginResultModel,
 } from '@/api/sys/model/userModel';
-import { changePasswordApi, doLogout, loginApi } from '@/api/sys/user';
+import { changePasswordApi, doLogout, loginApi, changeTenantApi } from '@/api/sys/user';
 import { useI18n } from '@/hooks/web/useI18n';
 import { useMessage } from '@/hooks/web/useMessage';
 import { router } from '@/router';
 import { usePermissionStore } from '@/store/modules/permission';
+import { usePermission } from '@/hooks/web/usePermission';
 import { RouteRecordRaw } from 'vue-router';
 import { PAGE_NOT_FOUND_ROUTE } from '@/router/routes/basic';
 import { h } from 'vue';
@@ -59,6 +60,15 @@ export const useUserStore = defineStore({
     getLastUpdateTime(state): number {
       return state.lastUpdateTime;
     },
+    getUserTenant(): UserTenant | undefined {
+      return this.getUserInfo.userTenant;
+    },
+    /**
+     * 是否是平台租户
+     */
+    getIsPlatformTenant(): boolean {
+      return this.getUserTenant?.platformYn || false;
+    },
   },
   actions: {
     setToken(info: string | undefined) {
@@ -83,6 +93,17 @@ export const useUserStore = defineStore({
       this.roleList = [];
       this.sessionTimeout = false;
     },
+    /**
+     * 切换租户
+     */
+    async changeTenant(tenantId: number) {
+      const data = await changeTenantApi(tenantId);
+      // 刷新菜单
+      await this.afterLoginAction(data, false);
+      const { refreshMenu } = usePermission();
+      await refreshMenu();
+      router.replace(data.user?.homePath || PageEnum.BASE_HOME);
+    },
     /**
      * @description: login
      */
@@ -114,7 +135,6 @@ export const useUserStore = defineStore({
       this.setUserInfo({
         ...userInfo,
       });
-
       const sessionTimeout = this.sessionTimeout;
       if (sessionTimeout) {
         this.setSessionTimeout(false);

+ 13 - 0
types/store.d.ts

@@ -41,6 +41,19 @@ export interface UserInfo {
   avatar: string;
   desc?: string;
   homePath?: string;
+  // 租户信息
+  userTenant?: UserTenant;
+}
+
+/**
+ * 用户租户信息
+ */
+export interface UserTenant {
+  tenantId: number;
+  tenantCode: string;
+  tenantName: string;
+  tenantShortName?: string;
+  platformYn: boolean;
 }
 
 export interface BeforeMiniState {