Files
qiming/qiming-mobile/subpackages/pages/login/components/login-form/login-form.uvue

644 lines
16 KiB
Plaintext
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.
<template>
<view class="container">
<!-- 分段器组件 -->
<segmented-control
:options="loginOptions"
v-model="currentLoginType"
@change="handleLoginTypeChange"
class="login-segmented"
/>
<view class="sub-title-wrapper">
<text class="sub-title">{{
t("Mobile.Auth.welcomeUse", {
siteName: tenantConfigInfo?.siteName || "",
})
}}</text>
</view>
<view class="phone-input-wrapper" v-if="!isEmailAuth">
<view class="country-code">
<text>{{ t("Mobile.Auth.defaultAreaCode") }}</text>
<uni-icon class="iconfont icon-a-Chevrondown" />
</view>
<input
class="phone-input"
type="number"
:placeholder="t('Mobile.Auth.inputPhone')"
v-model="phoneNumber"
cursor-color="#5147FF"
/>
<view class="clear-btn" v-if="phoneNumber" @click="clearPhoneNumber">
<uni-icon class="iconfont icon-a-Xcircle-fill clear-icon" />
</view>
</view>
<view v-else class="phone-input-wrapper">
<input
class="phone-input"
type="email"
:placeholder="t('Mobile.Auth.inputEmail')"
v-model="emailNumber"
cursor-color="#5147FF"
/>
<view class="clear-btn" v-if="emailNumber" @click="clearEmailNumber">
<uni-icon class="iconfont icon-a-Xcircle-fill clear-icon" />
</view>
</view>
<view class="password-input-wrapper" v-if="!isCaptchaVerify">
<input
class="password-input"
:password="!showPassword"
:placeholder="t('Mobile.Auth.inputPassword')"
v-model="password"
cursor-color="#5147FF"
/>
<!-- <view class="eye-btn" @click="togglePasswordVisibility">
<text class="eye-icon">{{ showPassword ? '👁' : '🙈' }}</text>
</view> -->
<view class="clear-btn" v-if="password" @click="clearPassword">
<uni-icon class="iconfont icon-a-Xcircle-fill clear-icon" />
</view>
</view>
<button
class="login-btn"
@click="onFinish"
:loading="loading"
:disabled="loading"
>
{{
isCaptchaVerify
? t("Mobile.Auth.nextStep")
: t("Mobile.Auth.login")
}}
</button>
<agreement-checkbox v-model="agreeTerms" />
<!-- #ifdef H5 || WEB-->
<button :id="buttonId" type="button" class="captcha-button" />
<aliyun-captcha-h5
:config="tenantConfigInfo"
:element-id="buttonId"
@doAction="handlerSuccess"
/>
<!-- #endif -->
</view>
</template>
<script setup lang="uts">
import { ref } from "vue";
import { apiLogin } from "@/servers/account";
import type { ILoginResult, LoginFieldType } from "@/types/interfaces/login";
import { SUCCESS_CODE, AGENT_NOT_EXIST } from "@/constants/codes.constants";
import { ACCESS_TOKEN, EXPIRE_DATE } from "@/constants/home.constants";
import { apiSendCode } from "@/servers/account";
import type { TenantConfigInfo } from "@/types/interfaces/login";
import SegmentedControl from "@/components/segmented-control/segmented-control.uvue";
import AgreementCheckbox from "@/components/agreement-checkbox/agreement-checkbox.uvue";
import { redirectTo } from "@/utils/common.uts";
import { useI18n } from "@/utils/i18n";
const { t } = useI18n();
// 定义组件props
interface Props {
tenantConfigInfo?: TenantConfigInfo;
redirectUrl?: string;
isPopup?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
tenantConfigInfo: null,
redirectUrl: "",
isPopup: false,
});
const emit = defineEmits(["show-verify", "login-success"]);
const phoneNumber = ref("");
const emailNumber = ref("");
const password = ref("");
const agreeTerms = ref(false);
const showPassword = ref(false);
const captchaVerifyParam = ref(""); // 验证码
const isCaptchaVerify = ref(false); // 是否验证码
const loading = ref(false);
// 阿里云验证码按钮id
const buttonId = ref<string>("aliyun-captcha-id");
// 分段器相关数据
const currentLoginType = ref("password");
const loginOptions = computed(() => [
{ label: t("Mobile.Auth.passwordLogin"), value: "password" },
{ label: t("Mobile.Auth.codeLoginRegister"), value: "phone" },
]);
// 处理登录方式切换
const handleLoginTypeChange = (value: string | number, index: number) => {
currentLoginType.value = value as string;
// 根据选择的登录方式更新状态
switch (value) {
case "password":
isCaptchaVerify.value = false;
break;
case "phone":
isCaptchaVerify.value = true;
break;
}
};
// 判断配置信息是否邮箱登录
const isEmailAuth = computed(() => {
if (!props.tenantConfigInfo) {
return false;
}
return props.tenantConfigInfo?.authType === 3;
});
const onFinish = () => {
if (!agreeTerms.value) {
uni.showModal({
title: t("Mobile.Common.tip"),
content: t("Mobile.Auth.pleaseAgreeProtocol"),
confirmText: t("Mobile.Common.agree"),
cancelText: t("Mobile.Common.disagree"),
success: function (res) {
if (res.confirm) {
agreeTerms.value = true;
doLogin();
}
},
});
} else {
doLogin();
}
};
const doLogin = () => {
// 阿里云验证码
const tenantConfigInfo = props?.tenantConfigInfo;
const { captchaSceneId, captchaPrefix, openCaptcha } =
tenantConfigInfo || {};
// 只有同时满足三个条件才启用验证码场景ID存在、身份标存在、开启验证码
const needAliyunCaptcha = !!(
tenantConfigInfo &&
captchaSceneId !== "" &&
captchaPrefix !== "" &&
openCaptcha
);
// 如果需要阿里云验证码,则点击按钮触发验证码
if (needAliyunCaptcha) {
// #ifdef H5 || WEB
// H5和WEB端验证码
document?.getElementById(buttonId.value)?.click();
// #endif
// #ifdef MP-WEIXIN
// 微信小程序直接执行登录/验证码逻辑
handleClickAliyunCaptcha();
// #endif
} else {
//不需要阿里云验证码,直接执行登录/验证码逻辑
handlerSuccess();
}
};
const handleClickAliyunCaptcha = () => {
uni.navigateTo({
url: "/subpackages/pages/aliyun-captcha/aliyun-captcha",
events: {
getCaptchaVerifyParam: (captchaVerifyParam) => {
// 定义一个getCaptchaVerifyParam事件获取二次验证参数captchaVerifyParam
console.log("验证码返回结果:", captchaVerifyParam);
// 发起二次校验
handlerSuccess(captchaVerifyParam);
},
},
});
};
const handlerSuccess = (captchaVerifyParam: string = "") => {
if (isCaptchaVerify.value) {
// 验证码登录
handleNext(captchaVerifyParam);
} else {
// 密码登录
handleLogin(captchaVerifyParam);
}
};
const handleLogin = (captchaVerifyParam: string = "") => {
if (isEmailAuth.value) {
handleLoginByEmail(captchaVerifyParam);
} else {
handleLoginByPhone(captchaVerifyParam);
}
};
// 校验登录参数
const validateLoginParams = () => {
if (isEmailAuth.value) {
// 校验邮箱是否为空
if (!emailNumber.value) {
uni.showToast({
title: t("Mobile.Auth.inputEmail"),
icon: "none",
});
return false;
}
// 校验邮箱格式
const emailRegex =
/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/;
if (!emailRegex.test(emailNumber.value)) {
uni.showToast({
title: t("Mobile.Auth.invalidEmailFormat"),
icon: "none",
});
return false;
}
} else {
// 校验手机号是否为空
if (!phoneNumber.value) {
uni.showToast({
title: t("Mobile.Auth.inputPhone"),
icon: "none",
});
return false;
}
// 校验手机号格式
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(phoneNumber.value)) {
uni.showToast({
title: t("Mobile.Auth.invalidPhoneFormat"),
icon: "none",
});
return false;
}
}
return true;
};
// 下一步
const handleNext = async (captchaVerifyParam: string = "") => {
// 校验登录参数
if (!validateLoginParams()) {
return;
}
const params = {
type: "LOGIN_OR_REGISTER",
captchaVerifyParam,
// "phone": phoneNumber.value
};
if (isEmailAuth.value) {
params.email = emailNumber.value;
} else {
params.phone = phoneNumber.value;
}
const phoneOrEmail = isEmailAuth.value
? emailNumber.value
: phoneNumber.value;
// 发送验证码
try {
loading.value = true;
const { code, data, message } = await apiSendCode(params);
if (isEmailAuth.value) {
// 邮箱验证码
if (code === SUCCESS_CODE) {
// 系统未配置邮件服务请直接输出本次验证码779895
uni.showToast({
title: t("Mobile.Auth.codeSentSuccess"),
icon: "none",
});
emit("show-verify", phoneOrEmail, captchaVerifyParam);
} else if (code === AGENT_NOT_EXIST) {
uni.showToast({
title: message,
icon: "none",
duration: 5000,
});
emit("show-verify", phoneOrEmail, captchaVerifyParam);
}
} else {
// 手机验证码
if (code === SUCCESS_CODE) {
uni.showToast({
title: t("Mobile.Auth.codeSentSuccess"),
icon: "none",
});
emit("show-verify", phoneOrEmail, captchaVerifyParam);
} else {
// 手机验证失败需要将 message 消息提示出来
uni.showToast({
title: message,
icon: "none",
});
}
}
} finally {
loading.value = false;
}
};
// 手机号登录
const handleLoginByPhone = async (captchaVerifyParam: string = "") => {
// 校验登录参数
if (!validateLoginParams()) {
return;
}
if (!password.value) {
uni.showToast({
title: t("Mobile.Auth.inputPassword"),
icon: "none",
});
return;
}
// 执行登录逻辑
const params: LoginFieldType = {
phoneOrEmail: phoneNumber.value,
areaCode: "86",
password: password.value,
captchaVerifyParam,
};
try {
loading.value = true;
const result = await apiLogin(params);
handleLoginSuccess(result);
} finally {
loading.value = false;
}
};
// 邮箱登录
const handleLoginByEmail = async (captchaVerifyParam: string = "") => {
// 校验登录参数
if (!validateLoginParams()) {
return;
}
if (!password.value) {
uni.showToast({
title: t("Mobile.Auth.inputPassword"),
icon: "none",
});
return;
}
// 执行登录逻辑
const params: LoginFieldType = {
phoneOrEmail: emailNumber.value,
areaCode: "",
password: password.value,
captchaVerifyParam,
};
const { code, data, message } = await apiLogin(params);
if (code === SUCCESS_CODE) {
// 邮箱登录
uni.showToast({
title: t("Mobile.Auth.loginSuccess"),
icon: "success",
});
setTimeout(() => {
// #ifdef H5 || WEB
if (process.env.NODE_ENV === "development") {
uni.setStorageSync(ACCESS_TOKEN, data.token);
}
// #endif
// #ifdef MP-WEIXIN
uni.setStorageSync(ACCESS_TOKEN, data.token);
// #endif
if (props.isPopup) {
emit("login-success");
} else {
redirectTo(props.redirectUrl);
}
}, 1000);
} else if (code === AGENT_NOT_EXIST) {
uni.showToast({
title: message,
icon: "none",
});
}
};
// 登录成功后
const handleLoginSuccess = (result: ILoginResult) => {
const { code, data, message } = result;
if (code === SUCCESS_CODE) {
// 手机号登录
uni.showToast({
title: t("Mobile.Auth.loginSuccess"),
icon: "success",
});
setTimeout(() => {
// #ifdef H5 || WEB
if (process.env.NODE_ENV === "development") {
uni.setStorageSync(ACCESS_TOKEN, data.token);
}
// #endif
// #ifdef MP-WEIXIN
uni.setStorageSync(ACCESS_TOKEN, data.token);
// #endif
if (props.isPopup) {
emit("login-success");
} else {
redirectTo(props.redirectUrl);
}
}, 1000);
} else if (code === AGENT_NOT_EXIST) {
uni.showToast({
title: message,
icon: "none",
});
}
};
const clearPhoneNumber = () => {
phoneNumber.value = "";
};
const clearEmailNumber = () => {
emailNumber.value = "";
};
const clearPassword = () => {
password.value = "";
};
const togglePasswordVisibility = () => {
showPassword.value = !showPassword.value;
};
</script>
<style lang="scss" scoped>
.container {
// .login-segmented {
// margin-bottom: 80rpx;
// }
.sub-title-wrapper {
margin-top: 80rpx;
margin-bottom: 48rpx;
.sub-title {
color: #000;
text-align: center;
font-size: 40rpx;
font-style: normal;
font-weight: 600;
line-height: 56rpx;
}
}
.clear-btn {
position: absolute;
right: 32rpx;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
.clear-icon {
color: #b2b4b8;
font-size: 32rpx;
}
}
.phone-input-wrapper {
display: flex;
flex-direction: row;
align-items: center;
background: rgba(12, 20, 102, 0.04);
border-radius: 16rpx;
padding: 24rpx 32rpx;
margin-bottom: 36rpx;
position: relative;
border: 2rpx solid transparent;
transition: border-color 0.3s ease;
&:focus-within {
border-color: #5147ff;
}
.country-code {
color: rgba(21, 23, 31, 1);
margin-right: 24rpx;
border-right: 2rpx solid rgba(12, 20, 102, 0.12);
padding-right: 24rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 10rpx;
text {
margin-left: 10rpx;
font-size: 32rpx;
font-weight: 400;
line-height: 48rpx;
}
.iconfont {
font-size: 28rpx;
}
}
.phone-input {
flex: 1;
background: transparent;
border: none;
outline: none;
font-size: 32rpx;
color: #15171f;
font-weight: 400;
line-height: 48rpx;
padding-right: 50rpx;
caret-color: #5147ff;
cursor-color: #5147ff;
}
}
.password-input-wrapper {
background: rgba(12, 20, 102, 0.04);
border-radius: 16rpx;
padding: 24rpx 32rpx;
margin-bottom: 36rpx;
position: relative;
border: 2rpx solid transparent;
transition: border-color 0.3s ease;
&:focus-within {
border-color: #5147ff;
}
.password-input {
width: 100%;
background: transparent;
border: none;
outline: none;
font-size: 32rpx;
color: #15171f;
font-weight: 400;
line-height: 48rpx;
padding-right: 60rpx;
caret-color: #5147ff;
cursor-color: #5147ff;
}
.eye-btn {
position: absolute;
right: 80rpx;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
.eye-icon {
font-size: 32rpx;
color: #b2b4b8;
}
}
}
.login-btn {
margin-top: 20rpx;
width: 100%;
border-radius: 16rpx;
padding: 25rpx 24rpx;
font-size: 32rpx;
font-weight: 400;
margin-bottom: 32rpx;
line-height: 48rpx;
text-align: center;
background: #5147ff;
color: #ffffff;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.captcha-button {
position: absolute;
left: 10px;
top: 10px;
visibility: hidden;
opacity: 0;
}
}
</style>