support sms login
This commit is contained in:
@@ -1,212 +0,0 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<section class="login-panel">
|
||||
<div class="brand">
|
||||
<div class="logo">
|
||||
<span>糕</span>
|
||||
</div>
|
||||
<h1>糕店客户管理</h1>
|
||||
</div>
|
||||
<t-form class="form" label-align="left">
|
||||
<t-form-item label="账号">
|
||||
<t-input
|
||||
v-model.trim="formModel.user"
|
||||
type="tel"
|
||||
inputmode="tel"
|
||||
placeholder="请输入账号或手机号"
|
||||
borderless
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="密码">
|
||||
<t-input
|
||||
v-model.trim="formModel.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
borderless
|
||||
/>
|
||||
</t-form-item>
|
||||
<t-form-item label="验证码">
|
||||
<t-input
|
||||
v-model.trim="formModel.captcha"
|
||||
placeholder="请输入验证码"
|
||||
borderless
|
||||
/>
|
||||
</t-form-item>
|
||||
<div class="captcha-wrapper">
|
||||
<captcha class="captcha" ref="captchaRef" />
|
||||
</div>
|
||||
<t-button class="submit" theme="primary" size="medium" :loading="isSubmitting" @click="login">登录</t-button>
|
||||
</t-form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {Toast} from "tdesign-mobile-vue";
|
||||
import {Captcha, useUserStore} from "timi-web";
|
||||
import {useCurrentStoreStore} from "@/store/currentStoreStore";
|
||||
|
||||
type CaptchaExpose = {
|
||||
captchaId: string;
|
||||
update: () => Promise<void>;
|
||||
};
|
||||
|
||||
defineOptions({
|
||||
name: "Login"
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
const formModel = reactive({
|
||||
user: "",
|
||||
password: "",
|
||||
captcha: ""
|
||||
});
|
||||
const isSubmitting = ref(false);
|
||||
const captchaRef = ref<CaptchaExpose | null>(null);
|
||||
|
||||
function getCaptchaId(): string {
|
||||
return captchaRef.value?.captchaId || "";
|
||||
}
|
||||
|
||||
async function login(): Promise<void> {
|
||||
const captchaId = getCaptchaId();
|
||||
if (!formModel.user || !formModel.password || !formModel.captcha || !captchaId) {
|
||||
Toast({
|
||||
theme: "warning",
|
||||
direction: "row",
|
||||
message: "请输入账号、密码和验证码"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
await userStore.login({
|
||||
captchaId,
|
||||
captcha: formModel.captcha,
|
||||
data: {
|
||||
user: formModel.user,
|
||||
password: formModel.password
|
||||
}
|
||||
});
|
||||
await currentStoreStore.loadAfterLogin();
|
||||
const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/management";
|
||||
await router.replace(redirect);
|
||||
} catch (error) {
|
||||
formModel.captcha = "";
|
||||
await captchaRef.value?.update();
|
||||
throw error;
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.login-page {
|
||||
width: 100%;
|
||||
padding: 4.5rem 1rem 1.5rem 1rem;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: #F5F5F8;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
.login-panel {
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
max-width: 28rem;
|
||||
background: rgba(255, 255, 255, .94);
|
||||
box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08);
|
||||
border-radius: 1.4rem;
|
||||
flex-direction: column;
|
||||
|
||||
.captcha-wrapper {
|
||||
display: flex;
|
||||
margin-top: .5rem;
|
||||
justify-content: end;
|
||||
|
||||
.captcha {
|
||||
border: 1px solid var(--td-border-level-1-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
h1 {
|
||||
margin: .875rem 0 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: #FFF;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
display: grid;
|
||||
background: #0f766e;
|
||||
box-shadow: 0 .75rem 1.5rem rgba(15, 118, 110, .22);
|
||||
place-items: center;
|
||||
border-radius: 1.25rem;
|
||||
|
||||
span {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
margin: 0;
|
||||
padding: .25rem 0;
|
||||
background: transparent;
|
||||
|
||||
.captcha-row {
|
||||
gap: .75rem;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.tui-captcha) {
|
||||
flex: 0 0 7.5rem;
|
||||
}
|
||||
|
||||
.submit {
|
||||
gap: .5rem;
|
||||
width: 8rem;
|
||||
margin: 2rem auto 0 auto;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.login-page {
|
||||
background: #10161f;
|
||||
|
||||
.login-panel {
|
||||
background: rgba(15, 23, 42, .88);
|
||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
:deep(.tui-captcha) {
|
||||
color: #94a3b8;
|
||||
background: #0f172a;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<section class="login-panel">
|
||||
<div class="brand">
|
||||
<div class="logo"><span>糕</span></div>
|
||||
<h1>糕店客户管理</h1>
|
||||
</div>
|
||||
<t-segmented class="login-type" v-model="loginType" :options="loginTypeOptions" />
|
||||
<password-login v-if="loginType === 'password'" class="form" />
|
||||
<sms-login v-else class="form" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PasswordLogin from "@/pages/user/component/PasswordLogin.vue";
|
||||
import SmsLogin from "@/pages/user/component/SmsLogin.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "LoginIndex"
|
||||
});
|
||||
|
||||
type LoginType = "password" | "sms";
|
||||
|
||||
const loginType = ref<LoginType>("password");
|
||||
const loginTypeOptions = [
|
||||
{ value: "password" as LoginType, label: "密码登录" },
|
||||
{ value: "sms" as LoginType, label: "短信登录" }
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.login-page {
|
||||
width: 100%;
|
||||
padding: 4.5rem 1rem 1.5rem;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: #F5F5F8;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
|
||||
.login-panel {
|
||||
width: 100%;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
max-width: 28rem;
|
||||
background: rgba(255, 255, 255, .94);
|
||||
box-shadow: 0 .75rem 2rem rgba(15, 23, 42, .08);
|
||||
border-radius: 1.4rem;
|
||||
flex-direction: column;
|
||||
|
||||
.form {
|
||||
margin: 0;
|
||||
|
||||
:deep(.t-form__label) {
|
||||
width: 5.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.login-type {
|
||||
width: fit-content;
|
||||
margin: 1rem auto 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
|
||||
h1 {
|
||||
margin: .875rem 0 0;
|
||||
color: #0f172a;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: #FFF;
|
||||
display: grid;
|
||||
background: #0f766e;
|
||||
box-shadow: 0 .75rem 1.5rem rgba(15, 118, 110, .22);
|
||||
place-items: center;
|
||||
border-radius: 1.25rem;
|
||||
|
||||
span {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.login-page {
|
||||
background: #10161f;
|
||||
|
||||
.login-panel {
|
||||
background: rgba(15, 23, 42, .88);
|
||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<t-form class="password-login" @submit="login">
|
||||
<t-form-item label="账号">
|
||||
<t-input v-model.trim="formModel.user" type="tel" inputmode="tel" placeholder="请输入账号或手机号" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="密码">
|
||||
<t-input v-model="formModel.password" type="password" placeholder="请输入密码" borderless />
|
||||
</t-form-item>
|
||||
<t-form-item label="验证码">
|
||||
<t-input v-model.trim="formModel.captcha" placeholder="请输入验证码" borderless />
|
||||
</t-form-item>
|
||||
<div class="captcha-wrapper">
|
||||
<captcha class="captcha" ref="captchaRef" :width="120" :height="40" />
|
||||
</div>
|
||||
<div class="exec">
|
||||
<t-button theme="primary" class="button" type="submit" :loading="isSubmitting" :disabled="isSubmitting">登录</t-button>
|
||||
</div>
|
||||
</t-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { Captcha, useUserStore } from "timi-web";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
|
||||
type CaptchaExpose = {
|
||||
captchaId: string;
|
||||
update: () => Promise<void>;
|
||||
};
|
||||
|
||||
type PasswordForm = {
|
||||
user: string;
|
||||
password: string;
|
||||
captcha: string;
|
||||
};
|
||||
|
||||
defineOptions({
|
||||
name: "PasswordLogin"
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const currentStoreStore = useCurrentStoreStore();
|
||||
const formModel = reactive<PasswordForm>({
|
||||
user: "",
|
||||
password: "",
|
||||
captcha: ""
|
||||
});
|
||||
const isSubmitting = ref(false);
|
||||
const captchaRef = ref<CaptchaExpose | null>(null);
|
||||
|
||||
function getRedirectPath(): string {
|
||||
return typeof route.query.redirect === "string" ? route.query.redirect : "/management";
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
if (!formModel.user || !formModel.password || !formModel.captcha || !captchaRef.value?.captchaId) {
|
||||
Toast.warning("请输入账号、密码和验证码");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function login(): Promise<void> {
|
||||
if (isSubmitting.value || !validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
await userStore.login({
|
||||
captchaId: captchaRef.value?.captchaId || "",
|
||||
captcha: formModel.captcha,
|
||||
data: {
|
||||
user: formModel.user,
|
||||
password: formModel.password
|
||||
}
|
||||
});
|
||||
await currentStoreStore.loadAfterLogin();
|
||||
await router.replace(getRedirectPath());
|
||||
} catch {
|
||||
formModel.captcha = "";
|
||||
await captchaRef.value?.update();
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.password-login {
|
||||
padding: .25rem 0;
|
||||
background: transparent;
|
||||
|
||||
.captcha-wrapper {
|
||||
display: flex;
|
||||
margin-top: .5rem;
|
||||
justify-content: end;
|
||||
|
||||
.captcha {
|
||||
border: 1px solid var(--td-border-level-1-color);
|
||||
}
|
||||
}
|
||||
|
||||
.exec {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
justify-content: center;
|
||||
|
||||
.button {
|
||||
width: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(html.theme-dark) {
|
||||
.password-login {
|
||||
.captcha {
|
||||
background: #0f172a;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,214 @@
|
||||
<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" />
|
||||
</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">登录</t-button>
|
||||
</div>
|
||||
</t-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Toast } from "tdesign-mobile-vue";
|
||||
import { Captcha, NotifyAPI, UserAPI, useUserStore } from "timi-web";
|
||||
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 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) {
|
||||
Toast.warning("图形验证码尚未加载,请稍后重试");
|
||||
await captchaRef.value?.update();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
formModel.notifyDetailId = await NotifyAPI.smsSend({
|
||||
captchaId,
|
||||
captcha: formModel.captcha,
|
||||
data: {
|
||||
detailList: [
|
||||
{
|
||||
sendTo: formModel.mobilePhone
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
startSmsTimer();
|
||||
Toast.success(`验证码已发送,请检查 ${formModel.mobilePhone} 手机短信`);
|
||||
} catch {
|
||||
formModel.notifyDetailId = "";
|
||||
} finally {
|
||||
formModel.captcha = "";
|
||||
await captchaRef.value?.update();
|
||||
}
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
if (!/^1\d{10}$/.test(formModel.mobilePhone)) {
|
||||
Toast.warning("请输入正确的 11 位手机号");
|
||||
return false;
|
||||
}
|
||||
if (!formModel.notifyDetailId || !formModel.smsCode) {
|
||||
Toast.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>
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { createRouter, createWebHistory, type RouteLocationNormalized } from "vue-router";
|
||||
import RootLayout from "@/layout/RootLayout.vue";
|
||||
import MainLayout from "@/layout/MainLayout.vue";
|
||||
import Login from "@/pages/Login.vue";
|
||||
import Login from "@/pages/user/LoginIndex.vue";
|
||||
import InviteRegister from "@/pages/InviteRegister.vue";
|
||||
import chartRoutes from "@/router/routes/chartRoutes";
|
||||
import customerRoutes from "@/router/routes/customerRoutes";
|
||||
|
||||
Reference in New Issue
Block a user