| | |
| | | <script lang="ts" setup> |
| | | import type { VbenFormSchema } from '..\..\..\packages\effects\common-ui\src'; |
| | | import type { VbenFormSchema } from '@vben/common-ui'; |
| | | |
| | | import type { AuthApi } from '#/api/core/auth'; |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | import { useRoute } from 'vue-router'; |
| | | |
| | | import { AuthenticationLogin, Verification, z } from '..\..\..\packages\effects\common-ui\src'; |
| | | import { isCaptchaEnable, isTenantEnable } from '..\..\..\packages\effects\hooks\src'; |
| | | import { $t } from '..\..\..\packages\locales\src'; |
| | | import { useAccessStore } from '..\..\..\packages\stores\src'; |
| | | import { AuthenticationLogin, Verification, z } from '@vben/common-ui'; |
| | | import { isCaptchaEnable, isTenantEnable } from '@vben/hooks'; |
| | | import { $t } from '@vben/locales'; |
| | | import { useAccessStore } from '@vben/stores'; |
| | | |
| | | import { |
| | | checkCaptcha, |
| | | getCaptcha, |
| | | getTenantByWebsite, |
| | | getTenantSimpleList, |
| | | socialAuthRedirect, |
| | | } from '#/api/core/auth'; |
| | | import { useAuthStore } from '#/store'; |
| | | |
| | | defineOptions({ name: 'Login' }); |
| | | |
| | | const { query } = useRoute(); |
| | | const authStore = useAuthStore(); |
| | | const accessStore = useAccessStore(); |
| | | const tenantEnable = isTenantEnable(); |
| | |
| | | |
| | | const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord' |
| | | |
| | | /** 获取租户列表,并默认选中 */ |
| | | /** 获取租户列表 */ |
| | | const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表 |
| | | async function fetchTenantList() { |
| | | if (!tenantEnable) { |
| | | return; |
| | | } |
| | | try { |
| | | // 获取租户列表、域名对应租户 |
| | | const websiteTenantPromise = getTenantByWebsite(window.location.hostname); |
| | | // 获取租户列表 |
| | | tenantList.value = await getTenantSimpleList(); |
| | | |
| | | // 选中租户:域名 > store 中的租户 > 首个租户 |
| | | let tenantId: null | number = null; |
| | | const websiteTenant = await websiteTenantPromise; |
| | | if (websiteTenant?.id) { |
| | | tenantId = websiteTenant.id; |
| | | } |
| | | // 如果没有从域名获取到租户,尝试从 store 中获取 |
| | | if (!tenantId && accessStore.tenantId) { |
| | | tenantId = accessStore.tenantId; |
| | | } |
| | | // 如果还是没有租户,使用列表中的第一个 |
| | | if (!tenantId && tenantList.value?.[0]?.id) { |
| | | tenantId = tenantList.value[0].id; |
| | | } |
| | | |
| | | // 设置选中的租户编号 |
| | | accessStore.setTenantId(tenantId); |
| | | loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString()); |
| | | } catch (error) { |
| | | console.error('获取租户列表失败:', error); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | /** 处理第三方登录 */ |
| | | const redirect = query?.redirect; |
| | | async function handleThirdLogin(type: number) { |
| | | if (type <= 0) { |
| | | return; |
| | | } |
| | | try { |
| | | // 计算 redirectUri |
| | | // tricky: type、redirect 需要先 encode 一次,否则钉钉回调会丢失。配合 social-login.vue#getUrlValue() 使用 |
| | | const redirectUri = `${ |
| | | location.origin |
| | | }/auth/social-login?${encodeURIComponent( |
| | | `type=${type}&redirect=${redirect || '/'}`, |
| | | )}`; |
| | | |
| | | // 进行跳转 |
| | | window.location.href = await socialAuthRedirect(type, redirectUri); |
| | | } catch (error) { |
| | | console.error('第三方登录处理失败:', error); |
| | | } |
| | | } |
| | | |
| | | /** 组件挂载时获取租户信息 */ |
| | | onMounted(() => { |
| | | fetchTenantList(); |
| | |
| | | |
| | | const formSchema = computed((): VbenFormSchema[] => { |
| | | return [ |
| | | { |
| | | component: 'VbenSelect', |
| | | componentProps: { |
| | | options: tenantList.value.map((item) => ({ |
| | | label: item.name, |
| | | value: item.id.toString(), |
| | | })), |
| | | placeholder: $t('authentication.tenantTip'), |
| | | }, |
| | | fieldName: 'tenantId', |
| | | label: $t('authentication.tenant'), |
| | | rules: z.string().min(1, { message: $t('authentication.tenantTip') }), |
| | | dependencies: { |
| | | triggerFields: ['tenantId'], |
| | | if: tenantEnable, |
| | | trigger(values) { |
| | | if (values.tenantId) { |
| | | accessStore.setTenantId(Number(values.tenantId)); |
| | | } |
| | | }, |
| | | }, |
| | | }, |
| | | // { |
| | | // component: 'VbenSelect', |
| | | // componentProps: { |
| | | // options: tenantList.value.map((item) => ({ |
| | | // label: item.name, |
| | | // value: item.id.toString(), |
| | | // })), |
| | | // placeholder: $t('authentication.tenantTip'), |
| | | // }, |
| | | // fieldName: 'tenantId', |
| | | // label: $t('authentication.tenant'), |
| | | // dependencies: { |
| | | // triggerFields: ['tenantId'], |
| | | // if: tenantEnable, |
| | | // trigger(values) { |
| | | // accessStore.setTenantId(values.tenantId ? Number(values.tenantId) : null); |
| | | // }, |
| | | // }, |
| | | // }, |
| | | { |
| | | component: 'VbenInput', |
| | | componentProps: { |
| | | placeholder: $t('authentication.usernameTip'), |
| | | autocomplete: 'username', |
| | | 'aria-label': $t('authentication.username'), |
| | | }, |
| | | fieldName: 'username', |
| | | label: $t('authentication.username'), |
| | |
| | | component: 'VbenInputPassword', |
| | | componentProps: { |
| | | placeholder: $t('authentication.passwordTip'), |
| | | autocomplete: 'current-password', |
| | | 'aria-label': $t('authentication.password'), |
| | | }, |
| | | fieldName: 'password', |
| | | label: $t('authentication.password'), |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <div class="herbal-login"> |
| | | <AuthenticationLogin |
| | | ref="loginRef" |
| | | :form-schema="formSchema" |
| | | :loading="authStore.loginLoading" |
| | | :show-code-login="false" |
| | | :show-qrcode-login="false" |
| | | :show-third-party-login="false" |
| | | :show-register="false" |
| | | :show-forget-password="false" |
| | | :show-doc-link="false" |
| | | @submit="handleLogin" |
| | | @third-login="handleThirdLogin" |
| | | /> |
| | | > |
| | | <template #title> |
| | | <div class="herbal-login__heading"> |
| | | <h1>{{ $t('authentication.welcomeBack') }}<svg class="herbal-login__seal" viewBox="0 0 20 30" aria-hidden="true"><rect x="1" y="1" width="18" height="28" rx="2"/><path d="M5 5h10v8H5V5Zm0 12h10v8H5v-8Zm5-12v8m0 4v8"/></svg></h1> |
| | | <p>{{ $t('authentication.loginSubtitle') }}</p> |
| | | </div> |
| | | </template> |
| | | </AuthenticationLogin> |
| | | <Verification |
| | | ref="verifyRef" |
| | | v-if="captchaEnable" |
| | |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .herbal-login__heading{margin-bottom:32px} |
| | | .herbal-login__heading h1{font-size:clamp(28px,2.4vw,36px);font-weight:700;line-height:1.4;letter-spacing:-.02em;margin:0 0 12px;color:#29251f;display:flex;align-items:center;gap:14px} |
| | | .herbal-login__heading p{font-size:15px;line-height:1.7;color:#756d63;margin:0} |
| | | .herbal-login__seal{width:20px;height:30px;flex-shrink:0;color:#a51d24;fill:none;stroke:currentColor;stroke-width:1.2} |
| | | .herbal-login :deep(input:not([type='checkbox'])){min-height:56px;border-radius:9px;border:1px solid #ddd6cc;background:rgba(255,255,255,.82);padding:12px 16px;font-size:16px;color:#30281f} |
| | | .herbal-login :deep(input::placeholder){color:#7a7167;opacity:1} |
| | | .herbal-login :deep(input:focus-visible){outline:2px solid #a51d24;outline-offset:3px} |
| | | .herbal-login :deep(input[autocomplete='current-password']){padding-right:44px} |
| | | .herbal-login :deep(button[aria-label='login']){min-height:58px;margin-top:8px;border-radius:9px;background:#a51d24;color:#fff;font-size:19px;font-weight:600;letter-spacing:.12em;box-shadow:0 5px 12px rgba(137,24,30,.12)} |
| | | .herbal-login :deep(button[aria-label='login']:hover){background:#87171d} |
| | | .herbal-login :deep(button[aria-label='login']:focus-visible){outline:2px solid #a51d24;outline-offset:4px} |
| | | .herbal-login :deep(button[aria-label='login']:disabled){opacity:.65;cursor:wait} |
| | | :global(.dark .herbal-login__heading h1){color:#f5eee4} |
| | | :global(.dark .herbal-login__heading p){color:#d1c1ae} |
| | | :global(.dark .herbal-login__seal){color:#eea5a4} |
| | | :global(.dark .herbal-login input:not([type='checkbox'])){background:#302720;border-color:#6f5b48;color:#f5eee4} |
| | | :global(.dark .herbal-login input::placeholder){color:#c2b29e} |
| | | @media(max-width:540px){.herbal-login__heading h1{font-size:28px}.herbal-login__heading p{font-size:14px}.herbal-login :deep(input:not([type='checkbox'])){min-height:52px}} |
| | | </style> |