refactor(auth): 移除多租户前端逻辑并修复接口加密密钥

- 删除租户选择组件 tenant-select.vue 及全局头部的引用
- 删除 fetchTenantList 接口调用及登录/注册页的租户列表加载逻辑
- 更新 .env.dev/.env.prod/.env.test 接口加密 RSA 公钥与私钥,与后端 Nacos api-decrypt 配置配对,修复登录 BadBlockException
- 登录失败后自动刷新验证码(验证码已消费需重新获取)
- pnpm-workspace.yaml 允许 es5-ext 执行构建脚本
- components.d.ts 同步自动生成的图标组件声明

refactor(auth): remove frontend multi-tenant logic and fix API encryption keys

- Remove tenant-select component and its usage in global header
- Remove fetchTenantList API and tenant list loading in login/register pages
- Update RSA public/private keys in .env files to pair with backend Nacos api-decrypt config, fixing login BadBlockException
- Refresh captcha after a failed login attempt
- Allow es5-ext build scripts in pnpm-workspace.yaml
This commit is contained in:
2026-08-30 18:26:57 +08:00
parent a0ab2dc424
commit 29f20b7e4d
10 changed files with 23 additions and 158 deletions
+2 -2
View File
@@ -21,6 +21,6 @@ VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
VITE_APP_RSA_PUBLIC_KEY='MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY='MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
VITE_APP_RSA_PRIVATE_KEY='MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
+2 -2
View File
@@ -18,6 +18,6 @@ VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
VITE_APP_RSA_PUBLIC_KEY='MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY='MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
VITE_APP_RSA_PRIVATE_KEY='MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
+2 -2
View File
@@ -18,6 +18,6 @@ VITE_APP_ENCRYPT=Y
# AES 加密头标识
VITE_HEADER_FLAG=encrypt-key
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY='MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
VITE_APP_RSA_PUBLIC_KEY='MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY='MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='
VITE_APP_RSA_PRIVATE_KEY='MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
+1
View File
@@ -2,6 +2,7 @@ packages:
- 'packages/*'
allowBuilds:
'@parcel/watcher': false
es5-ext: true
esbuild: false
simple-git-hooks: false
vue-demi: false
-103
View File
@@ -1,103 +0,0 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchTenantList } from '@/service/api';
import { fetchChangeTenant, fetchClearTenant } from '@/service/api/system/tenant';
import { useAppStore } from '@/store/modules/app';
import { useTabStore } from '@/store/modules/tab';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
defineOptions({ name: 'TenantSelect' });
interface Props {
clearable?: boolean;
}
withDefaults(defineProps<Props>(), {
clearable: false
});
const appStore = useAppStore();
const { userInfo } = useAuthStore();
const { clearTabs } = useTabStore();
const { toHome } = useRouterPush();
const tenantId = defineModel<CommonType.IdType>('tenantId', { required: false, default: undefined });
const enabled = defineModel<boolean>('enabled', { required: false, default: false });
const lastSelected = ref<CommonType.IdType>();
const tenantOption = ref<SelectOption[]>([]);
const { loading, startLoading, endLoading } = useLoading();
const showTenantSelect = computed<boolean>(() => {
return userInfo.user?.userId === 1 && enabled.value;
});
/**
* 关闭当前页面并刷新
*
* @param msg 提示信息
* @param val 租户ID
*/
async function closeAndRefresh(msg: string, val: CommonType.IdType = '') {
lastSelected.value = val;
window.$message?.success(msg);
clearTabs([], true);
toHome();
appStore.reloadPage(500);
}
async function handleChangeTenant(_tenantId: CommonType.IdType) {
if (!_tenantId) {
return;
}
if (lastSelected.value === _tenantId) {
return;
}
await fetchChangeTenant(_tenantId);
closeAndRefresh('切换租户成功', _tenantId);
}
async function handleClearTenant() {
await fetchClearTenant();
closeAndRefresh('切换为默认租户');
}
async function handleFetchTenantList() {
startLoading();
const { data, error } = await fetchTenantList();
if (error) return;
enabled.value = data.tenantEnabled;
if (data.tenantEnabled) {
tenantOption.value = data.voList.map(tenant => {
return {
label: tenant.companyName,
value: tenant.tenantId
};
});
}
endLoading();
}
onMounted(async () => {
if (userInfo.user?.userId !== 1) {
return;
}
await handleFetchTenantList();
});
</script>
<template>
<NSelect
v-if="showTenantSelect"
v-model:value="tenantId"
:clearable="clearable"
placeholder="请选择租户"
:options="tenantOption"
:loading="loading"
@update:value="handleChangeTenant"
@clear="handleClearTenant"
/>
</template>
@@ -1,9 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useFullscreen } from '@vueuse/core';
import { GLOBAL_HEADER_MENU_ID } from '@/constants/app';
import { useAppStore } from '@/store/modules/app';
import { useAuthStore } from '@/store/modules/auth';
import { useThemeStore } from '@/store/modules/theme';
import GlobalLogo from '../global-logo/index.vue';
import GlobalBreadcrumb from '../global-breadcrumb/index.vue';
@@ -28,11 +26,8 @@ interface Props {
defineProps<Props>();
const appStore = useAppStore();
const authStore = useAuthStore();
const themeStore = useThemeStore();
const { isFullscreen, toggle } = useFullscreen();
const tenantId = ref<CommonType.IdType>(authStore.userInfo?.user?.tenantId || '000000');
</script>
<template>
@@ -44,7 +39,6 @@ const tenantId = ref<CommonType.IdType>(authStore.userInfo?.user?.tenantId || '0
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
</div>
<div class="h-full flex-y-center justify-end">
<TenantSelect v-if="!appStore.isMobile" v-model:value="tenantId" class="mr-12px w-150px" />
<GlobalSearch v-if="themeStore.header.globalSearch.visible && !appStore.isMobile" />
<MessageButton />
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
-8
View File
@@ -1,13 +1,5 @@
import { request } from '../request';
/** Get tenant list */
export function fetchTenantList() {
return request<Api.Auth.LoginTenant>({
url: '/auth/tenant/list',
method: 'get'
});
}
/** Get image code */
export function fetchCaptchaCode() {
return request<Api.Auth.CaptchaCode>({
+12
View File
@@ -51,11 +51,17 @@ declare module 'vue' {
IconMaterialSymbolsUploadRounded: typeof import('~icons/material-symbols/upload-rounded')['default']
IconMaterialSymbolsWarningOutlineRounded: typeof import('~icons/material-symbols/warning-outline-rounded')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowRightThick: typeof import('~icons/mdi/arrow-right-thick')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiCheckCircleOutline: typeof import('~icons/mdi/check-circle-outline')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiGithub: typeof import('~icons/mdi/github')['default']
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconMdiLoading: typeof import('~icons/mdi/loading')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconMdiShieldAlertOutline: typeof import('~icons/mdi/shield-alert-outline')['default']
IconMdiShieldCheckOutline: typeof import('~icons/mdi/shield-check-outline')['default']
IconOcticonPin16: typeof import('~icons/octicon/pin16')['default']
IconOcticonPinSlash16: typeof import('~icons/octicon/pin-slash16')['default']
IconQuillCollapse: typeof import('~icons/quill/collapse')['default']
@@ -202,11 +208,17 @@ declare global {
const IconMaterialSymbolsUploadRounded: typeof import('~icons/material-symbols/upload-rounded')['default']
const IconMaterialSymbolsWarningOutlineRounded: typeof import('~icons/material-symbols/warning-outline-rounded')['default']
const IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
const IconMdiArrowRightThick: typeof import('~icons/mdi/arrow-right-thick')['default']
const IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
const IconMdiCheckCircleOutline: typeof import('~icons/mdi/check-circle-outline')['default']
const IconMdiDrag: typeof import('~icons/mdi/drag')['default']
const IconMdiGithub: typeof import('~icons/mdi/github')['default']
const IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
const IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
const IconMdiLoading: typeof import('~icons/mdi/loading')['default']
const IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
const IconMdiShieldAlertOutline: typeof import('~icons/mdi/shield-alert-outline')['default']
const IconMdiShieldCheckOutline: typeof import('~icons/mdi/shield-check-outline')['default']
const IconOcticonPin16: typeof import('~icons/octicon/pin16')['default']
const IconOcticonPinSlash16: typeof import('~icons/octicon/pin-slash16')['default']
const IconQuillCollapse: typeof import('~icons/quill/collapse')['default']
+3 -20
View File
@@ -3,7 +3,7 @@ import { computed, reactive, ref } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import CryptoJS from 'crypto-js';
import { fetchCaptchaCode, fetchTenantList } from '@/service/api';
import { fetchCaptchaCode } from '@/service/api';
import { fetchSocialAuthBinding } from '@/service/api/system';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
@@ -22,7 +22,7 @@ const authStore = useAuthStore();
const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useNaiveForm();
const { loading: codeLoading, startLoading: startCodeLoading, endLoading: endCodeLoading } = useLoading();
const { loading: tenantLoading, startLoading: startTenantLoading, endLoading: endTenantLoading } = useLoading();
const { loading: tenantLoading } = useLoading();
const codeUrl = ref<string>();
const captchaEnabled = ref<boolean>(false);
@@ -55,24 +55,6 @@ const rules = computed<Record<RuleKey, App.Global.FormRule[]>>(() => {
return loginRules;
});
async function handleFetchTenantList() {
startTenantLoading();
const { data, error } = await fetchTenantList();
if (error) return;
tenantEnabled.value = data.tenantEnabled;
if (data.tenantEnabled) {
tenantOption.value = data.voList.map(tenant => {
return {
label: tenant.companyName,
value: tenant.tenantId
};
});
}
endTenantLoading();
}
handleFetchTenantList();
async function handleSubmit() {
await validate();
// 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码
@@ -86,6 +68,7 @@ async function handleSubmit() {
try {
await authStore.login(model);
} catch {
// 登录失败后验证码已消费,刷新并重置
handleFetchCaptchaCode();
}
}
+1 -15
View File
@@ -2,7 +2,7 @@
import { computed, reactive, ref } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchCaptchaCode, fetchRegister, fetchTenantList } from '@/service/api';
import { fetchCaptchaCode, fetchRegister } from '@/service/api';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
@@ -72,20 +72,6 @@ async function handleSubmit() {
}
}
async function handleFetchTenantList() {
const { data, error } = await fetchTenantList();
if (error) return;
tenantEnabled.value = data.tenantEnabled;
tenantOption.value = data.voList.map(tenant => {
return {
label: tenant.companyName,
value: tenant.tenantId
};
});
}
handleFetchTenantList();
async function handleFetchCaptchaCode() {
startCodeLoading();
const { data, error } = await fetchCaptchaCode();