Files
qiming/qiming-mobile/subpackages/pages/login/components/captcha-verify/captcha-verify.uvue

451 lines
11 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="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>