644 lines
16 KiB
Plaintext
644 lines
16 KiB
Plaintext
<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>
|