<script lang="ts" setup>
|
import type { VbenFormSchema } from '@vben/common-ui';
|
|
import type { AuthApi } from '#/api/core/auth';
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { AuthenticationLogin, VbenButton, Verification, z } from '@vben/common-ui';
|
import { isCaptchaEnable, isTenantEnable } from '@vben/hooks';
|
import { SvgWeChatIcon } from '@vben/icons';
|
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 authStore = useAuthStore();
|
const accessStore = useAccessStore();
|
const tenantEnable = isTenantEnable();
|
const captchaEnable = isCaptchaEnable();
|
|
const loginRef = ref();
|
const verifyRef = ref();
|
|
const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWord'
|
|
/** 获取租户列表 */
|
const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表
|
async function fetchTenantList() {
|
if (!tenantEnable) {
|
return;
|
}
|
try {
|
// 获取租户列表
|
tenantList.value = await getTenantSimpleList();
|
} catch (error) {
|
console.error('获取租户列表失败:', error);
|
}
|
}
|
|
/** 处理登录 */
|
async function handleLogin(values: any) {
|
// 如果开启验证码,则先验证验证码
|
if (captchaEnable) {
|
verifyRef.value.show();
|
return;
|
}
|
// 无验证码,直接登录
|
await authStore.authLogin('username', values);
|
}
|
|
/** 验证码通过,执行登录 */
|
async function handleVerifySuccess({ captchaVerification }: any) {
|
try {
|
await authStore.authLogin('username', {
|
...(await loginRef.value.getFormApi().getValues()),
|
captchaVerification,
|
});
|
} catch (error) {
|
console.error('Error in handleLogin:', error);
|
}
|
}
|
|
/** 组件挂载时获取租户信息 */
|
onMounted(() => {
|
fetchTenantList();
|
});
|
|
// ========== 企业微信登录 ==========
|
|
/** 企业微信的社交类型,对应后端 SocialTypeEnum.WECHAT_ENTERPRISE */
|
const WXCP_SOCIAL_TYPE = 30;
|
|
const wxcpLoading = ref(false);
|
|
/**
|
* 企业微信扫码登录
|
*
|
* 流程:取授权地址 -> 整页跳转到企业微信扫码页 -> 扫码后企业微信带 code/state
|
* 回跳到 social-login 页面 -> 该页面调用 /system/auth/social-login 换取令牌
|
*/
|
async function handleWxcpLogin() {
|
try {
|
wxcpLoading.value = true;
|
// 注意:type 需要先 encode 一次,配合 social-login 页面的 getUrlValue() 使用;
|
// 否则企业微信回跳后 type 参数会被吞掉,导致无法识别社交类型
|
// 回调地址必须带 hash 前缀:生产用 hash 路由(VITE_ROUTER_HISTORY=hash),
|
// 少了 # 企业微信回跳后 vue-router 会解析到 /,落到登录页而不是 social-login 页面
|
const hashPrefix =
|
import.meta.env.VITE_ROUTER_HISTORY === 'hash' ? '/#' : '';
|
const redirectUri = `${location.origin}${hashPrefix}/auth/social-login?${encodeURIComponent(
|
`type=${WXCP_SOCIAL_TYPE}`,
|
)}`;
|
window.location.href = await socialAuthRedirect(
|
WXCP_SOCIAL_TYPE,
|
redirectUri,
|
);
|
} catch (error) {
|
console.error('企业微信登录失败:', error);
|
wxcpLoading.value = false;
|
}
|
}
|
|
const formSchema = computed((): VbenFormSchema[] => {
|
return [
|
{
|
component: 'VbenInput',
|
componentProps: {
|
autocomplete: 'username',
|
placeholder: $t('authentication.usernameTip'),
|
},
|
hideLabel: false,
|
labelWidth: 44,
|
fieldName: 'username',
|
label: $t('authentication.username'),
|
rules: z
|
.string()
|
.min(1, { message: $t('authentication.usernameTip') })
|
.default(import.meta.env.VITE_APP_DEFAULT_USERNAME),
|
},
|
{
|
component: 'VbenInputPassword',
|
componentProps: {
|
autocomplete: 'current-password',
|
placeholder: $t('authentication.passwordTip'),
|
},
|
hideLabel: false,
|
labelWidth: 44,
|
fieldName: 'password',
|
label: $t('authentication.password'),
|
rules: z
|
.string()
|
.min(1, { message: $t('authentication.passwordTip') })
|
.default(import.meta.env.VITE_APP_DEFAULT_PASSWORD),
|
},
|
];
|
});
|
</script>
|
|
<template>
|
<div class="academic-login-wrapper">
|
<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"
|
class="academic-login-form"
|
@submit="handleLogin"
|
>
|
<!-- 自定义标题:覆盖欢迎语 -->
|
<template #title>
|
<div class="academic-title-area">
|
<h2 class="academic-title">账号登录</h2>
|
<p class="academic-subtitle">欢迎回来,请使用您的账号登录</p>
|
</div>
|
</template>
|
|
<!-- 第三方登录 -->
|
<template #third-party-login>
|
<div class="academic-third-party">
|
<div class="academic-divider">
|
<span class="academic-divider-line"></span>
|
<span class="academic-divider-text">其他登录方式</span>
|
<span class="academic-divider-line"></span>
|
</div>
|
<VbenButton
|
class="academic-wxcp-btn"
|
variant="outline"
|
:loading="wxcpLoading"
|
@click="handleWxcpLogin"
|
>
|
<SvgWeChatIcon class="mr-2 size-4" />
|
企业微信登录
|
</VbenButton>
|
</div>
|
</template>
|
</AuthenticationLogin>
|
|
<!-- 验证码(滑块拼图),captchaEnable 时启用 -->
|
<Verification
|
ref="verifyRef"
|
v-if="captchaEnable"
|
:captcha-type="captchaType"
|
:check-captcha-api="checkCaptcha"
|
:get-captcha-api="getCaptcha"
|
:img-size="{ width: '400px', height: '200px' }"
|
mode="pop"
|
@on-success="handleVerifySuccess"
|
/>
|
</div>
|
</template>
|
|
<style scoped>
|
.academic-login-wrapper { width: 100%; }
|
.academic-title-area { margin-bottom: 28px; }
|
.academic-title { margin: 0 0 10px; color: #18365a; font-size: 26px; font-weight: 750; line-height: 1.4; }
|
.academic-subtitle { margin: 0; color: #5c7187; font-size: 13px; line-height: 1.7; }
|
:deep(.academic-login-form label) { color: #294d70; font-size: 13px; font-weight: 600; }
|
:deep(.academic-login-form input:not([type=checkbox])) { min-height: 46px; border-radius: 6px; border-color: #d6e1ec; background: #f8fbfe; color: #18365a; font-size: 14px; }
|
:deep(.academic-login-form input::placeholder) { color: #5c7187; }
|
:deep(.academic-login-form input:focus-visible) { border-color: #3476b5; outline-color: #3476b5; background: #fff; }
|
:deep(.academic-login-form button[aria-label=login]) { height: 46px; border: 0; border-radius: 6px; color: #fff; background: #b7242d; font-size: 15px; font-weight: 650; letter-spacing: .16em; box-shadow: 0 4px 12px rgb(150 30 40 / 12%); }
|
:deep(.academic-login-form button[aria-label=login]:hover) { background: #9f1d27; }
|
:deep(.academic-login-form button[aria-label=login]:disabled) { opacity: .65; cursor: wait; }
|
:deep(.academic-login-form .vben-link) { color: #28659d; }
|
.academic-third-party { margin-top: 24px; }
|
.academic-divider { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
|
.academic-divider-line { height: 1px; flex: 1; background: #e4ecf3; }
|
.academic-divider-text { color: #5c7187; font-size: 12px; white-space: nowrap; }
|
.academic-wxcp-btn { width: 100%; height: 44px; border: 1px solid #d8e4ee; border-radius: 6px; color: #315777; background: #fff; font-size: 13px; }
|
.academic-wxcp-btn:hover { border-color: #8cb5d5; background: #f3f8fc; }
|
:deep(button:focus-visible) { outline: 2px solid #2c70b2; outline-offset: 3px; }
|
</style>
|