Files
lingji-work-fe/src/views/login/components/login-form/index.vue
rd 1630481437 fix(input): 优化输入框交互和验证逻辑
- 修改密码输入框增加 @change 事件处理,用于清除确认密码的验证信息- 验证码输入框增加 @focus 事件处理,用于清除错误状态和信息
- 登录和注册页面的错误信息显示进行样式统一和优化
-调整注册表单中同意条款的样式
2025-09-16 17:14:52 +08:00

367 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- eslint-disable vue/no-duplicate-attributes -->
<template>
<div class="flex items-center w-400 h-100%">
<div class="w-full bg-#fff rounded-16px px-40px py-32px flex flex-col items-center">
<img src="@/assets/img/icon-logo.png" alt="" width="144" height="36" class="mb-24px" />
<Tabs v-model:activeKey="activeKey" class="mb-24px" @change="onTabChange">
<TabPane tab="密码登录" key="1" />
<TabPane tab="短信登录" key="2" />
</Tabs>
<Form ref="formRef" :model="loginForm" :rules="formRules" class="w-320 form-wrap">
<FormItem name="mobile">
<Input
v-model:value="loginForm.mobile"
placeholder="请输入手机号"
allowClear
:maxlength="11"
@change="clearErrorMsg"
/>
</FormItem>
<FormItem v-if="isCaptchaLogin" name="captcha" class="captcha-form-item">
<Input v-model:value="loginForm.captcha" placeholder="请输入验证码" :maxlength="6" @change="clearErrorMsg">
<template #suffix>
<div class="w-79px flex justify-center whitespace-nowrap">
<span
class="color-#939499 font-family-regular text-16px font-400 lh-24px cursor-not-allowed"
:class="{
'!color-#6D4CFE': isLegalMobile || countdown > 0,
'!cursor-pointer': canGetCaptcha,
}"
@click="getCode"
>{{ countdown > 0 ? `${countdown}s` : hasGetCode ? '重新发送' : '发送验证码' }}</span
>
</div>
</template>
</Input>
<p class="color-#F64B31 text-12px font-400 h-20px lh-20px font-family-regular">
{{ errMsg }}
</p>
</FormItem>
<FormItem v-else name="password" class="password-form-item">
<Input.Password v-model:value="loginForm.password" placeholder="请输入密码" @change="clearErrorMsg">
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
<p class="color-#F64B31 h-20px text-12px font-400 lh-20px font-family-regular">
{{ errMsg }}
</p>
</FormItem>
<FormItem class="mt-32px">
<div class="text-12px flex justify-center items-center mb-16px">
<Checkbox v-model:checked="hasAgree" class="mr-8px"></Checkbox>
<span class="text-12px color-#737478 font-400 lh-20px font-family-regular"
>登录即代表同意<span class="color-#6D4CFE"> 用户协议 </span><span class="color-#6D4CFE">
隐私政策</span
></span
>
</div>
<Button
type="primary"
class="w-full h-48 mb-8px !text-16px !font-500 !rounded-8px btn-login"
:class="disabledSubmitBtn ? 'cursor-no-drop' : 'cursor-pointer'"
:disabled="disabledSubmitBtn"
@click="handleSubmit"
>
登录
</Button>
<div class="flex justify-between btn-row">
<div>
<Button
v-show="!isCaptchaLogin"
class="!color-#939499 !p-0 !h-22px !hover:color-#6D4CFE !active:color-#573DCB"
size="small"
type="text"
@click="onForgetPassword"
>
忘记密码
</Button>
</div>
<Button
type="text"
class="!color-#939499 !p-0 !h-22px !hover:color-#6D4CFE !active:color-#573DCB"
size="small"
@click="onRegister"
>
注册
</Button>
</div>
</FormItem>
</Form>
</div>
</div>
<PuzzleVerification
:show="isVerificationVisible"
@submit="handleVerificationSubmit"
@cancel="isVerificationVisible = false"
/>
<SelectAccountModal ref="selectAccountModalRef" :mobileNumber="mobileNumber" :accounts="accounts" />
</template>
<script setup lang="ts">
import { Button, Checkbox, Form, FormItem, Input, message, Tabs, Typography } from 'ant-design-vue';
import PuzzleVerification from '../PuzzleVerification.vue';
import SelectAccountModal from '../select-account-modal/index.vue';
import { fetchAuthorizationsCaptcha, fetchLoginCaptCha, fetchProfileInfo, postLoginPassword } from '@/api/all/login';
import { postClearRateLimiter } from '@/api/all/common';
import { joinEnterpriseByInviteCode } from '@/api/all';
import { computed, onUnmounted, reactive, ref } from 'vue';
import { useUserStore } from '@/stores';
import { useEnterpriseStore } from '@/stores/modules/enterprise';
import { handleUserLogin } from '@/utils/user';
import { useRoute } from 'vue-router';
import icon1 from '@/assets/img/login/icon-close.png';
import icon2 from '@/assets/img/login/icon-open.png';
const { Link } = Typography;
const { TabPane } = Tabs;
const setPageType = inject('setPageType');
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const enterpriseStore = useEnterpriseStore();
const formRef = ref();
const countdown = ref(0);
let timer = ref();
const isLogin = ref(true);
const isVerificationVisible = ref(false);
const hasGetCode = ref(false);
const submitting = ref(false);
const hasAgree = ref(false);
const mobileNumber = ref('');
const selectAccountModalRef = ref(null);
const accounts = ref([]);
const activeKey = ref('1');
const isLegalMobile = ref(false);
const errMsg = ref('');
const loginForm = reactive({
mobile: '',
captcha: '',
password: '',
});
// 表单校验规则
const formRules = {
mobile: [
{
required: true,
validator: (_rule: any, value: string) => {
if (!value) {
isLegalMobile.value = false;
// return Promise.reject('手机号不能为空');
}
if (value && !/^1[3-9]\d{9}$/.test(value)) {
isLegalMobile.value = false;
return Promise.reject('手机号格式不正确');
} else {
isLegalMobile.value = true;
return Promise.resolve();
}
},
trigger: ['blur'],
},
],
password: [
{
// required: true,
// validator: (_rule: any, value: string) => {
// if (!value) {
// return Promise.reject('请输入密码');
// }
// if (value.length < 6) {
// return Promise.reject('密码长度不能小于6位');
// }
// return Promise.resolve();
// },
// trigger: ['blur'],
},
],
captcha: [
{
// required: true,
// validator: (_rule: any, value: string) => {
// if (!value) {
// return Promise.reject('请输入验证码');
// }
// if (!/^\d{6}$/.test(value)) {
// return Promise.reject('验证码必须是6位数字');
// } else {
// return Promise.resolve();
// }
// },
// trigger: ['blur'],
},
],
};
const isCaptchaLogin = computed(() => {
return activeKey.value === '2';
});
const canGetCaptcha = computed(() => {
return isLegalMobile.value && countdown.value === 0;
});
const clearErrorMsg = () => {
errMsg.value = '';
};
const disabledSubmitBtn = computed(() => {
if (isCaptchaLogin.value) {
return !hasAgree.value || !isLegalMobile.value || !loginForm.captcha.trim() || !/^\d{6}$/.test(loginForm.captcha);
}
// 密码登录时的验证逻辑
return !hasAgree.value || !isLegalMobile.value || !loginForm.password.trim();
});
const validateField = (field: string) => {
if (field === 'mobile') {
errMsg.value = '';
}
formRef.value.validateFields(field);
};
const clearError = (field: string) => {
formRef.value.clearValidate(field);
};
const getCode = async () => {
if (!canGetCaptcha.value) return;
formRef.value.validateFields('mobile').then(() => {
getCaptcha();
});
};
const getCaptcha = async () => {
try {
const { code, message: msg } = await fetchLoginCaptCha({ mobile: loginForm.mobile });
if (code === 200) {
startCountdown();
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
if (error.status === 429) {
isVerificationVisible.value = true;
}
}
};
// 验证码验证通过后
const handleVerificationSubmit = async () => {
isVerificationVisible.value = false;
await postClearRateLimiter();
getCaptcha();
};
const onTabChange = () => {
errMsg.value = '';
};
// 获取用户信息
const getProfileInfo = async () => {
const { code, data } = await fetchProfileInfo();
if (code === 200) {
const enterprises = data['enterprises'];
mobileNumber.value = data['mobile'];
accounts.value = enterprises;
if (enterprises.length > 0) {
enterpriseStore.setEnterpriseInfo(data.enterprises[0]);
if (enterprises.length === 1) {
handleUserLogin();
} else {
// 多个企业时候需要弹窗让用户选择企业
selectAccountModalRef.value.open();
}
} else {
router.push({ name: 'Trial' });
}
}
};
// 提交表单
const handleSubmit = async () => {
if (disabledSubmitBtn.value) return;
try {
// 校验所有字段
await formRef.value.validate();
if (!hasAgree.value) {
message.error('请先勾选同意用户协议');
return;
}
submitting.value = true;
const _fn = isCaptchaLogin.value ? fetchAuthorizationsCaptcha : postLoginPassword;
const { code, data, message: errorInfo } = await _fn(loginForm);
if (code === 10001) {
errMsg.value = errorInfo;
return;
}
if (code === 200) {
// 处理登录成功逻辑
message.success('登录成功');
userStore.setToken(data.access_token);
const { invite_code } = route.query;
if (invite_code) {
const { code } = await joinEnterpriseByInviteCode(invite_code as string);
if (code === 200) {
message.success('加入企业成功');
}
}
getProfileInfo();
}
} finally {
submitting.value = false;
}
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value as number);
timer.value = null;
}
}, 1000);
};
const onForgetPassword = () => {
setPageType('resetPasswordForm');
};
const onRegister = () => {
setPageType('registerForm');
};
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>