edit login & otp

This commit is contained in:
2026-05-31 14:12:18 +03:30
parent 2f485914b4
commit 27c44710be
34 changed files with 448 additions and 67 deletions
+52 -37
View File
@@ -1,46 +1,60 @@
<template>
<AuthLayout title="منتایم" subtitle="ورود به حساب کاربری">
<p
v-if="error"
class="mb-4 rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
>
{{ error }}
</p>
<AuthLayout brand subtitle="ورود به حساب کاربری">
<div class="flex flex-1 flex-col justify-start gap-8 pt-10">
<section class="flex flex-col items-center text-center">
<AppLogo variant="vertical" class="mb-3 h-28 w-auto object-contain" />
<p class="max-w-xs text-sm leading-7 text-muted">
نوبت‌دهی آنلاین برای مشتریان<br />
مدیریت ساده برای کسب‌وکار شما
</p>
</section>
<AuthModeSwitch v-model="mode" />
<section class="flex flex-col gap-4">
<p
v-if="error"
class="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-600 dark:text-red-400"
>
{{ error }}
</p>
<form class="mt-auto space-y-4" @submit.prevent="onSubmit">
<AppInput
id="mobile"
v-model="mobile"
label="شماره موبایل"
type="tel"
inputmode="numeric"
placeholder="09121234567"
autocomplete="tel"
ltr
required
/>
<div class="flex flex-col gap-8">
<AuthModeSwitch v-model="mode" />
<AppInput
v-if="mode === 'password'"
id="password"
v-model="password"
label="رمز عبور"
type="password"
autocomplete="current-password"
ltr
required
/>
<form class="flex flex-col gap-4" @submit.prevent="onSubmit">
<AppInput
id="mobile"
v-model="mobile"
label="شماره موبایل"
type="tel"
inputmode="numeric"
placeholder="09121234567"
autocomplete="tel"
ltr
required
/>
<AppButton type="submit" :loading="loading">
{{ mode === 'otp' ? 'دریافت کد تأیید' : 'ورود' }}
</AppButton>
</form>
<AppInput
v-if="mode === 'password'"
id="password"
v-model="password"
label="رمز عبور"
type="password"
autocomplete="current-password"
ltr
required
/>
<p v-if="mode === 'password'" class="mt-3 text-center text-xs text-muted">
اولین بار؟ با کد یکبارمصرف وارد شوید و در پروفایل رمز بسازید.
</p>
<AppButton type="submit" :loading="loading">
{{ mode === 'otp' ? 'دریافت کد تأیید' : 'ورود' }}
</AppButton>
</form>
</div>
<p v-if="mode === 'password'" class="text-center text-xs text-muted">
اولین بار؟ با کد یکبارمصرف وارد شوید و در پروفایل رمز بسازید.
</p>
</section>
</div>
</AuthLayout>
</template>
@@ -54,6 +68,7 @@ import AuthModeSwitch from '@/components/auth/AuthModeSwitch.vue';
import type { AuthLoginMode } from '@/components/auth/AuthModeSwitch.vue';
import AppButton from '@/components/ui/AppButton.vue';
import AppInput from '@/components/ui/AppInput.vue';
import AppLogo from '@/components/brand/AppLogo.vue';
import AuthLayout from '@/layouts/AuthLayout.vue';
import { useAuthStore } from '@/stores/auth';
+37 -16
View File
@@ -14,19 +14,14 @@
</p>
<form class="space-y-4" @submit.prevent="onSubmit">
<AppInput
id="code"
<OtpPinInput
ref="pinRef"
v-model="code"
label="کد ۶ رقمی"
type="text"
inputmode="numeric"
maxlength="8"
placeholder="123456"
autocomplete="one-time-code"
ltr
required
label="کد ۴ رقمی"
:disabled="loading"
@complete="onSubmit"
/>
<AppButton type="submit" :loading="loading">ورود</AppButton>
<p v-if="loading" class="text-center text-sm text-muted">در حال تأیید...</p>
</form>
<p v-if="secondsLeft > 0" class="mt-4 text-center text-sm text-muted">
@@ -43,12 +38,12 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { requestOtp, verifyOtp } from '@/api/auth';
import { ApiError } from '@/api/client';
import AppButton from '@/components/ui/AppButton.vue';
import AppInput from '@/components/ui/AppInput.vue';
import OtpPinInput from '@/components/auth/OtpPinInput.vue';
import AuthLayout from '@/layouts/AuthLayout.vue';
import { useAuthStore } from '@/stores/auth';
@@ -59,21 +54,34 @@ const auth = useAuthStore();
const mobile = computed(() => String(route.query.mobile ?? ''));
const devCode = computed(() => route.query.devCode as string | undefined);
const code = ref(devCode.value ?? '');
const pinRef = ref<InstanceType<typeof OtpPinInput> | null>(null);
const code = ref('');
const loading = ref(false);
const resendLoading = ref(false);
const error = ref('');
const secondsLeft = ref(Number(route.query.expires) || 120);
let timer: ReturnType<typeof setInterval> | null = null;
onMounted(() => {
onMounted(async () => {
if (!mobile.value) {
router.replace({ name: 'login' });
return;
}
timer = setInterval(() => {
if (secondsLeft.value > 0) secondsLeft.value -= 1;
}, 1000);
if (devCode.value) {
code.value = devCode.value.replace(/\D/g, '').slice(0, 4);
await nextTick();
if (code.value.length === 4) {
await onSubmit();
}
return;
}
pinRef.value?.focus();
});
onUnmounted(() => {
@@ -81,15 +89,21 @@ onUnmounted(() => {
});
async function onSubmit() {
if (loading.value || code.value.length !== 4) {
return;
}
error.value = '';
loading.value = true;
try {
const res = await verifyOtp(mobile.value, code.value.trim());
const res = await verifyOtp(mobile.value, code.value);
auth.setSession(res.accessToken, res.user);
await router.replace({ name: 'app-home' });
} catch (e) {
error.value =
e instanceof ApiError ? e.message : 'کد نامعتبر است. دوباره تلاش کنید.';
code.value = '';
pinRef.value?.clear();
} finally {
loading.value = false;
}
@@ -101,10 +115,17 @@ async function resend() {
try {
const res = await requestOtp(mobile.value);
secondsLeft.value = res.expiresInSeconds;
code.value = '';
pinRef.value?.clear();
if (res.otpCode) {
await router.replace({
query: { ...route.query, devCode: res.otpCode, expires: String(res.expiresInSeconds) },
});
code.value = res.otpCode.replace(/\D/g, '').slice(0, 4);
await nextTick();
if (code.value.length === 4) {
await onSubmit();
}
}
} catch (e) {
error.value =