| | |
| | | |
| | | import { computed, onMounted, ref } from 'vue'; |
| | | |
| | | import { AuthenticationLogin, Verification, z } from '@vben/common-ui'; |
| | | 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'; |
| | | |
| | |
| | | getCaptcha, |
| | | getTenantByWebsite, |
| | | getTenantSimpleList, |
| | | socialAuthRedirect, |
| | | } from '#/api/core/auth'; |
| | | import { useAuthStore } from '#/store'; |
| | | |
| | |
| | | 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: '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: { |
| | | autocomplete: 'username', |
| | | placeholder: $t('authentication.usernameTip'), |
| | | }, |
| | | hideLabel: false, |
| | | labelWidth: 44, |
| | | fieldName: 'username', |
| | | label: $t('authentication.username'), |
| | | rules: z |
| | |
| | | { |
| | | component: 'VbenInputPassword', |
| | | componentProps: { |
| | | autocomplete: 'current-password', |
| | | placeholder: $t('authentication.passwordTip'), |
| | | }, |
| | | hideLabel: false, |
| | | labelWidth: 44, |
| | | fieldName: 'password', |
| | | label: $t('authentication.password'), |
| | | rules: z |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <div class="academic-login-wrapper"> |
| | | <AuthenticationLogin |
| | | ref="loginRef" |
| | | :form-schema="formSchema" |
| | |
| | | :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" |
| | |
| | | /> |
| | | </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> |