You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

359 lines
11 KiB

1 year ago
<template>
<LoginFormTitle v-show="getShow" class="enter-x" />
<Form class="p-4 enter-x" :model="formData" :rules="getFormRules" ref="formRef" v-show="getShow">
<FormItem name="tenantId" class="enter-x" v-if="tenant.tenantEnabled">
<Select v-model:value="formData.tenantId" size="large" @change="handleTenantChange">
<template #suffixIcon>
<Icon icon="mdi:company" />
</template>
<SelectOption v-for="item in tenant.voList" :key="item.tenantId" :value="item.tenantId">{{
item.companyName
}}</SelectOption>
</Select>
</FormItem>
<FormItem name="account" class="enter-x">
<Input size="large" v-model:value="formData.account" :placeholder="t('sys.login.userName')" />
</FormItem>
<FormItem name="password" class="enter-x">
<InputPassword
size="large"
visibilityToggle
v-model:value="formData.password"
:placeholder="t('sys.login.password')"
@keypress.enter="handleLogin"
/>
</FormItem>
<FormItem name="code" class="enter-x" v-if="image.requiredCaptcha">
<Input
ref="imageCodeRef"
size="large"
v-model:value="formData.code"
placeholder="输入验证码"
@keypress.enter="handleLogin"
>
<template #addonAfter>
<Image
class="rounded-r-lg"
:preview="false"
:height="40"
:width="105"
:src="image.imageInfo"
@click="refreshCaptchaImage"
/>
</template>
</Input>
</FormItem>
<ARow class="enter-x">
<ACol :span="12">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="rememberMe" size="small">
{{ t('sys.login.rememberMe') }}
</Checkbox>
</FormItem>
</ACol>
<ACol :span="12">
<FormItem :style="{ 'text-align': 'right' }">
<!-- No logic, you need to deal with it yourself -->
<Button
:disabled="true"
type="link"
size="small"
@click="setLoginState(LoginStateEnum.RESET_PASSWORD)"
>
{{ t('sys.login.forgetPassword') }}
</Button>
</FormItem>
</ACol>
</ARow>
<FormItem class="enter-x">
<a-button
type="primary"
size="large"
block
:disabled="!submitBtnEnable"
@click="handleLogin"
:loading="loading"
>
{{ t('sys.login.loginButton') }}
</a-button>
<!-- <Button size="large" class="mt-4 enter-x" block @click="handleRegister">
{{ t('sys.login.registerButton') }}
</Button> -->
</FormItem>
<ARow class="enter-x" :gutter="[16, 16]">
<ACol :md="8" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.MOBILE)" disabled>
1 year ago
{{ t('sys.login.mobileSignInFormTitle') }}
</Button>
</ACol>
<ACol :md="8" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.QR_CODE)" disabled>
{{ t('sys.login.qrSignInFormTitle') }}
</Button>
</ACol>
<ACol :md="8" :xs="24">
<Button block @click="setLoginState(LoginStateEnum.REGISTER)" disabled>
{{ t('sys.login.registerButton') }}
</Button>
</ACol>
</ARow>
<!-- 其他登录方式 -->
<!-- <Divider class="enter-x">{{ t('sys.login.otherSignIn') }}</Divider> -->
1 year ago
<!-- <div class="flex justify-evenly enter-x" :class="`${prefixCls}-sign-in-way`">
<GithubFilled />
<WechatFilled />
<AlipayCircleFilled />
<GoogleCircleFilled />
<TwitterCircleFilled />
</div> -->
<!-- <OAuthLogin :disabled="loading" /> -->
1 year ago
</Form>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, computed, onMounted } from 'vue';
import {
Checkbox,
Form,
Input,
Row,
Col,
Button,
Image,
Select,
SelectOption,
Divider,
} from 'ant-design-vue';
// import {
// GithubFilled,
// WechatFilled,
// AlipayCircleFilled,
// GoogleCircleFilled,
// TwitterCircleFilled,
// } from '@ant-design/icons-vue';
import LoginFormTitle from './LoginFormTitle.vue';
import Icon from '@/components/Icon/Icon.vue';
import { useI18n } from '@/hooks/web/useI18n';
import { useMessage } from '@/hooks/web/useMessage';
import { useUserStore } from '@/store/modules/user';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
import { useDesign } from '@/hooks/web/useDesign';
import { tenantList } from '@/api/auth';
import { captchaImage } from '@/api/auth/captcha';
import { TenantResp } from '@/api/auth/model';
import { useLocalStorage } from '@vueuse/core';
import OAuthLogin from './OAuthLogin.vue';
import { createLocalStorage } from '@/utils/cache';
import { SelectValue } from 'ant-design-vue/es/select';
const ACol = Col;
const ARow = Row;
const FormItem = Form.Item;
const InputPassword = Input.Password;
const { t } = useI18n();
const { notification, createErrorModal } = useMessage();
const { prefixCls } = useDesign('login');
const userStore = useUserStore();
const { setLoginState, getLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref();
const loading = ref(false);
// 默认租户ID
const defaultTenantId = '000000';
const formData: any = reactive({
account: 'ai',
password: 'ai123456',
1 year ago
// account: '',
// password: '',
code: '',
uuid: '',
tenantId: defaultTenantId, // 默认租户
});
// 验证码信息
const image = reactive({
requiredCaptcha: false,
imageInfo: '',
loadSuccess: false,
});
// 租户信息
const tenant = reactive<TenantResp & { loadSuccess: boolean }>({
tenantEnabled: false,
voList: [],
loadSuccess: false,
});
async function refreshCaptchaImage() {
try {
const ret = await captchaImage();
// 验证码UUID
formData.uuid = ret.uuid;
image.requiredCaptcha = ret.captchaEnabled;
image.imageInfo = `data:image/gif;base64,${ret.img}`;
// 清空输入框
formData.code = '';
// 获取焦点
unref(imageCodeRef)?.focus();
// 设置为image加载成功
image.loadSuccess = true;
} catch (e) {
console.error('验证码异常');
image.loadSuccess = false;
}
}
// 这里主要是第三方登录要使用tenantId
const localTenantId = useLocalStorage('tenantId', defaultTenantId);
function handleTenantChange(tenantId: SelectValue) {
localTenantId.value = tenantId as string;
}
async function tenantSelectInit() {
try {
const ret = await tenantList();
// 启用租户的话默认选择第一项
const { tenantEnabled, voList } = ret;
if (tenantEnabled) {
const currentTenantId = voList.length ? voList[0].tenantId : defaultTenantId;
formData.tenantId = currentTenantId;
localTenantId.value = currentTenantId;
}
Object.assign(tenant, ret);
// 设置为租户加载成功
tenant.loadSuccess = true;
} catch (e) {
console.error('租户选择异常');
tenant.loadSuccess = false;
}
}
// tenant和验证码都加载ok 登录按钮才可用
const submitBtnEnable = computed(() => tenant.loadSuccess && image.loadSuccess);
// prod环境默认加密 RUOYI_PLUS__PRODUCTION__版本__REMEMBERME
const storage = createLocalStorage();
const cacheKey = 'rememberMe';
const rememberMe = ref<boolean>(!!storage.get(cacheKey));
function handleRememberMe(data: Recordable) {
const { tenantId = defaultTenantId, username, password } = data;
const rememberMeData = { tenantId, username, password };
if (rememberMe.value) {
storage.set(cacheKey, rememberMeData);
} else {
storage.remove(cacheKey);
}
}
function setRememberMeData() {
const data = storage.get(cacheKey);
if (data) {
console.log(data);
const { tenantId = defaultTenantId, username = '', password = '' } = data;
formData.tenantId = tenantId;
formData.account = username;
formData.password = password;
}
}
onMounted(async () => {
// 验证码
await refreshCaptchaImage();
// 租户下拉框
await tenantSelectInit();
// 设置记住我数据
setRememberMeData();
});
const { validForm } = useFormValid(formRef);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN);
const imageCodeRef = ref<HTMLInputElement>();
async function handleLogin() {
// 由于keypress.enter 这里需要再次判断
if (!submitBtnEnable.value) return;
// 验证表单
const data = await validForm();
if (!data) return;
try {
loading.value = true;
const requestParam: any = {
username: data.account,
password: data.password,
grantType: 'password',
};
// 验证码启用需要添加参数
if (image.requiredCaptcha) {
requestParam.uuid = formData.uuid;
requestParam.code = data.code;
}
// 租户启用需要添加参数
if (tenant.tenantEnabled) {
requestParam.tenantId = formData.tenantId;
} else {
// 没有启用租户的话添加默认租户
requestParam.tenantId = defaultTenantId;
}
const userInfo = await userStore.login({
...requestParam,
mode: 'none', //不要默认的错误提示
});
if (userInfo) {
// 租户id添加到localStorage 绑定账号需要用到
localTenantId.value = requestParam.tenantId ?? defaultTenantId;
// 记住我
handleRememberMe(requestParam);
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.nickName}`,
duration: 3,
});
}
} catch (error) {
await refreshCaptchaImage();
const content = (error as unknown as Error).message || t('sys.api.networkExceptionMsg');
createErrorModal({
title: t('sys.api.errorTip'),
content,
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
onOk() {
// 后端验证码失效/错误并没有返回唯一code 只能这样判断
if (
image.requiredCaptcha &&
(content.includes('验证码') || content.includes('Captcha'))
) {
formData.code = '';
unref(imageCodeRef)?.focus();
}
},
});
} finally {
loading.value = false;
}
}
</script>
<style lang="less" scoped>
/** 表单左右宽度 */
:deep(.ant-input-group-addon) {
padding: 0;
border: none;
}
</style>