chore: 完善切换租户组件

This commit is contained in:
AN 2025-04-26 13:07:34 +08:00
parent 15acc1ff68
commit 28c88c485f
6 changed files with 138 additions and 34 deletions

View File

@ -1,17 +1,60 @@
<script setup lang="ts">
import { ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
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 { useTabStore } from '@/store/modules/tab';
import { useAuth } from '@/hooks/business/auth';
const { hasRole } = useAuth();
const { clearTabs } = useTabStore();
const router = useRouter();
defineOptions({ name: 'TenantSelect' });
const value = defineModel<CommonType.IdType>('value', { required: false, default: '000000' });
const enabled = defineModel<boolean>('enabled', { required: false, default: false });
interface Props {
clearable?: boolean;
}
withDefaults(defineProps<Props>(), {
clearable: false
});
const tenantId = defineModel<CommonType.IdType>('tenantId', { required: false, default: undefined });
const enabled = defineModel<boolean>('enabled', { required: false, default: true });
const lastSelected = ref<CommonType.IdType>();
const tenantOption = ref<SelectOption[]>([]);
const { loading, startLoading, endLoading } = useLoading();
const showTenantSelect = computed<boolean>(() => {
return hasRole('superadmin') && enabled.value;
});
async function handleChangeTenant(_tenantId: CommonType.IdType) {
if (!_tenantId) {
return;
}
if (lastSelected.value === _tenantId) {
return;
}
await fetchChangeTenant(_tenantId);
lastSelected.value = _tenantId;
window.$message?.success('切换租户成功');
clearTabs();
router.push('/');
}
async function handleClearTenant() {
await fetchClearTenant();
lastSelected.value = '';
window.$message?.success('切换为默认租户');
clearTabs();
router.push('/');
}
async function handleFetchTenantList() {
startLoading();
const { data, error } = await fetchTenantList();
@ -25,16 +68,23 @@ async function handleFetchTenantList() {
});
endLoading();
}
handleFetchTenantList();
onMounted(async () => {
if (!hasRole('superadmin')) {
return;
}
await handleFetchTenantList();
});
</script>
<template>
<NSelect
v-if="enabled"
v-model:value="value"
placeholder="请选择/输入公司名称"
v-if="showTenantSelect"
v-model:value="tenantId"
:clearable="clearable"
placeholder="请选择租户"
:options="tenantOption"
:loading="loading"
@update:value="handleChangeTenant"
@clear="handleClearTenant"
/>
</template>

View File

@ -9,7 +9,7 @@ export function useFormRules() {
userName: {
pattern: REG_USER_NAME,
message: $t('form.userName.invalid'),
trigger: 'change'
trigger: ['change', 'blur']
},
phone: {
pattern: REG_PHONE,
@ -19,7 +19,7 @@ export function useFormRules() {
pwd: {
pattern: REG_PWD,
message: $t('form.pwd.invalid'),
trigger: 'change'
trigger: ['change', 'blur']
},
code: {
pattern: REG_CODE_SIX,

View File

@ -48,7 +48,7 @@ watch(tenantId, async () => {
<GlobalBreadcrumb v-if="!appStore.isMobile" class="ml-12px" />
</div>
<div class="h-full flex-y-center justify-end">
<TenantSelect v-if="authStore.userInfo?.user?.userId === 1" class="mr-12px w-150px" />
<TenantSelect class="mr-12px w-150px" :clearable="true" />
<GlobalSearch />
<FullScreen v-if="!appStore.isMobile" :full="isFullscreen" @click="toggle" />
<LangSwitch

View File

@ -54,3 +54,11 @@ export function fetchChangeTenant(tenantId: CommonType.IdType) {
method: 'get'
});
}
/** 清空租户 */
export function fetchClearTenant() {
return request<boolean>({
url: '/system/tenant/dynamic/clear',
method: 'get'
});
}

View File

@ -3,14 +3,12 @@ import { computed, reactive, ref } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchCaptchaCode, fetchTenantList } from '@/service/api';
// import { fetchGetConfigDetail } from '@/service/api/system/config';
import { fetchSocialAuthBinding } from '@/service/api/system';
import { useAuthStore } from '@/store/modules/auth';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { localStg } from '@/utils/storage';
import { $t } from '@/locales';
defineOptions({
name: 'PwdLogin'
});
@ -19,12 +17,15 @@ 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 codeUrl = ref<string>();
const captchaEnabled = ref<boolean>(false);
const tenantEnabled = ref<boolean>(false);
const registerEnabled = ref<boolean>(false);
const remberMe = ref<boolean>(false);
const tenantEnabled = ref<boolean>(false);
const tenantOption = ref<SelectOption[]>([]);
const model: Api.Auth.PwdLoginForm = reactive({
@ -32,7 +33,6 @@ const model: Api.Auth.PwdLoginForm = reactive({
username: '',
password: ''
});
type RuleKey = Extract<keyof Api.Auth.PwdLoginForm, 'username' | 'password' | 'code' | 'tenantId'>;
const rules = computed<Record<RuleKey, App.Global.FormRule[]>>(() => {
@ -41,13 +41,28 @@ const rules = computed<Record<RuleKey, App.Global.FormRule[]>>(() => {
const loginRules: Record<RuleKey, App.Global.FormRule[]> = {
username: [...formRules.userName, { required: true }],
password: [...formRules.pwd, { required: true }],
password: [createRequiredRule($t('form.pwd.required'))],
code: captchaEnabled.value ? [createRequiredRule($t('form.code.required'))] : [],
tenantId: tenantEnabled.value ? formRules.tenantId : []
};
return loginRules;
});
async function handleFetchTenantList() {
startTenantLoading();
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
};
});
endTenantLoading();
}
handleFetchTenantList();
async function handleSubmit() {
await validate();
@ -66,20 +81,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();
@ -121,8 +122,14 @@ async function handleSocialLogin(type: Api.System.SocialSource) {
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem v-if="tenantEnabled" path="tenantId">
<TenantSelect v-model:value="model.tenantId" :enabled="tenantEnabled" />
<NFormItem path="tenantId">
<NSelect
v-if="tenantEnabled"
v-model:value="model.tenantId"
placeholder="请选择租户"
:options="tenantOption"
:loading="tenantLoading"
/>
</NFormItem>
<NFormItem path="username">
<NInput v-model:value="model.username" :placeholder="$t('page.login.common.userNamePlaceholder')" />

View File

@ -1,7 +1,46 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { computed } from 'vue';
import { useAppStore } from '@/store/modules/app';
import HeaderBanner from './modules/header-banner.vue';
import CardData from './modules/card-data.vue';
import LineChart from './modules/line-chart.vue';
import PieChart from './modules/pie-chart.vue';
import ProjectNews from './modules/project-news.vue';
import CreativityBanner from './modules/creativity-banner.vue';
const appStore = useAppStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
</script>
<template>
<FileUpload />
<NSpace vertical :size="16">
<NAlert :title="$t('common.warning')" type="warning">
{{ $t('page.home.branchDesc') }}
</NAlert>
<HeaderBanner />
<CardData />
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:14">
<NCard :bordered="false" class="card-wrapper">
<LineChart />
</NCard>
</NGi>
<NGi span="24 s:24 m:10">
<NCard :bordered="false" class="card-wrapper">
<PieChart />
</NCard>
</NGi>
</NGrid>
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:14">
<ProjectNews />
</NGi>
<NGi span="24 s:24 m:10">
<CreativityBanner />
</NGi>
</NGrid>
</NSpace>
</template>
<style scoped></style>