451 lines
11 KiB
Plaintext
451 lines
11 KiB
Plaintext
<template>
|
||
<view class="captcha-verify">
|
||
<view class="captcha-verify-header">
|
||
<!-- 返回按钮 -->
|
||
<view class="back-box" @tap="handleBack">
|
||
<text class="iconfont icon-a-Chevronleft"></text>
|
||
</view>
|
||
</view>
|
||
<view class="captcha-verify-content">
|
||
<view class="title">
|
||
{{
|
||
phoneOrEmail?.includes("@")
|
||
? t("Mobile.Auth.inputEmailCode")
|
||
: t("Mobile.Auth.inputSmsCode")
|
||
}}
|
||
</view>
|
||
<view class="phone-info">
|
||
{{ t("Mobile.Auth.codeSentToPrefix") }}
|
||
{{
|
||
phoneOrEmail?.includes("@")
|
||
? `${t("Mobile.Auth.codeSentToEmailTarget")} `
|
||
: `${t("Mobile.Auth.codeSentToPhoneTarget")} `
|
||
}}
|
||
{{ `${!phoneOrEmail?.includes("@") ? "86" : ""} ${phoneOrEmail}` }}
|
||
</view>
|
||
<view class="captcha-input-container">
|
||
<input
|
||
ref="captchaInputRef"
|
||
class="captcha-input"
|
||
:class="{ active: isFocused }"
|
||
type="number"
|
||
pattern="[0-9]*"
|
||
inputmode="numeric"
|
||
maxlength="6"
|
||
:value="captchaCode"
|
||
:focus="isFocused"
|
||
:adjust-position="false"
|
||
:hold-keyboard="true"
|
||
cursor-spacing="0"
|
||
:placeholder="t('Mobile.Auth.inputVerifyCode')"
|
||
cursor-color="#5147FF"
|
||
@input="handleInput"
|
||
@focus="handleFocus"
|
||
@blur="handleBlur"
|
||
@paste="handlePaste"
|
||
@longpress="handleLongPress"
|
||
/>
|
||
</view>
|
||
<view class="resend-container">
|
||
<text class="countdown" v-if="countdown > 0">{{
|
||
t("Mobile.Auth.resendAfterSeconds", { seconds: countdown })
|
||
}}</text>
|
||
<text
|
||
class="resend-btn"
|
||
:class="{ disabled: countdown > 0 }"
|
||
@tap="handleResend"
|
||
>{{ t("Mobile.Auth.resend") }}</text
|
||
>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script lang="uts" setup>
|
||
import { apiSendCode } from "@/servers/account";
|
||
import { SUCCESS_CODE, AGENT_NOT_EXIST } from "@/constants/codes.constants";
|
||
import { useI18n } from "@/utils/i18n";
|
||
|
||
const { t } = useI18n();
|
||
|
||
// 定义组件props
|
||
interface Props {
|
||
phoneOrEmail?: string;
|
||
tenantConfigInfo?: TenantConfigInfo;
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
phoneOrEmail: "",
|
||
tenantConfigInfo: null,
|
||
});
|
||
|
||
const emit = defineEmits(["verify-login", "back"]);
|
||
|
||
const captchaCode = ref<string>("");
|
||
const countdown = ref<number>(59);
|
||
let countdownTimer: number | null = null;
|
||
const captchaInputRef = ref<any>(null);
|
||
const isFocused = ref<boolean>(false);
|
||
|
||
// 判断配置信息是否邮箱登录
|
||
const isEmailAuth = computed(() => {
|
||
if (!props.tenantConfigInfo) {
|
||
return false;
|
||
}
|
||
return props.tenantConfigInfo?.authType === 3;
|
||
});
|
||
|
||
onMounted(() => {
|
||
open();
|
||
});
|
||
|
||
const open = () => {
|
||
captchaCode.value = "";
|
||
startCountdown();
|
||
nextTick(() => {
|
||
focusInput();
|
||
});
|
||
};
|
||
|
||
const handleBack = () => {
|
||
clearCountdown();
|
||
emit("back");
|
||
};
|
||
|
||
const handleInput = async (event: any) => {
|
||
let value = event.detail.value || "";
|
||
|
||
// 强制过滤非数字字符
|
||
value = value.replace(/[^0-9]/g, "");
|
||
|
||
// 限制最大长度为6位
|
||
if (value.length > 6) {
|
||
value = value.slice(0, 6);
|
||
}
|
||
|
||
captchaCode.value = value;
|
||
|
||
// 强制更新输入框显示值
|
||
// #ifdef MP-WEIXIN
|
||
if (captchaInputRef.value) {
|
||
captchaInputRef.value.value = value;
|
||
}
|
||
// #endif
|
||
|
||
// 检查是否输入完成(6位数字)
|
||
if (value.length === 6 && /^\d{6}$/.test(value)) {
|
||
emit("verify-login", value);
|
||
}
|
||
};
|
||
|
||
const handleFocus = () => {
|
||
isFocused.value = true;
|
||
};
|
||
|
||
const handleBlur = () => {
|
||
isFocused.value = false;
|
||
};
|
||
|
||
const handlePaste = async (event: any) => {
|
||
// #ifdef H5
|
||
// H5平台使用 clipboardData
|
||
event.preventDefault();
|
||
let pasteData = "";
|
||
if (event.clipboardData && event.clipboardData.getData) {
|
||
pasteData = event.clipboardData.getData("text");
|
||
await processPasteData(pasteData);
|
||
}
|
||
// #endif
|
||
|
||
// #ifdef MP-WEIXIN || APP-PLUS
|
||
// 微信小程序和App平台在用户点击粘贴后触发此事件
|
||
// 延迟一下确保粘贴操作完成后再读取
|
||
setTimeout(async () => {
|
||
await getClipboardAndPaste();
|
||
}, 100);
|
||
// #endif
|
||
};
|
||
|
||
const getClipboardAndPaste = async () => {
|
||
try {
|
||
const res = await uni.getClipboardData();
|
||
await processPasteData(res.data);
|
||
} catch (error) {
|
||
uni.showToast({
|
||
title: t("Mobile.Auth.clipboardGetFailed"),
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
}
|
||
};
|
||
|
||
const processPasteData = async (pasteData: string) => {
|
||
// 验证粘贴内容是否为6位数字
|
||
if (pasteData && /^\d{6}$/.test(pasteData)) {
|
||
// 直接设置验证码
|
||
captchaCode.value = pasteData;
|
||
|
||
// 强制更新输入框显示值
|
||
// #ifdef MP-WEIXIN
|
||
if (captchaInputRef.value) {
|
||
captchaInputRef.value.value = pasteData;
|
||
}
|
||
// #endif
|
||
|
||
// 触发验证
|
||
emit("verify-login", pasteData);
|
||
|
||
// 提示用户
|
||
uni.showToast({
|
||
title: t("Mobile.Auth.codeAutoFilled"),
|
||
icon: "success",
|
||
duration: 1500,
|
||
});
|
||
} else {
|
||
// 粘贴内容不是6位数字,提示用户
|
||
uni.showToast({
|
||
title: t("Mobile.Auth.pasteSixDigits"),
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleLongPress = async (event: any) => {
|
||
// #ifdef MP-WEIXIN || APP-PLUS
|
||
// 长按时直接读取剪贴板并询问是否填充
|
||
try {
|
||
const res = await uni.getClipboardData();
|
||
const clipData = res.data;
|
||
// 检查剪贴板是否包含6位数字验证码
|
||
if (clipData && /^\d{6}$/.test(clipData)) {
|
||
// 直接填充,不询问
|
||
await processPasteData(clipData);
|
||
} else if (clipData) {
|
||
// 剪贴板有内容但不是6位数字
|
||
uni.showToast({
|
||
title: t("Mobile.Auth.clipboardNotSixDigits"),
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: t("Mobile.Auth.clipboardEmpty"),
|
||
icon: "none",
|
||
duration: 1500,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.showToast({
|
||
title: t("Mobile.Auth.clipboardReadFailed"),
|
||
icon: "none",
|
||
duration: 2000,
|
||
});
|
||
}
|
||
// #endif
|
||
};
|
||
|
||
const startCountdown = () => {
|
||
countdown.value = 59;
|
||
countdownTimer = setInterval(() => {
|
||
countdown.value--;
|
||
if (countdown.value <= 0) {
|
||
clearCountdown();
|
||
}
|
||
}, 1000);
|
||
};
|
||
|
||
const clearCountdown = () => {
|
||
if (countdownTimer) {
|
||
clearInterval(countdownTimer);
|
||
countdownTimer = null;
|
||
}
|
||
};
|
||
|
||
// 兼容微信小程序和H5的聚焦方法
|
||
const focusInput = () => {
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序使用 isFocused 响应式变量控制聚焦
|
||
isFocused.value = true;
|
||
// #endif
|
||
|
||
// #ifdef H5
|
||
// H5平台使用 ref 引用
|
||
if (captchaInputRef.value && captchaInputRef.value.focus) {
|
||
captchaInputRef.value.focus();
|
||
}
|
||
// #endif
|
||
|
||
// #ifdef APP-PLUS
|
||
// App平台使用 ref 引用
|
||
if (captchaInputRef.value && captchaInputRef.value.focus) {
|
||
captchaInputRef.value.focus();
|
||
}
|
||
// #endif
|
||
};
|
||
|
||
const handleResend = async () => {
|
||
if (countdown.value > 0) return;
|
||
const params = {
|
||
type: "LOGIN_OR_REGISTER",
|
||
// "phone": props.phoneOrEmail
|
||
};
|
||
|
||
if (isEmailAuth.value) {
|
||
params.email = props.phoneOrEmail;
|
||
} else {
|
||
params.phone = props.phoneOrEmail;
|
||
}
|
||
try {
|
||
uni.showLoading({
|
||
title: t("Mobile.Auth.sending"),
|
||
mask: false,
|
||
});
|
||
// 发送验证码
|
||
const { code, data, message } = await apiSendCode(params);
|
||
if (isEmailAuth.value) {
|
||
// 邮箱验证码
|
||
if (code === SUCCESS_CODE) {
|
||
startCountdown();
|
||
uni.showToast({
|
||
title: t("Mobile.Auth.resendSuccess"),
|
||
icon: "success",
|
||
});
|
||
} else if (code === AGENT_NOT_EXIST) {
|
||
uni.showToast({
|
||
title: message,
|
||
icon: "none",
|
||
duration: 5000,
|
||
});
|
||
}
|
||
} else {
|
||
// 手机验证码
|
||
if (code === SUCCESS_CODE) {
|
||
startCountdown();
|
||
uni.showToast({
|
||
title: t("Mobile.Auth.resendSuccess"),
|
||
icon: "success",
|
||
});
|
||
} else {
|
||
// 手机验证失败提示消息
|
||
uni.showToast({
|
||
title: message,
|
||
icon: "none",
|
||
});
|
||
}
|
||
}
|
||
} finally {
|
||
uni.hideLoading();
|
||
}
|
||
};
|
||
|
||
onUnmounted(() => {
|
||
clearCountdown();
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.captcha-verify {
|
||
overflow: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.captcha-verify-header {
|
||
.back-box {
|
||
display: inline-block;
|
||
width: 58rpx;
|
||
height: 58rpx;
|
||
line-height: 58rpx;
|
||
text-align: center;
|
||
background-color: rgba(12, 20, 102, 0.04);
|
||
border-radius: 50%;
|
||
|
||
.iconfont {
|
||
font-size: 40rpx;
|
||
line-height: 58rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
.captcha-verify-content {
|
||
flex: 1;
|
||
overflow: auto;
|
||
|
||
.title {
|
||
font-size: 40rpx;
|
||
line-height: 56rpx;
|
||
font-weight: 600;
|
||
color: rgba(21, 23, 31, 1);
|
||
margin-bottom: 24rpx;
|
||
line-height: 64rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.phone-info {
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
color: rgba(21, 23, 31, 0.7);
|
||
margin-bottom: 80rpx;
|
||
line-height: 44rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.captcha-input-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 80rpx;
|
||
|
||
.captcha-input {
|
||
width: 100%;
|
||
// max-width: 600rpx;
|
||
height: 88rpx;
|
||
border: 2rpx solid transparent;
|
||
border-radius: 16rpx;
|
||
text-align: left;
|
||
font-size: 32rpx;
|
||
font-weight: 400;
|
||
color: rgba(21, 23, 31, 1);
|
||
background-color: rgba(12, 20, 102, 0.04);
|
||
padding: 0 24rpx;
|
||
// letter-spacing: 8rpx;
|
||
|
||
&.active {
|
||
border-color: rgba(81, 71, 255, 1);
|
||
}
|
||
|
||
&:focus {
|
||
border-color: rgba(81, 71, 255, 1);
|
||
background-color: #fff;
|
||
outline: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.resend-container {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 16rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 400;
|
||
line-height: 44rpx;
|
||
color: rgba(21, 23, 31, 0.7);
|
||
|
||
.countdown {
|
||
}
|
||
|
||
.resend-btn {
|
||
cursor: pointer;
|
||
color: #5147ff;
|
||
&.disabled {
|
||
color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|