refactor(ui): replace modal dialogs with toast/sheet components
Remove DialogVant/NotifyVant and add Toast and Sheet components with useToast, useReveal, and usePageTransition composables. Rework Navigation, RegisterView, and styles across both apps; rebuild dist assets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
148b99c4a6
commit
c05352d6fd
@@ -61,30 +61,25 @@
|
||||
</container>
|
||||
</Spinner>
|
||||
|
||||
<DialogVantOneButton
|
||||
<IslandSheet
|
||||
v-model:show="done"
|
||||
:time-out="-1"
|
||||
art="success"
|
||||
title="تکمیل اطلاعات شما با موفقیت انجام شد."
|
||||
description="مدارک شما با موفقیت بارگذاری شد."
|
||||
confirm-text="متوجه شدم"
|
||||
:close-on-click-overlay="false"
|
||||
>
|
||||
<template #title>
|
||||
<div class="cpf__dialogText">
|
||||
تکمیل اطلاعات شما با موفقیت انجام شد.
|
||||
</div>
|
||||
</template>
|
||||
<template #button>
|
||||
<div class="cpf__dialogBtn" @click="finish">متوجه شدم</div>
|
||||
</template>
|
||||
</DialogVantOneButton>
|
||||
@confirm="finish"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showNotify } from 'vant'
|
||||
import { toast } from '../../composables/useToast'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
import DialogVantOneButton from '../../components/DialogVantOneButton.vue'
|
||||
import IslandSheet from '../../components/ui/IslandSheet.vue'
|
||||
import SingleImageUploader from '../../components/upload/SingleImageUploader.vue'
|
||||
import { api } from '../../api/client'
|
||||
|
||||
@@ -106,7 +101,7 @@ async function submit() {
|
||||
// The reference checked all three client-side before posting
|
||||
// (completeProfile.vue:191-198); the server requires them too.
|
||||
if (!form.business_license_image_id || !form.shop_image_id || !form.ncode_image_id) {
|
||||
showNotify('لطفا تمامی عکس های خواسته شده را آپلود کنید')
|
||||
toast.warning('لطفا تمامی عکس های خواسته شده را آپلود کنید')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -184,16 +179,4 @@ function finish() {
|
||||
opacity: 0.6;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.cpf__dialogText {
|
||||
padding: 20px;
|
||||
line-height: 1.9;
|
||||
text-align: justify;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cpf__dialogBtn {
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -205,32 +205,25 @@
|
||||
</container>
|
||||
</PullRefresh>
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="data.notify" :closeOnClickOverlay="true">
|
||||
<template v-slot:img>
|
||||
<img :src="exitImage" alt="" class="opacity-70 mb-8" />
|
||||
</template>
|
||||
<template v-slot:title>آیا می خواهید از برنامه خارج شوید؟</template>
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-[#1C2D56]" @click="logOut">بله</div>
|
||||
<div class="text-center text-[#6b7280]" @click="data.notify = false">بی خیال</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
<IslandSheet
|
||||
v-model:show="data.notify"
|
||||
art="logout"
|
||||
title="آیا می خواهید از برنامه خارج شوید؟"
|
||||
description="برای استفاده دوباره از امکانات منتزر، باید دوباره وارد حساب کاربری خود شوید."
|
||||
confirm-text="بله"
|
||||
cancel-text="بی خیال"
|
||||
@confirm="logOut"
|
||||
/>
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="data.loginNotify" :closeOnClickOverlay="true">
|
||||
<template v-slot:img>
|
||||
<img :src="loginDialogImage" alt="" class="opacity-80" />
|
||||
</template>
|
||||
<template v-slot:title>
|
||||
<div class="p-8 text-justify">
|
||||
برای دسترسی به تمامی امکانات نرم افزار منتزر، ابتدا گزینه ورود/ثبت نام را لمس کنید و در
|
||||
صورت نداشتن حساب کاربری، ثبت نام نمایید.
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-[#1C2D56]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
|
||||
<div class="text-center text-danger" @click="data.loginNotify = false">بیخیال</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
<IslandSheet
|
||||
v-model:show="data.loginNotify"
|
||||
art="login"
|
||||
title="ابتدا وارد حساب کاربری شوید"
|
||||
description="برای دسترسی به تمامی امکانات نرم افزار منتزر، گزینه ورود/ثبت نام را لمس کنید و در صورت نداشتن حساب کاربری، ثبت نام نمایید."
|
||||
confirm-text="ورود/ثبت نام"
|
||||
cancel-text="بیخیال"
|
||||
@confirm="router.push({ name: 'login' })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -240,9 +233,7 @@ import { useRouter } from 'vue-router'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import RateStrip from '../../components/home/RateStrip.vue'
|
||||
import DialogVant from '../../components/DialogVant.vue'
|
||||
import exitImage from '../../assets/img/empty/dialogEmpty5.svg'
|
||||
import loginDialogImage from '../../assets/img/empty/dialogEmpty1.svg'
|
||||
import IslandSheet from '../../components/ui/IslandSheet.vue'
|
||||
import { price } from '../../utils/filters'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
import { api, type CustomerDetail, type GoldPriceQuote } from '../../api/client'
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="auth">
|
||||
<NotifyVant :timeOut="2000" v-model="notify.show" :text="notify.text" :color="'background-color: #DC2626'" />
|
||||
|
||||
<PageBar title="ورود" back-to="home" />
|
||||
|
||||
<container class="auth__body">
|
||||
@@ -88,7 +86,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { Form, FormItem, Input } from 'ant-design-vue'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import NotifyVant from '../../components/NotifyVant.vue'
|
||||
import { toast } from '../../composables/useToast'
|
||||
import OtpInput from '../../components/input/OtpInput.vue'
|
||||
import { required, mobile as mobileRule } from '../../utils/validator'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
@@ -100,7 +98,6 @@ const route = useRoute()
|
||||
const session = useSessionStore()
|
||||
|
||||
const formModel = reactive({ username: undefined as string | undefined })
|
||||
const notify = reactive({ show: false, text: 'اطلاعات کاربری در سیستم وجود ندارد.' })
|
||||
const body = reactive({
|
||||
step: 1,
|
||||
loading: false,
|
||||
@@ -146,8 +143,7 @@ const sendPhone = async () => {
|
||||
// 429 = the send throttle (too soon, or too many in the window). Its message
|
||||
// is already Persian and user-facing, so show it rather than a generic one.
|
||||
const text = e instanceof Error && /\(429\)/.test(e.message) ? extractServerMessage(e) : null
|
||||
notify.text = text ?? 'خطا در ارسال کد. لطفاً دوباره تلاش کنید.'
|
||||
notify.show = true
|
||||
toast.error(text ?? 'خطا در ارسال کد. لطفاً دوباره تلاش کنید.')
|
||||
} finally {
|
||||
body.loading = false
|
||||
}
|
||||
@@ -177,14 +173,13 @@ const sendToken = async (otpCode: string) => {
|
||||
}
|
||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : undefined
|
||||
await router.push(redirect ?? { name: 'home' })
|
||||
notify.text = 'با موفقیت وارد شدید'
|
||||
notify.show = true
|
||||
toast.success('با موفقیت وارد شدید')
|
||||
} catch (e: unknown) {
|
||||
notify.text =
|
||||
toast.error(
|
||||
e instanceof Error && /\(429\)/.test(e.message)
|
||||
? (extractServerMessage(e) ?? 'تعداد تلاشها بیش از حد مجاز است.')
|
||||
: 'کد وارد شده صحیح نیست'
|
||||
notify.show = true
|
||||
: 'کد وارد شده صحیح نیست',
|
||||
)
|
||||
} finally {
|
||||
body.loading = false
|
||||
}
|
||||
|
||||
@@ -1,89 +1,188 @@
|
||||
<!--
|
||||
Ported from legacy henza_pwa/src/views/user/LoginRegister/components/Register.vue,
|
||||
restyled into the redesigned design language (see components/home/* and
|
||||
HomeView).
|
||||
Store sign-up, redesigned as a two-step form:
|
||||
1. اطلاعات شما — first/last name, mobile
|
||||
2. اطلاعات فروشگاه — store name, store phone, city, address
|
||||
followed by an in-page "request received" state.
|
||||
|
||||
DROPPED as confirmed-dead code (verified unreachable, not merely "possibly
|
||||
dead"): legacy rendered a whole cascading country->province->city picker
|
||||
(WorkActionSheet + selectCityArrays + getCities() against POST /cities/list)
|
||||
alongside a plain "شهر" text <Input>. The only element that ever opened the
|
||||
picker (`#citybtn`) was itself commented out in legacy's own template, so the
|
||||
picker was unreachable UI even in the original app; the visible form uses the
|
||||
plain text input. That whole complex, and the parallel unused
|
||||
`userFormModel`/`defaultUserFormModel` objects, are not ported.
|
||||
Why two steps: seven fields in one column read as a wall on a phone. Split
|
||||
by who they describe, each step fits one screen, the progress bar sets the
|
||||
expectation, and the CTA sits in a sticky footer so it is always in reach.
|
||||
|
||||
STUBBED, not real, and unchanged from the previous version: legacy's
|
||||
POST /major/register — apps/api has no create-customer endpoint. Submitting
|
||||
shows legacy's exact success message without persisting anything. The copy
|
||||
says the account awaits review, which is what legacy said too, so the screen
|
||||
does not claim an account exists when none was created.
|
||||
Ant's Form only validates the FormItems currently mounted, so a submit on
|
||||
step 1 validates just step 1's fields; `onFinish` then advances instead of
|
||||
registering. Enter on the keyboard therefore behaves like the CTA.
|
||||
|
||||
Numeric fields are `type="tel"` + `inputmode="numeric"` (a real number input
|
||||
drops the leading 0 and shows spinners), and Persian/Arabic digits are
|
||||
converted to Latin as the user types, so the validators accept them.
|
||||
|
||||
STUBBED, unchanged from before: legacy's POST /major/register has no
|
||||
counterpart in apps/api yet. Submitting shows the "awaiting review" state
|
||||
without persisting anything — the copy says the request is under review,
|
||||
which is what legacy said too, so it does not claim an account exists.
|
||||
-->
|
||||
<template>
|
||||
<div class="auth">
|
||||
<PageBar title="ثبت نام" back-to="home" />
|
||||
<div class="reg">
|
||||
<PageBar title="ثبت نام" :back-action="goBack" />
|
||||
|
||||
<Spinner :spinning="profileData.loading" tip="در حال ثبت اطلاعات">
|
||||
<container class="auth__body">
|
||||
<section class="auth__intro">
|
||||
<div class="auth__mark" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:user-add" size="28" />
|
||||
</div>
|
||||
<h2 class="auth__title">ساخت حساب کاربری</h2>
|
||||
<p class="auth__sub">اطلاعات فروشگاه خود را وارد کنید</p>
|
||||
</section>
|
||||
|
||||
<section class="auth__card">
|
||||
<Form :model="formModel" layout="vertical" @finish="register" class="auth__form">
|
||||
<FormItem name="fname" label="نام" :rules="required('نام')">
|
||||
<Input v-model:value="formModel.fname" />
|
||||
</FormItem>
|
||||
<FormItem name="lname" label="نام خانوادگی" :rules="required('نام خانوادگی')">
|
||||
<Input v-model:value="formModel.lname" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="mobile"
|
||||
label="شماره موبایل"
|
||||
:rules="[mobileRule('شماره موبایل'), required('شماره موبایل')]"
|
||||
>
|
||||
<Input v-model:value="formModel.mobile" type="number" />
|
||||
</FormItem>
|
||||
<FormItem name="store_name" label="نام فروشگاه" :rules="required('نام فروشگاه')">
|
||||
<Input v-model:value="formModel.store_name" />
|
||||
</FormItem>
|
||||
<FormItem name="phone" label="تلفن فروشگاه" :rules="[required('تلفن فروشگاه')]">
|
||||
<Input v-model:value="formModel.phone" type="number" />
|
||||
</FormItem>
|
||||
<FormItem name="address" label="آدرس پستی" :rules="required('آدرس')">
|
||||
<Textarea v-model:value="formModel.address" :rows="3" />
|
||||
</FormItem>
|
||||
<FormItem name="city" label="شهر" :rules="required('شهر')">
|
||||
<Input v-model:value="formModel.city" />
|
||||
</FormItem>
|
||||
|
||||
<button type="submit" class="auth__cta">ثبت اطلاعات</button>
|
||||
</Form>
|
||||
</section>
|
||||
|
||||
<p class="auth__switch">
|
||||
اگر حساب کاربری دارید
|
||||
<button type="button" @click="emit('register', false)">وارد شوید</button>
|
||||
</p>
|
||||
</container>
|
||||
</Spinner>
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="registeredInfo" :closeOnClickOverlay="false">
|
||||
<template v-slot:img>
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80" />
|
||||
</template>
|
||||
<template v-slot:title>
|
||||
<div class="p-5">
|
||||
ثبت نام شما با موفقیت انجام شد. منتظر تایید اطلاعات ثبت نامی شما توسط کارشناسان بمانید.
|
||||
<!-- Success -->
|
||||
<Transition name="reg-fade" mode="out-in">
|
||||
<container v-if="done" key="done" class="reg__body reg__done">
|
||||
<div class="reg__doneMark" aria-hidden="true">
|
||||
<span class="reg__doneRing" />
|
||||
<Icon icon="vuesax-linear:tick-circle" size="40" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:button>
|
||||
<div class="text-center" @click="router.push({ name: 'home' })">متوجه شدم</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
<h2 class="reg__doneTitle">درخواست شما ثبت شد</h2>
|
||||
<p class="reg__doneText">
|
||||
اطلاعات فروشگاه {{ formModel.store_name }} برای کارشناسان ما ارسال شد. پس از بررسی و تایید، میتوانید با
|
||||
شماره <span class="reg__ltr">{{ formModel.mobile }}</span> وارد شوید.
|
||||
</p>
|
||||
<div class="reg__doneActions">
|
||||
<button type="button" class="reg__cta" @click="router.push({ name: 'home' })">بازگشت به صفحه اصلی</button>
|
||||
<button type="button" class="reg__ghost" @click="emit('register', false)">رفتن به صفحه ورود</button>
|
||||
</div>
|
||||
</container>
|
||||
|
||||
<!-- Form -->
|
||||
<container v-else key="form" class="reg__body">
|
||||
<header class="reg__head">
|
||||
<div class="reg__steps" role="progressbar" :aria-valuenow="step" aria-valuemin="1" aria-valuemax="2">
|
||||
<span class="reg__seg is-on" />
|
||||
<span class="reg__seg" :class="{ 'is-on': step === 2 }" />
|
||||
</div>
|
||||
<p class="reg__stepLabel">مرحله {{ step === 1 ? '۱' : '۲' }} از ۲</p>
|
||||
<h2 class="reg__title">{{ step === 1 ? 'اطلاعات شما' : 'اطلاعات فروشگاه' }}</h2>
|
||||
<p class="reg__sub">
|
||||
{{
|
||||
step === 1
|
||||
? 'برای ساخت حساب فروشگاه، ابتدا خودتان را معرفی کنید.'
|
||||
: 'این اطلاعات برای ارسال سفارش و تایید حساب استفاده میشود.'
|
||||
}}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<Form ref="formRef" :model="formModel" layout="vertical" class="reg__form" @finish="onFinish">
|
||||
<Transition :name="step === 2 ? 'reg-next' : 'reg-prev'" mode="out-in">
|
||||
<!-- Step 1 -->
|
||||
<div v-if="step === 1" key="s1" class="reg__fields">
|
||||
<div class="reg__row">
|
||||
<FormItem name="fname" label="نام" :rules="[need('نام خود')]">
|
||||
<Input
|
||||
v-model:value="formModel.fname"
|
||||
placeholder="مثلاً علی"
|
||||
autocomplete="given-name"
|
||||
enterkeyhint="next"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="lname" label="نام خانوادگی" :rules="[need('نام خانوادگی')]">
|
||||
<Input
|
||||
v-model:value="formModel.lname"
|
||||
placeholder="مثلاً محمدی"
|
||||
autocomplete="family-name"
|
||||
enterkeyhint="next"
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<FormItem
|
||||
name="mobile"
|
||||
label="شماره موبایل"
|
||||
:rules="[need('شماره موبایل'), { pattern: /^09\d{9}$/, message: 'شماره موبایل باید ۱۱ رقم و با ۰۹ شروع شود' }]"
|
||||
extra="کد ورود به این شماره پیامک میشود."
|
||||
>
|
||||
<Input
|
||||
:value="formModel.mobile"
|
||||
class="reg__num"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
:maxlength="11"
|
||||
placeholder="09123456789"
|
||||
autocomplete="tel"
|
||||
enterkeyhint="next"
|
||||
@update:value="(v: string) => (formModel.mobile = digits(v))"
|
||||
>
|
||||
<template #prefix><Icon icon="vuesax-linear:mobile" size="18" /></template>
|
||||
</Input>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div v-else key="s2" class="reg__fields">
|
||||
<FormItem name="store_name" label="نام فروشگاه" :rules="[need('نام فروشگاه')]">
|
||||
<Input
|
||||
v-model:value="formModel.store_name"
|
||||
placeholder="مثلاً گالری طلای آفتاب"
|
||||
autocomplete="organization"
|
||||
enterkeyhint="next"
|
||||
>
|
||||
<template #prefix><Icon icon="vuesax-linear:shop" size="18" /></template>
|
||||
</Input>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="phone"
|
||||
label="تلفن فروشگاه"
|
||||
:rules="[need('تلفن فروشگاه'), { pattern: /^(0\d{10}|\d{8})$/, message: 'تلفن را با کد شهر وارد کنید، مثلاً 02112345678' }]"
|
||||
>
|
||||
<Input
|
||||
:value="formModel.phone"
|
||||
class="reg__num"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
:maxlength="11"
|
||||
placeholder="02112345678"
|
||||
autocomplete="tel-national"
|
||||
enterkeyhint="next"
|
||||
@update:value="(v: string) => (formModel.phone = digits(v))"
|
||||
>
|
||||
<template #prefix><Icon icon="vuesax-linear:call" size="18" /></template>
|
||||
</Input>
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="city" label="شهر" :rules="[need('شهر')]">
|
||||
<Input
|
||||
v-model:value="formModel.city"
|
||||
placeholder="مثلاً تهران"
|
||||
autocomplete="address-level2"
|
||||
enterkeyhint="next"
|
||||
>
|
||||
<template #prefix><Icon icon="vuesax-linear:buildings" size="18" /></template>
|
||||
</Input>
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="address" label="آدرس پستی" :rules="[need('آدرس فروشگاه')]">
|
||||
<Textarea
|
||||
v-model:value="formModel.address"
|
||||
:rows="3"
|
||||
placeholder="خیابان، کوچه، پلاک، واحد"
|
||||
autocomplete="street-address"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<div class="reg__note">
|
||||
<Icon icon="vuesax-linear:shield-tick" size="18" />
|
||||
<span>حساب شما پس از بررسی اطلاعات توسط کارشناسان فعال میشود.</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Sticky action bar; inside the Form so it submits it. -->
|
||||
<footer class="reg__footer">
|
||||
<div class="reg__footerInner">
|
||||
<button type="submit" class="reg__cta" :class="{ 'is-loading': loading }" :disabled="loading">
|
||||
<span v-if="loading" class="reg__spinner" aria-hidden="true" />
|
||||
<span>{{ step === 1 ? 'ادامه' : loading ? 'در حال ثبت…' : 'ثبت درخواست' }}</span>
|
||||
<Icon v-if="step === 1" icon="vuesax-linear:arrow-left" size="18" />
|
||||
</button>
|
||||
<p v-if="step === 1" class="reg__switch">
|
||||
حساب دارید؟
|
||||
<button type="button" @click="emit('register', false)">وارد شوید</button>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</Form>
|
||||
</container>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -91,18 +190,16 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Form, FormItem, Input, Textarea } from 'ant-design-vue'
|
||||
import registerDelayImage from '../../assets/img/registerDelay.svg'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
import DialogVant from '../../components/DialogVant.vue'
|
||||
import { required, mobile as mobileRule } from '../../utils/validator'
|
||||
|
||||
const emit = defineEmits<{ (e: 'register', value: boolean): void }>()
|
||||
const router = useRouter()
|
||||
|
||||
const profileData = reactive({ loading: false })
|
||||
const registeredInfo = ref(false)
|
||||
const formRef = ref()
|
||||
const step = ref<1 | 2>(1)
|
||||
const loading = ref(false)
|
||||
const done = ref(false)
|
||||
|
||||
const formModel = reactive({
|
||||
fname: undefined as string | undefined,
|
||||
@@ -110,109 +207,532 @@ const formModel = reactive({
|
||||
mobile: undefined as string | undefined,
|
||||
store_name: undefined as string | undefined,
|
||||
phone: undefined as string | undefined,
|
||||
address: undefined as string | undefined,
|
||||
city: undefined as string | undefined,
|
||||
address: undefined as string | undefined,
|
||||
})
|
||||
|
||||
const register = async () => {
|
||||
const need = (what: string) => ({ required: true, whitespace: true, message: `لطفاً ${what} را وارد کنید` })
|
||||
|
||||
// Persian (۰-۹) and Arabic (٠-٩) digits → Latin, everything else dropped.
|
||||
function digits(v: string): string {
|
||||
return (v ?? '')
|
||||
.replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x06f0))
|
||||
.replace(/[٠-٩]/g, (d) => String(d.charCodeAt(0) - 0x0660))
|
||||
.replace(/\D/g, '')
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (done.value) router.push({ name: 'home' })
|
||||
else if (step.value === 2) step.value = 1
|
||||
else emit('register', false)
|
||||
}
|
||||
|
||||
async function onFinish() {
|
||||
if (step.value === 1) {
|
||||
step.value = 2
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
return
|
||||
}
|
||||
try {
|
||||
profileData.loading = true
|
||||
loading.value = true
|
||||
// Stubbed — no create-customer endpoint exists in apps/api yet.
|
||||
registeredInfo.value = true
|
||||
await new Promise((r) => setTimeout(r, 600))
|
||||
done.value = true
|
||||
window.scrollTo({ top: 0 })
|
||||
} finally {
|
||||
profileData.loading = false
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.auth {
|
||||
background: @canvas;
|
||||
.reg {
|
||||
min-height: 100vh;
|
||||
background: @canvas;
|
||||
}
|
||||
|
||||
.auth__body {
|
||||
padding-top: 86px;
|
||||
padding-bottom: 90px;
|
||||
.reg__body {
|
||||
padding-top: 78px;
|
||||
// Room for the sticky footer.
|
||||
padding-bottom: calc(150px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.auth__intro {
|
||||
/* ---------- Header + progress ---------- */
|
||||
|
||||
.reg__head {
|
||||
padding: 6px 2px 22px;
|
||||
}
|
||||
|
||||
.reg__steps {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.reg__seg {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
background: @hairline;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: @wine;
|
||||
transform: scaleX(0);
|
||||
transform-origin: right center;
|
||||
transition: transform 0.5s cubic-bezier(0.65, 0, 0.35, 1);
|
||||
}
|
||||
|
||||
&.is-on::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
.reg__stepLabel {
|
||||
margin: 14px 0 0;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: @wine;
|
||||
}
|
||||
|
||||
.reg__title {
|
||||
margin: 4px 0 0;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.reg__sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.8;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
/* ---------- Fields ---------- */
|
||||
|
||||
// minmax(0, 1fr): a plain 1fr column won't shrink below its content's
|
||||
// min-width, which pushed the second name field off the screen.
|
||||
.reg__row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.reg__form {
|
||||
// Force label-above-input. Ant's FormItem is a wrapping flex row, so in a
|
||||
// narrow cell its label and control ended up side by side.
|
||||
:deep(.ant-form-item) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label),
|
||||
:deep(.ant-form-item-control) {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label) {
|
||||
padding: 0 0 6px;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-label > label) {
|
||||
height: auto;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: @inkSoft;
|
||||
|
||||
// Ant's required asterisk — every field here is required, so it's noise.
|
||||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
border-radius: 14px;
|
||||
border: 1.5px solid @hairline;
|
||||
background: @canvasSoft;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
color: @ink;
|
||||
box-shadow: none;
|
||||
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.ant-input) {
|
||||
min-height: 50px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
min-height: 50px;
|
||||
padding: 0 14px;
|
||||
|
||||
.ant-input {
|
||||
min-height: 46px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(textarea.ant-input) {
|
||||
min-height: 96px;
|
||||
line-height: 1.8;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
:deep(.ant-input-prefix) {
|
||||
margin-inline-end: 10px;
|
||||
color: @inkFaint;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.ant-input::placeholder) {
|
||||
color: #b4b8c0;
|
||||
}
|
||||
|
||||
:deep(.ant-input:hover),
|
||||
:deep(.ant-input-affix-wrapper:hover) {
|
||||
border-color: darken(@hairline, 8%);
|
||||
}
|
||||
|
||||
:deep(.ant-input:focus),
|
||||
:deep(.ant-input-focused),
|
||||
:deep(.ant-input-affix-wrapper-focused) {
|
||||
border-color: @wine;
|
||||
background: @canvas;
|
||||
box-shadow: 0 0 0 4px fade(@wine, 10%);
|
||||
|
||||
.ant-input-prefix {
|
||||
color: @wine;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-input-affix-wrapper-focused .ant-input-prefix) {
|
||||
color: @wine;
|
||||
}
|
||||
|
||||
// Errors
|
||||
:deep(.ant-form-item-has-error .ant-input),
|
||||
:deep(.ant-form-item-has-error .ant-input-affix-wrapper) {
|
||||
border-color: @red !important;
|
||||
background: fade(@red, 3%);
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-has-error .ant-input:focus),
|
||||
:deep(.ant-form-item-has-error .ant-input-affix-wrapper-focused) {
|
||||
box-shadow: 0 0 0 4px fade(@red, 10%) !important;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-has-error .ant-input-affix-wrapper .ant-input) {
|
||||
background: transparent;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-explain),
|
||||
:deep(.ant-form-item-extra) {
|
||||
min-height: 0;
|
||||
margin-top: 6px;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item-extra) {
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
// Ant's own feedback icons duplicate the red border.
|
||||
:deep(.ant-form-item-children-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Numbers are typed left-to-right but stay aligned with the RTL labels.
|
||||
.reg__num :deep(input) {
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.reg__note {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: @wineWash;
|
||||
color: @wine;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.8;
|
||||
|
||||
:deep(svg) {
|
||||
flex: 0 0 auto;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Sticky footer ---------- */
|
||||
|
||||
.reg__footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 15;
|
||||
padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
||||
background: fade(@canvas, 86%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
border-top: 1px solid @hairline;
|
||||
}
|
||||
|
||||
.reg__footerInner {
|
||||
max-width: 736px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.reg__cta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 52px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 16px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
box-shadow: 0 10px 22px -12px fade(@ink, 70%);
|
||||
transition: transform 0.15s ease, opacity 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
&.is-loading,
|
||||
&:disabled {
|
||||
opacity: 0.75;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:active :deep(svg) {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
.reg__spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid fade(#fff, 35%);
|
||||
border-top-color: #fff;
|
||||
animation: reg-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
.reg__ghost {
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
border: 1.5px solid @hairline;
|
||||
border-radius: 16px;
|
||||
background: @canvas;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.reg__switch {
|
||||
margin: 10px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: @wine;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Success ---------- */
|
||||
|
||||
.reg__done {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding-bottom: 24px;
|
||||
padding-top: 120px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.auth__mark {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
.reg__doneMark {
|
||||
position: relative;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
border-radius: 26px;
|
||||
color: @green;
|
||||
background: fade(@green, 10%);
|
||||
animation: reg-pop 0.6s cubic-bezier(0.2, 1.5, 0.4, 1) both;
|
||||
|
||||
:deep(svg) {
|
||||
animation: reg-pop 0.6s 0.15s cubic-bezier(0.2, 1.6, 0.4, 1) both;
|
||||
}
|
||||
}
|
||||
|
||||
.auth__title {
|
||||
margin: 14px 0 0;
|
||||
font-size: 16px;
|
||||
.reg__doneRing {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
border: 2px solid @green;
|
||||
opacity: 0;
|
||||
animation: reg-ring 1.1s 0.3s ease-out both;
|
||||
}
|
||||
|
||||
.reg__doneTitle {
|
||||
margin: 22px 0 0;
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.auth__sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12.5px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.auth__card {
|
||||
padding: 18px 16px 20px;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 20px;
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item-label > label) {
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__cta {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
padding: 13px 18px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
font-family: inherit;
|
||||
.reg__doneText {
|
||||
max-width: 320px;
|
||||
margin: 10px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.auth__cta:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.auth__switch {
|
||||
margin: 20px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 2;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
.auth__switch button {
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
.reg__ltr {
|
||||
direction: ltr;
|
||||
unicode-bidi: isolate;
|
||||
font-weight: 700;
|
||||
color: @wine;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.reg__doneActions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* ---------- Motion ---------- */
|
||||
|
||||
.reg-next-enter-active,
|
||||
.reg-next-leave-active,
|
||||
.reg-prev-enter-active,
|
||||
.reg-prev-leave-active {
|
||||
transition: transform 0.28s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.22s ease;
|
||||
}
|
||||
|
||||
// RTL: moving forward slides content toward the left.
|
||||
.reg-next-enter-from,
|
||||
.reg-prev-leave-to {
|
||||
transform: translateX(-24px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.reg-next-leave-to,
|
||||
.reg-prev-enter-from {
|
||||
transform: translateX(24px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.reg-fade-enter-active,
|
||||
.reg-fade-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.reg-fade-enter-from,
|
||||
.reg-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes reg-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reg-pop {
|
||||
0% {
|
||||
transform: scale(0.4);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reg-ring {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.55);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reg-next-enter-active,
|
||||
.reg-next-leave-active,
|
||||
.reg-prev-enter-active,
|
||||
.reg-prev-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.reg-next-enter-from,
|
||||
.reg-next-leave-to,
|
||||
.reg-prev-enter-from,
|
||||
.reg-prev-leave-to {
|
||||
transform: none;
|
||||
}
|
||||
.reg__doneMark,
|
||||
.reg__doneMark :deep(svg),
|
||||
.reg__doneRing {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user