chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,450 @@
<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>