560 lines
13 KiB
Vue
560 lines
13 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import FormErrorBanner from '@/components/ui/FormErrorBanner.vue'
|
||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||
import IconHeadset from '@/components/icons/IconHeadset.vue'
|
||
import IconPhone from '@/components/icons/IconPhone.vue'
|
||
import IconQemashMark from '@/components/icons/IconQemashMark.vue'
|
||
import { getPublicContact, telHref } from '@/services/settings'
|
||
import { normalizePhone, toPersianDigits } from '@/utils/format'
|
||
import { validatePhone } from '@/utils/validation'
|
||
import {
|
||
FORM_VALIDATION_SUMMARY,
|
||
clearFieldError,
|
||
hasFieldErrors,
|
||
type FieldErrors,
|
||
} from '@/utils/formErrors'
|
||
import fabricUrl from '@/assets/images/login-fabric.jpg'
|
||
|
||
const router = useRouter()
|
||
const auth = useAuthStore()
|
||
|
||
const supportPhone = ref<string | null>(null)
|
||
const supportHref = computed(() => telHref(supportPhone.value))
|
||
|
||
const phone = ref('')
|
||
const fieldErrors = ref<FieldErrors>({})
|
||
const formError = ref('')
|
||
|
||
const displayPhone = computed(() => (phone.value ? toPersianDigits(phone.value) : ''))
|
||
|
||
function phoneErrorMessage(value: string): string {
|
||
if (!value) return 'شماره موبایل را وارد کنید.'
|
||
if (!validatePhone(value)) return 'شماره موبایل معتبر نیست. (مثال: ۰۹۱۲۱۲۳۴۵۶۷)'
|
||
return ''
|
||
}
|
||
|
||
function clearPhoneError() {
|
||
fieldErrors.value = clearFieldError(fieldErrors.value, 'phone')
|
||
if (!hasFieldErrors(fieldErrors.value)) formError.value = ''
|
||
}
|
||
|
||
function onPhoneInput(value: string) {
|
||
phone.value = normalizePhone(value)
|
||
clearPhoneError()
|
||
}
|
||
|
||
function onPhoneBlur() {
|
||
if (!phone.value) return
|
||
const message = phoneErrorMessage(phone.value)
|
||
if (message) {
|
||
fieldErrors.value = { ...fieldErrors.value, phone: message }
|
||
}
|
||
}
|
||
|
||
async function submit() {
|
||
fieldErrors.value = {}
|
||
formError.value = ''
|
||
|
||
const normalized = normalizePhone(phone.value)
|
||
phone.value = normalized
|
||
|
||
const message = phoneErrorMessage(normalized)
|
||
if (message) {
|
||
fieldErrors.value = { phone: message }
|
||
formError.value = FORM_VALIDATION_SUMMARY
|
||
return
|
||
}
|
||
|
||
const result = await auth.requestOtp(normalized, { isRegister: false })
|
||
if (result.success) {
|
||
router.push({ name: 'otp', query: { phone: normalized, mode: 'login' } })
|
||
} else {
|
||
formError.value = result.message ?? 'خطا در ارسال کد تأیید.'
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
const contact = await getPublicContact()
|
||
supportPhone.value = contact.supportPhone
|
||
} catch {
|
||
supportPhone.value = null
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="page login-page">
|
||
<img class="login-page__bg" :src="fabricUrl" alt="" />
|
||
|
||
<div class="login-page__scroll">
|
||
<div class="login-page__stage">
|
||
<header class="login-top">
|
||
<p class="login-top__welcome">به دنیای پارچه خوش آمدید</p>
|
||
</header>
|
||
|
||
<div class="login-brand">
|
||
<IconQemashMark :size="78" />
|
||
<h1 class="login-brand__fa">نادمی تکس</h1>
|
||
<p class="login-brand__en">NADEMITEX</p>
|
||
<p class="login-brand__tag">انتخاب حرفهایها</p>
|
||
</div>
|
||
|
||
<form class="login-card" @submit.prevent="submit">
|
||
<h2 class="login-card__title">ورود به حساب کاربری</h2>
|
||
<p class="login-card__lead">برای ادامه، شماره موبایل خود را وارد کنید.</p>
|
||
|
||
<FormErrorBanner :message="formError" />
|
||
|
||
<div class="phone-field" dir="ltr" :class="{ 'is-invalid': Boolean(fieldErrors.phone) }">
|
||
<span class="phone-field__locale" aria-hidden="true">
|
||
<svg class="iran-flag" viewBox="0 0 21 15" xmlns="http://www.w3.org/2000/svg">
|
||
<rect width="21" height="5" fill="#239F40" />
|
||
<rect y="5" width="21" height="5" fill="#fff" />
|
||
<rect y="10" width="21" height="5" fill="#DA0001" />
|
||
<circle cx="10.5" cy="7.5" r="1.35" fill="#DA0001" />
|
||
</svg>
|
||
<svg class="phone-field__caret" viewBox="0 0 10 6" fill="none">
|
||
<path
|
||
d="M1 1.2 5 5l4-3.8"
|
||
stroke="currentColor"
|
||
stroke-width="1.3"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
/>
|
||
</svg>
|
||
</span>
|
||
<input
|
||
class="phone-field__input"
|
||
:value="displayPhone"
|
||
type="tel"
|
||
inputmode="numeric"
|
||
autocomplete="tel"
|
||
dir="ltr"
|
||
maxlength="11"
|
||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||
:aria-invalid="Boolean(fieldErrors.phone)"
|
||
aria-label="شماره موبایل"
|
||
@input="onPhoneInput(($event.target as HTMLInputElement).value)"
|
||
@blur="onPhoneBlur"
|
||
/>
|
||
</div>
|
||
<p v-if="fieldErrors.phone" class="phone-field__error">{{ fieldErrors.phone }}</p>
|
||
|
||
<button class="login-submit" type="submit" :disabled="auth.loading">
|
||
<span v-if="auth.loading" class="login-submit__spinner" aria-hidden="true" />
|
||
<span class="login-submit__chevron" aria-hidden="true">
|
||
<IconChevronLeft :size="18" color="#F4EDE4" />
|
||
</span>
|
||
<span>دریافت کد تأیید</span>
|
||
</button>
|
||
|
||
<p class="login-register">
|
||
حساب کاربری ندارید؟
|
||
<RouterLink to="/register">ثبتنام کنید</RouterLink>
|
||
</p>
|
||
|
||
<div class="login-or" role="separator">
|
||
<span>یا</span>
|
||
</div>
|
||
|
||
<div class="login-help">
|
||
<a
|
||
class="login-help__item"
|
||
:href="supportHref"
|
||
:class="{ 'is-disabled': !supportHref }"
|
||
:aria-disabled="!supportHref"
|
||
>
|
||
<span class="login-help__icon">
|
||
<IconHeadset :size="22" color="#3A3936" />
|
||
</span>
|
||
پشتیبانی
|
||
</a>
|
||
<a
|
||
class="login-help__item"
|
||
:href="supportHref"
|
||
:class="{ 'is-disabled': !supportHref }"
|
||
:aria-disabled="!supportHref"
|
||
>
|
||
<span class="login-help__icon">
|
||
<IconPhone :size="22" color="#3A3936" />
|
||
</span>
|
||
تماس با ما
|
||
</a>
|
||
</div>
|
||
|
||
<p class="login-card__foot">پارچه، آغاز یک اعتماد ماندگار…</p>
|
||
</form>
|
||
|
||
<div class="login-spacer" aria-hidden="true" />
|
||
<p class="login-watermark">MORE<br />THAN<br />FABRIC</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.login-page {
|
||
position: relative;
|
||
isolation: isolate;
|
||
height: 100%;
|
||
min-height: 100%;
|
||
color: #f4ede4;
|
||
background: #1a1612;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.login-page__bg {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
object-position: center center;
|
||
pointer-events: none;
|
||
user-select: none;
|
||
}
|
||
|
||
.login-page__scroll {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: 100%;
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
}
|
||
|
||
.login-page__stage {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100%;
|
||
width: 100%;
|
||
max-width: 360px;
|
||
margin-inline: auto;
|
||
padding: calc(10px + env(safe-area-inset-top, 0px)) 16px
|
||
calc(14px + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
|
||
.login-top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
min-height: 36px;
|
||
}
|
||
|
||
.login-top__welcome {
|
||
color: #f3e6d6;
|
||
font-size: 0.75rem;
|
||
font-weight: 400;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.login-brand {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
margin-top: 28px;
|
||
margin-bottom: 20px;
|
||
color: #f3e6d6;
|
||
}
|
||
|
||
.login-brand__fa {
|
||
margin: 14px 0 0;
|
||
font-size: 1.75rem;
|
||
font-weight: 700;
|
||
line-height: 1.25;
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.login-brand__en {
|
||
margin: 6px 0 0;
|
||
font-size: 0.6875rem;
|
||
font-weight: 500;
|
||
letter-spacing: 0.28em;
|
||
color: #ead7c0;
|
||
}
|
||
|
||
.login-brand__tag {
|
||
margin: 10px 0 0;
|
||
font-size: 0.875rem;
|
||
font-weight: 400;
|
||
color: #efe2d2;
|
||
}
|
||
|
||
.login-card {
|
||
width: 100%;
|
||
margin: 0;
|
||
padding: 22px 18px 16px;
|
||
border: none;
|
||
border-radius: 28px;
|
||
background: #f4f1ed;
|
||
box-shadow: 0 18px 40px rgba(12, 10, 8, 0.22);
|
||
color: #2a2622;
|
||
}
|
||
|
||
.login-card__title {
|
||
margin: 0;
|
||
text-align: center;
|
||
font-size: 1.0625rem;
|
||
font-weight: 700;
|
||
line-height: 1.5;
|
||
color: #1c1b19;
|
||
}
|
||
|
||
.login-card__lead {
|
||
margin: 6px 0 0;
|
||
text-align: center;
|
||
font-size: 0.75rem;
|
||
line-height: 1.7;
|
||
color: #8a847c;
|
||
}
|
||
|
||
.login-card :deep(.form-error-banner) {
|
||
margin-top: 14px;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.phone-field {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 8px;
|
||
height: 48px;
|
||
margin-top: 18px;
|
||
padding: 0 8px 0 14px;
|
||
border: 1px solid #e4e0db;
|
||
border-radius: 999px;
|
||
background: #f7f5f1;
|
||
}
|
||
|
||
.phone-field.is-invalid {
|
||
border-color: color-mix(in srgb, var(--color-danger) 55%, #e4e0db);
|
||
}
|
||
|
||
.phone-field__locale {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
color: #9a9590;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.iran-flag {
|
||
width: 22px;
|
||
height: 16px;
|
||
border-radius: 2px;
|
||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.phone-field__caret {
|
||
width: 10px;
|
||
height: 6px;
|
||
}
|
||
|
||
.phone-field__input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 100%;
|
||
border: none;
|
||
background: transparent;
|
||
color: #2a2622;
|
||
font-family: inherit;
|
||
font-size: 0.9375rem;
|
||
letter-spacing: 0.04em;
|
||
text-align: start;
|
||
outline: none;
|
||
}
|
||
|
||
.phone-field__input::placeholder {
|
||
color: #b0aaa3;
|
||
}
|
||
|
||
.phone-field__error {
|
||
margin: 8px 4px 0;
|
||
color: var(--color-danger);
|
||
font-size: 0.75rem;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.login-submit {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 48px;
|
||
margin-top: 12px;
|
||
padding: 0 44px;
|
||
border: none;
|
||
border-radius: 999px;
|
||
background: var(--color-primary);
|
||
color: #f7f4ef;
|
||
font-family: inherit;
|
||
font-size: 0.9375rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: background 0.15s ease;
|
||
}
|
||
|
||
.login-submit:not(:disabled):hover,
|
||
.login-submit:not(:disabled):focus-visible {
|
||
background: var(--color-primary-hover);
|
||
}
|
||
|
||
.login-submit:focus-visible {
|
||
outline: 2px solid #c9a56a;
|
||
outline-offset: 2px;
|
||
}
|
||
|
||
.login-submit:disabled {
|
||
opacity: 0.7;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.login-submit__chevron {
|
||
position: absolute;
|
||
inset-inline-end: 16px;
|
||
display: grid;
|
||
place-items: center;
|
||
}
|
||
|
||
.login-submit__spinner {
|
||
position: absolute;
|
||
inset-inline-start: 16px;
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid rgba(255, 255, 255, 0.28);
|
||
border-top-color: #fff;
|
||
border-radius: 50%;
|
||
animation: login-spin 0.6s linear infinite;
|
||
}
|
||
|
||
.login-register {
|
||
margin: 12px 0 0;
|
||
text-align: center;
|
||
font-size: 0.8125rem;
|
||
line-height: 1.6;
|
||
color: #8a847c;
|
||
}
|
||
|
||
.login-register a {
|
||
font-weight: 700;
|
||
color: var(--color-primary);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.login-or {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin: 16px 4px 14px;
|
||
color: #9a9590;
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.login-or::before,
|
||
.login-or::after {
|
||
content: '';
|
||
flex: 1;
|
||
height: 1px;
|
||
background: #e6e1db;
|
||
}
|
||
|
||
.login-help {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 48px;
|
||
}
|
||
|
||
.login-help__item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: #6f6b66;
|
||
font-size: 0.75rem;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.login-help__item.is-disabled {
|
||
pointer-events: none;
|
||
}
|
||
|
||
.login-help__icon {
|
||
width: 52px;
|
||
height: 52px;
|
||
border: 1px solid #d8d3cd;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
background: transparent;
|
||
}
|
||
|
||
.login-card__foot {
|
||
margin: 18px 0 2px;
|
||
text-align: center;
|
||
font-size: 0.75rem;
|
||
line-height: 1.7;
|
||
color: #8a847c;
|
||
}
|
||
|
||
.login-spacer {
|
||
flex: 1 1 auto;
|
||
min-height: 16px;
|
||
}
|
||
|
||
.login-watermark {
|
||
margin: 0;
|
||
padding-bottom: 8px;
|
||
color: rgba(244, 237, 228, 0.58);
|
||
font-size: 0.625rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.2em;
|
||
line-height: 1.35;
|
||
text-align: left;
|
||
direction: ltr;
|
||
}
|
||
|
||
@keyframes login-spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 359px) {
|
||
.login-page__stage {
|
||
padding-inline: 12px;
|
||
}
|
||
|
||
.login-brand {
|
||
margin-top: 16px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.login-brand__fa {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.login-help {
|
||
gap: 36px;
|
||
}
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
.login-page {
|
||
padding: 0;
|
||
max-width: none;
|
||
margin: 0;
|
||
height: 100%;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
</style>
|