Files
gao-customer-h5/src/pages/user/component/SmsLogin.vue
T
2026-09-12 23:51:38 +08:00

226 lines
5.1 KiB
Vue

<template>
<t-form class="sms-login" @submit="login" label-width="6rem">
<t-form-item label="手机号">
<t-input v-model.trim="formModel.mobilePhone" type="tel" inputmode="tel" placeholder="请输入手机号" borderless />
</t-form-item>
<t-form-item label="图形验证码">
<t-input v-model.trim="formModel.captcha" class="captcha-input" placeholder="请输入验证码" borderless />
</t-form-item>
<div class="captcha-wrapper">
<captcha ref="captchaRef" class="captcha" :width="120" :height="40" :request="CommonAPI.captcha" />
</div>
<t-form-item label="短信验证码">
<t-input v-model.trim="formModel.smsCode" class="sms-code" placeholder="请输入短信验证码" borderless />
</t-form-item>
<div class="send-wrapper">
<t-button
type="button"
class="send"
size="extra-small"
theme="primary"
:content="sendSmsText"
:disabled="!canSendSms"
@click="sendSms"
/>
</div>
<div class="exec">
<t-button theme="primary" class="button" type="submit" :loading="isSubmitting" :disabled="isSubmitting || !props.agreementAccepted">登录</t-button>
</div>
</t-form>
</template>
<script setup lang="ts">
import { Message } from "@/utils/message";
import { Captcha } from "timi-web";
import { CommonAPI, NotifyAPI, UserAPI, useUserStore } from "timi-web-api";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
type CaptchaExpose = {
captchaId: string;
update: () => Promise<void>;
};
type SmsForm = {
mobilePhone: string;
captcha: string;
smsCode: string;
notifyDetailId: string;
};
defineOptions({
name: "SmsLogin"
});
const props = withDefaults(defineProps<{
agreementAccepted?: boolean;
}>(), {
agreementAccepted: false
});
const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const formModel = reactive<SmsForm>({
mobilePhone: "",
captcha: "",
smsCode: "",
notifyDetailId: ""
});
const captchaRef = ref<CaptchaExpose | null>(null);
const isSubmitting = ref(false);
const sendSmsCountdown = ref(0);
let sendSmsTimer: number | undefined;
const isSendingSms = computed(() => 0 < sendSmsCountdown.value);
const sendSmsText = computed(() => isSendingSms.value ? `${sendSmsCountdown.value} 秒后重新发送` : "发送验证码");
const canSendSms = computed(() => (
!isSendingSms.value
&& /^1\d{10}$/.test(formModel.mobilePhone)
&& 4 === formModel.captcha.length
));
function getRedirectPath(): string {
return typeof route.query.redirect === "string" ? route.query.redirect : "/";
}
function getCaptchaId(): string {
return captchaRef.value?.captchaId || "";
}
function clearSmsTimer(): void {
if (sendSmsTimer === undefined) {
return;
}
window.clearInterval(sendSmsTimer);
sendSmsTimer = undefined;
}
function startSmsTimer(): void {
clearSmsTimer();
sendSmsCountdown.value = 60;
sendSmsTimer = window.setInterval(() => {
sendSmsCountdown.value -= 1;
if (!(0 < sendSmsCountdown.value)) {
clearSmsTimer();
}
}, 1000);
}
async function sendSms(): Promise<void> {
if (!canSendSms.value) {
return;
}
const captchaId = getCaptchaId();
if (!captchaId) {
Message.warning("图形验证码尚未加载,请稍后重试");
await captchaRef.value?.update();
return;
}
try {
formModel.notifyDetailId = await NotifyAPI.smsSend({
captchaId,
captcha: formModel.captcha,
data: {
detailList: [
{
sendTo: formModel.mobilePhone
}
]
}
});
startSmsTimer();
Message.success(`验证码已发送,请检查 ${formModel.mobilePhone} 手机短信(易保网络)`, 5000);
} catch {
formModel.notifyDetailId = "";
} finally {
formModel.captcha = "";
await captchaRef.value?.update();
}
}
function validateForm(): boolean {
if (!props.agreementAccepted) {
Message.warning("请先阅读并同意用户协议");
return false;
}
if (!/^1\d{10}$/.test(formModel.mobilePhone)) {
Message.warning("请输入正确的 11 位手机号");
return false;
}
if (!formModel.notifyDetailId || !formModel.smsCode) {
Message.warning("请先获取并输入短信验证码");
return false;
}
return true;
}
async function login(): Promise<void> {
if (isSubmitting.value || !validateForm()) {
return;
}
isSubmitting.value = true;
try {
await userStore.updateLoginUser(await UserAPI.loginBySms({
detailId: formModel.notifyDetailId,
captcha: formModel.smsCode
}));
await currentStoreStore.loadAfterLogin();
await router.replace(getRedirectPath());
} catch {
formModel.smsCode = "";
} finally {
isSubmitting.value = false;
}
}
onUnmounted(() => {
clearSmsTimer();
});
</script>
<style scoped lang="less">
.sms-login {
padding: .25rem 0;
background: transparent;
.send-wrapper,
.captcha-wrapper {
display: flex;
margin-top: .5rem;
justify-content: end;
.captcha {
border: 1px solid var(--td-border-level-1-color);
}
}
.sms-code {
.send {
height: 1.5rem;
}
}
.exec {
display: flex;
margin-top: .5rem;
justify-content: center;
.button {
width: 10rem;
}
}
}
:global(html.theme-dark) {
.sms-login {
.captcha {
background: #0f172a;
}
}
}
</style>