226 lines
5.1 KiB
Vue
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>
|