Polish user-app auth, nav, and product UI for Hedena Gold.
Gate profile/orders on login, autofocus OTP, stretch product sheet white, and refresh dialog illustrations to the brand palette.
This commit is contained in:
@@ -128,7 +128,7 @@
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="loginNotify" :closeOnClickOverlay="true">
|
||||
<template v-slot:img>
|
||||
<img :src="loginDialogImage" alt="" class="opacity-80" />
|
||||
<img :src="loginDialogImage" alt="" />
|
||||
</template>
|
||||
<template v-slot:title>
|
||||
<div class="p-8 text-justify">برای ثبت سفارش ابتدا باید وارد حساب کاربری خود شوید.</div>
|
||||
@@ -147,7 +147,7 @@ import { useRouter } from 'vue-router'
|
||||
import { Drawer, Form, FormItem, Textarea } from 'ant-design-vue'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import dialogEmpty4 from '../assets/img/empty/dialogEmpty4.svg'
|
||||
import loginDialogImage from '../assets/img/empty/dialogEmpty1.svg'
|
||||
import loginDialogImage from '../assets/img/empty/dialogLogin.svg'
|
||||
import Icon from '../components/icon/Icon.vue'
|
||||
import PageBar from '../components/ui/PageBar.vue'
|
||||
import Spinner from '../components/Spinner.vue'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
2. the live 18k rate strip (real: GET /pricing/latest)
|
||||
3. an editorial showcase hero built from real categories
|
||||
4. the category rail (real: derived from GET /catalog/products)
|
||||
5. "برگزیده" — a wide feature card + a two-column grid
|
||||
5. "برگزیده" — masonry of product cards
|
||||
6. "تازهها" — a horizontal rail
|
||||
7. the account block (real: GET /customers/:id when signed in)
|
||||
|
||||
@@ -59,17 +59,33 @@
|
||||
@select="onCategory"
|
||||
/>
|
||||
|
||||
<section v-if="featured" class="home__section reveal" :style="revealDelay(3)">
|
||||
<section v-if="featuredProducts.length" class="home__section reveal" :style="revealDelay(3)">
|
||||
<SectionHead
|
||||
:title="`برگزیده ${brand.name}`"
|
||||
subtitle="انتخابشده از میان مجموعههای موجود"
|
||||
action-label="همه محصولات"
|
||||
@action="goSearch"
|
||||
/>
|
||||
<ShowcaseCard :value="featured" wide badge="برگزیده" @open="openProduct" />
|
||||
|
||||
<div v-if="gridProducts.length" class="home__grid">
|
||||
<ShowcaseCard v-for="item in gridProducts" :key="item.id" :value="item" @open="openProduct" />
|
||||
<div class="home__masonry">
|
||||
<div class="home__masonryCol">
|
||||
<ShowcaseCard
|
||||
v-for="(cell, i) in masonryCols.start"
|
||||
:key="cell.item.id"
|
||||
:value="cell.item"
|
||||
:badge="i === 0 ? 'برگزیده' : undefined"
|
||||
:size="cell.size"
|
||||
@open="openProduct"
|
||||
/>
|
||||
</div>
|
||||
<div class="home__masonryCol">
|
||||
<ShowcaseCard
|
||||
v-for="cell in masonryCols.end"
|
||||
:key="cell.item.id"
|
||||
:value="cell.item"
|
||||
:size="cell.size"
|
||||
@open="openProduct"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -160,9 +176,37 @@ const adminSlides = ref<AdminSlide[] | null>(reads.peek<AdminSlide[]>(KEYS.homeS
|
||||
// and later returns to this tab do not replay it.
|
||||
const { revealed, revealDelay, reveal } = useReveal('home')
|
||||
|
||||
const featured = computed<ProductSummary | null>(() => products.value[0] ?? null)
|
||||
const gridProducts = computed(() => products.value.slice(1, 5))
|
||||
const newestProducts = computed(() => products.value.slice(5, 13))
|
||||
/**
|
||||
* Featured masonry — two real columns with deliberately *different* height
|
||||
* rhythms (not a mirrored tall/short alternation), so the block reads
|
||||
* asymmetric. RTL puts `start` on the right.
|
||||
*/
|
||||
const featuredProducts = computed(() => products.value.slice(0, 9))
|
||||
const newestProducts = computed(() => products.value.slice(9, 17))
|
||||
|
||||
type MasonrySize = 'hero' | 'tall' | 'mid' | 'short' | 'squat'
|
||||
|
||||
/** Start column (visual right in RTL): opens tall, then irregular. */
|
||||
const START_SIZES: MasonrySize[] = ['hero', 'short', 'tall', 'mid', 'squat']
|
||||
/** End column: different sequence + often one fewer/taller cards. */
|
||||
const END_SIZES: MasonrySize[] = ['mid', 'hero', 'short', 'tall']
|
||||
|
||||
const masonryCols = computed(() => {
|
||||
const items = featuredProducts.value
|
||||
const start: { item: ProductSummary; size: MasonrySize }[] = []
|
||||
const end: { item: ProductSummary; size: MasonrySize }[] = []
|
||||
items.forEach((item, i) => {
|
||||
// Uneven split: 5 on start / 4 on end when we have 9 — keeps columns unequal.
|
||||
if (i % 2 === 0 && start.length < START_SIZES.length) {
|
||||
start.push({ item, size: START_SIZES[start.length] })
|
||||
} else if (end.length < END_SIZES.length) {
|
||||
end.push({ item, size: END_SIZES[end.length] })
|
||||
} else {
|
||||
start.push({ item, size: START_SIZES[start.length % START_SIZES.length] })
|
||||
}
|
||||
})
|
||||
return { start, end }
|
||||
})
|
||||
|
||||
// The hero shows ONLY what the admin uploaded in the slider section
|
||||
// (manage/Const.vue "اسلایدر موبایل"). It used to fall back to slides built
|
||||
@@ -337,11 +381,19 @@ onActivated(async () => {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.home__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
/* Asymmetric masonry: two independent columns, unequal size rhythms. */
|
||||
.home__masonry {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
.home__masonryCol {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 11px;
|
||||
margin-top: 11px;
|
||||
}
|
||||
|
||||
.home__reel {
|
||||
|
||||
@@ -98,20 +98,22 @@
|
||||
|
||||
<!-- Nav grid -->
|
||||
<section class="dash__section reveal" :style="revealDelay(1)">
|
||||
<SectionHead title="دسترسیها" subtitle="بخشهای حساب شما" />
|
||||
<div class="dash__grid">
|
||||
<div class="dash__access">
|
||||
<button
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="dash__tile"
|
||||
:class="{ 'is-disabled': item.disabled }"
|
||||
:class="[`is-${item.tone}`, { 'is-disabled': item.disabled }]"
|
||||
@click="goToDetails(item.id)"
|
||||
>
|
||||
<span class="dash__tileIcon" aria-hidden="true">
|
||||
<Icon :icon="item.icon" size="20" />
|
||||
<Icon :icon="item.icon" size="21" />
|
||||
</span>
|
||||
<span class="dash__tileTitle">{{ item.title }}</span>
|
||||
<span class="dash__tileArrow" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:arrow-left-2" size="14" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -126,7 +128,7 @@
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="data.notify" :closeOnClickOverlay="true">
|
||||
<template v-slot:img>
|
||||
<img :src="exitImage" alt="" class="opacity-70 mb-8" />
|
||||
<img :src="exitImage" alt="" />
|
||||
</template>
|
||||
<template v-slot:title>آیا می خواهید از برنامه خارج شوید؟</template>
|
||||
<template v-slot:button>
|
||||
@@ -137,7 +139,7 @@
|
||||
|
||||
<DialogVant :timeOut="-1" v-model:show="data.loginNotify" :closeOnClickOverlay="true">
|
||||
<template v-slot:img>
|
||||
<img :src="loginDialogImage" alt="" class="opacity-80" />
|
||||
<img :src="loginDialogImage" alt="" />
|
||||
</template>
|
||||
<template v-slot:title>
|
||||
<div class="p-8 text-justify">
|
||||
@@ -162,8 +164,8 @@ import Icon from '../../components/icon/Icon.vue'
|
||||
import SectionHead from '../../components/home/SectionHead.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 exitImage from '../../assets/img/empty/dialogLogout.svg'
|
||||
import loginDialogImage from '../../assets/img/empty/dialogLogin.svg'
|
||||
import { price } from '../../utils/filters'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
import { useReveal } from '../../composables/useReveal'
|
||||
@@ -191,14 +193,14 @@ const grams = computed(() =>
|
||||
const data = reactive({ notify: false, loginNotify: false })
|
||||
|
||||
const BASE_TILES = [
|
||||
{ id: 0, icon: 'vuesax-linear:box', title: 'سفارشات', disabled: false },
|
||||
{ id: 1, icon: 'vuesax-linear:receipt-text', title: 'اسناد', disabled: false },
|
||||
{ id: 2, icon: 'vuesax-linear:user', title: 'پروفایل', disabled: false },
|
||||
{ id: 3, icon: 'vuesax-linear:heart', title: 'علاقمندی', disabled: false },
|
||||
{ id: 0, icon: 'vuesax-linear:box', title: 'سفارشات', tone: 'forest', disabled: false },
|
||||
{ id: 1, icon: 'vuesax-linear:receipt-text', title: 'اسناد', tone: 'sand', disabled: false },
|
||||
{ id: 2, icon: 'vuesax-linear:user', title: 'پروفایل', tone: 'wine', disabled: false },
|
||||
{ id: 3, icon: 'vuesax-linear:heart', title: 'علاقمندی', tone: 'rose', disabled: false },
|
||||
// D24 — the account statement. The reference put it on the dashboard too,
|
||||
// gated on hasMCode() (Dashboard.vue:179, :449). The target has no mcode, so
|
||||
// every signed-in customer sees it and the server scopes it to their token.
|
||||
{ id: 6, icon: 'vuesax-linear:chart-2', title: 'گزارش', disabled: false },
|
||||
{ id: 6, icon: 'vuesax-linear:chart-2', title: 'گزارش', tone: 'slate', disabled: false },
|
||||
]
|
||||
|
||||
/**
|
||||
@@ -208,13 +210,13 @@ const BASE_TILES = [
|
||||
* server refuses both routes for a non-expert token regardless.
|
||||
*/
|
||||
const EXPERT_TILES = [
|
||||
{ id: 4, icon: 'vuesax-linear:box-tick', title: 'سفارش مشتریان', disabled: false },
|
||||
{ id: 5, icon: 'vuesax-linear:profile-2user', title: 'مشتریان من', disabled: false },
|
||||
{ id: 4, icon: 'vuesax-linear:box-tick', title: 'سفارش مشتریان', tone: 'forest', disabled: false },
|
||||
{ id: 5, icon: 'vuesax-linear:profile-2user', title: 'مشتریان من', tone: 'sand', disabled: false },
|
||||
]
|
||||
|
||||
// Appearance (theme + reduced motion) — always last, and open to everyone:
|
||||
// device preferences, not account data. See goToDetails.
|
||||
const APPEARANCE_TILE = { id: 7, icon: 'vuesax-linear:brush', title: 'ظاهر', disabled: false }
|
||||
const APPEARANCE_TILE = { id: 7, icon: 'vuesax-linear:brush', title: 'ظاهر', tone: 'ink', disabled: false }
|
||||
|
||||
const list = computed(() => [
|
||||
...BASE_TILES,
|
||||
@@ -522,47 +524,91 @@ onMounted(async () => {
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.dash__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 9px;
|
||||
/* One island holding account shortcuts — denser, clearer than a flat 4-col grid. */
|
||||
.dash__access {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
padding: 6px;
|
||||
border-radius: 22px;
|
||||
background: @canvas;
|
||||
box-shadow:
|
||||
0 1px 0 rgba(15, 23, 42, 0.04),
|
||||
0 10px 28px rgba(15, 23, 42, 0.06);
|
||||
border: 1px solid @hairline;
|
||||
}
|
||||
|
||||
.dash__tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: 44px 1fr 18px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 6px;
|
||||
border: 1px solid @hairline;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 11px 12px;
|
||||
border: 0;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
background: @canvas;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
transition: transform 0.18s ease;
|
||||
text-align: start;
|
||||
transition: background 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.dash__tile:active {
|
||||
transform: scale(0.97);
|
||||
transform: scale(0.99);
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.dash__tile.is-disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dash__tileIcon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @inkSoft;
|
||||
}
|
||||
|
||||
.dash__tile.is-forest .dash__tileIcon {
|
||||
color: @primary;
|
||||
background: alpha(@primary, 10%);
|
||||
}
|
||||
.dash__tile.is-sand .dash__tileIcon {
|
||||
color: @secondary;
|
||||
background: alpha(@secondary, 12%);
|
||||
}
|
||||
.dash__tile.is-wine .dash__tileIcon {
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
}
|
||||
.dash__tile.is-rose .dash__tileIcon {
|
||||
color: @wineSoft;
|
||||
background: alpha(@wineSoft, 12%);
|
||||
}
|
||||
.dash__tile.is-slate .dash__tileIcon {
|
||||
color: @inkMuted;
|
||||
background: @hairlineSoft;
|
||||
}
|
||||
.dash__tile.is-ink .dash__tileIcon {
|
||||
color: @inkSoft;
|
||||
background: alpha(@ink, 6%);
|
||||
}
|
||||
|
||||
.dash__tileTitle {
|
||||
font-size: 11px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
color: @inkSoft;
|
||||
color: @ink;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.dash__tileArrow {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @inkFaint;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,19 +22,34 @@
|
||||
<PageBar title="ورود" back-to="home" />
|
||||
|
||||
<container class="auth__body">
|
||||
<section class="auth__intro">
|
||||
<div class="auth__mark" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:security-user" size="28" />
|
||||
</div>
|
||||
<section class="auth__hero">
|
||||
<img src="/img/brand-logo.png" :alt="brand.name" class="auth__logo" />
|
||||
<h2 class="auth__title">ورود به {{ brand.name }}</h2>
|
||||
<p class="auth__sub">
|
||||
{{ body.step === 1 ? 'شماره موبایل خود را وارد کنید' : 'رمز یکبارمصرف را وارد کنید' }}
|
||||
{{ body.step === 1 ? 'با شماره موبایل وارد شوید؛ کد یکبارمصرف پیامک میشود.' : 'کد ۴ رقمی پیامکشده را وارد کنید.' }}
|
||||
</p>
|
||||
|
||||
<ol class="auth__steps" aria-label="مراحل ورود">
|
||||
<li class="auth__step" :class="{ 'is-on': body.step === 1, 'is-done': body.step > 1 }">
|
||||
<span class="auth__stepNum">۱</span>
|
||||
<span>شماره</span>
|
||||
</li>
|
||||
<li class="auth__stepLine" aria-hidden="true" />
|
||||
<li class="auth__step" :class="{ 'is-on': body.step === 2 }">
|
||||
<span class="auth__stepNum">۲</span>
|
||||
<span>کد تأیید</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="auth__card">
|
||||
<section class="auth__island">
|
||||
<!-- Step 2: OTP -->
|
||||
<template v-if="body.step === 2">
|
||||
<div class="auth__phoneChip">
|
||||
<span>{{ formModel.username }}</span>
|
||||
<button type="button" class="auth__editPhone" @click="backToPhone">ویرایش</button>
|
||||
</div>
|
||||
|
||||
<OtpInput
|
||||
class="auth__otp"
|
||||
input-classes="otp-input"
|
||||
@@ -46,19 +61,27 @@
|
||||
/>
|
||||
|
||||
<p v-if="timer > 0" class="auth__timer">
|
||||
امکان دریافت مجدد رمز در {{ counterZeroShow(timer) }} ثانیه دیگر
|
||||
ارسال مجدد تا <strong>{{ counterZeroShow(timer) }}</strong> ثانیه
|
||||
</p>
|
||||
<p v-else class="auth__timer">برای دریافت رمز عبور مجدد تلاش کنید</p>
|
||||
<p v-else class="auth__timer is-ready">میتوانید کد را دوباره دریافت کنید</p>
|
||||
</template>
|
||||
|
||||
<Form :model="formModel" layout="vertical" @finish="sendPhone" class="auth__form">
|
||||
<FormItem
|
||||
v-show="body.step === 1"
|
||||
name="username"
|
||||
label="تلفن همراه"
|
||||
:rules="[required('تلفن همراه'), mobileRule('تلفن همراه')]"
|
||||
label="شماره موبایل"
|
||||
:rules="[required('شماره موبایل'), mobileRule('شماره موبایل')]"
|
||||
>
|
||||
<Input type="number" v-model:value="formModel.username" />
|
||||
<Input
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
dir="ltr"
|
||||
placeholder="09123456789"
|
||||
autocomplete="tel"
|
||||
v-model:value="formModel.username"
|
||||
class="auth__phoneInput"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<button
|
||||
@@ -67,14 +90,15 @@
|
||||
:class="{ 'is-loading': body.loading }"
|
||||
:disabled="body.loading || (timer > 0 && body.step === 2)"
|
||||
>
|
||||
<span v-if="body.loading" class="auth__ctaSpin" aria-hidden="true" />
|
||||
{{ btnText }}
|
||||
</button>
|
||||
</Form>
|
||||
</section>
|
||||
|
||||
<p class="auth__switch">
|
||||
اگر ثبت نام نکردهاید
|
||||
<button type="button" @click="emit('register', true)">ثبت نام کنید</button>
|
||||
حساب ندارید؟
|
||||
<button type="button" @click="emit('register', true)">ثبتنام فروشگاه</button>
|
||||
</p>
|
||||
</container>
|
||||
</div>
|
||||
@@ -85,7 +109,6 @@ import { brand } from '../../brand'
|
||||
import { computed, nextTick, reactive, ref } from 'vue'
|
||||
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 { toast } from '../../composables/useToast'
|
||||
import OtpInput from '../../components/input/OtpInput.vue'
|
||||
@@ -117,6 +140,12 @@ const countDownTimer = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function backToPhone() {
|
||||
body.step = 1
|
||||
body.defaultFocus = false
|
||||
timer.value = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1 — ask the server to send an SMS code.
|
||||
*
|
||||
@@ -215,56 +244,145 @@ function extractServerMessage(e: Error): string | null {
|
||||
|
||||
const counterZeroShow = (value: number) => (value < 10 ? '0' + value : value)
|
||||
|
||||
const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور' : 'دریافت مجدد رمز عبور'))
|
||||
const btnText = computed(() => (body.step === 1 ? 'دریافت کد ورود' : 'ارسال مجدد کد'))
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.auth {
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(120% 60% at 50% -10%, alpha(@primary, 8%), transparent 55%),
|
||||
@page;
|
||||
}
|
||||
|
||||
.auth__body {
|
||||
padding-top: 86px;
|
||||
padding-bottom: 90px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
|
||||
.auth__intro {
|
||||
.auth__hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding-bottom: 24px;
|
||||
padding: 4px 8px 22px;
|
||||
}
|
||||
|
||||
.auth__mark {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
.auth__logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: contain;
|
||||
border-radius: 20px;
|
||||
background: @canvas;
|
||||
box-shadow:
|
||||
0 1px 0 rgba(15, 23, 42, 0.04),
|
||||
0 12px 28px rgba(15, 23, 42, 0.08);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.auth__title {
|
||||
margin: 14px 0 0;
|
||||
font-size: 16px;
|
||||
margin: 16px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.auth__sub {
|
||||
margin: 6px 0 0;
|
||||
margin: 8px 0 0;
|
||||
max-width: 28rem;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.7;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.auth__card {
|
||||
padding: 18px 16px;
|
||||
.auth__steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin: 18px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.auth__step {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.auth__step.is-on,
|
||||
.auth__step.is-done {
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
.auth__stepNum {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 11px;
|
||||
background: @hairlineSoft;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.auth__step.is-on .auth__stepNum {
|
||||
background: @primary;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.auth__step.is-done .auth__stepNum {
|
||||
background: alpha(@primary, 14%);
|
||||
color: @primary;
|
||||
}
|
||||
|
||||
.auth__stepLine {
|
||||
width: 28px;
|
||||
height: 1px;
|
||||
background: @hairline;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.auth__island {
|
||||
padding: 20px 16px 18px;
|
||||
border-radius: 24px;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 1px 0 rgba(15, 23, 42, 0.04),
|
||||
0 14px 36px rgba(15, 23, 42, 0.07);
|
||||
}
|
||||
|
||||
.auth__phoneChip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 14px;
|
||||
background: @canvasSoft;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: @ink;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.auth__editPhone {
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: @wine;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.auth__otp {
|
||||
@@ -272,29 +390,69 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
direction: ltr;
|
||||
padding-bottom: 6px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.auth__timer {
|
||||
margin: 12px 0 16px;
|
||||
margin: 14px 0 16px;
|
||||
text-align: center;
|
||||
font-size: 11.5px;
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
|
||||
strong {
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
&.is-ready {
|
||||
color: @primary;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item-label > label) {
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: @inkSoft;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-input) {
|
||||
height: 48px;
|
||||
border-radius: 14px;
|
||||
border-color: @hairline;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-input:focus),
|
||||
.auth__form :deep(.ant-input-focused) {
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 0 3px alpha(@primary, 12%);
|
||||
}
|
||||
|
||||
.auth__phoneInput {
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.auth__cta {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 50px;
|
||||
padding: 13px 18px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
border-radius: 16px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @primary;
|
||||
@@ -310,10 +468,25 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.auth__ctaSpin {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: auth-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes auth-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.auth__switch {
|
||||
margin: 20px 0 0;
|
||||
margin: 22px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-size: 12.5px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
@@ -321,10 +494,10 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
padding: 0 4px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 12.5px;
|
||||
font-weight: 800;
|
||||
color: @wine;
|
||||
}
|
||||
</style>
|
||||
@@ -333,23 +506,25 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
|
||||
/* OtpInput renders its inputs with this class from a prop, so the rule cannot
|
||||
be scoped to this component. */
|
||||
.otp-input {
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
width: 52px;
|
||||
height: 56px;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
background: @canvas;
|
||||
background: @canvasSoft;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 13px;
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
transition: border-color 0.18s ease;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.otp-input:focus {
|
||||
outline: none;
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 0 3px alpha(@primary, 12%);
|
||||
background: @canvas;
|
||||
}
|
||||
|
||||
.otp-input.error {
|
||||
|
||||
@@ -18,53 +18,91 @@
|
||||
-->
|
||||
<template>
|
||||
<div class="auth">
|
||||
<PageBar title="ثبت نام" back-to="home" />
|
||||
<PageBar title="ثبتنام" back-to="home" />
|
||||
|
||||
<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 class="auth__hero">
|
||||
<img src="/img/brand-logo.png" :alt="brand.name" class="auth__logo" />
|
||||
<h2 class="auth__title">ساخت حساب فروشگاه</h2>
|
||||
<p class="auth__sub">
|
||||
پس از ثبت، کارشناسان {{ brand.name }} اطلاعات را بررسی میکنند و سپس میتوانید وارد شوید.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="auth__card">
|
||||
<Form :model="formModel" layout="vertical" @finish="register" class="auth__form">
|
||||
<Form :model="formModel" layout="vertical" @finish="register" class="auth__form">
|
||||
<section class="auth__island">
|
||||
<header class="auth__groupHead">
|
||||
<h3>اطلاعات شخصی</h3>
|
||||
<p>نام و شمارهٔ تماس مالک فروشگاه</p>
|
||||
</header>
|
||||
|
||||
<FormItem name="fname" label="نام" :rules="required('نام')">
|
||||
<Input v-model:value="formModel.fname" />
|
||||
<Input v-model:value="formModel.fname" autocomplete="given-name" placeholder="مثلاً علی" />
|
||||
</FormItem>
|
||||
<FormItem name="lname" label="نام خانوادگی" :rules="required('نام خانوادگی')">
|
||||
<Input v-model:value="formModel.lname" />
|
||||
<Input v-model:value="formModel.lname" autocomplete="family-name" placeholder="مثلاً محمدی" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="mobile"
|
||||
label="شماره موبایل"
|
||||
:rules="[mobileRule('شماره موبایل'), required('شماره موبایل')]"
|
||||
class="auth__itemLast"
|
||||
>
|
||||
<Input v-model:value="formModel.mobile" type="number" />
|
||||
<Input
|
||||
v-model:value="formModel.mobile"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
dir="ltr"
|
||||
placeholder="09123456789"
|
||||
autocomplete="tel"
|
||||
class="auth__ltr"
|
||||
/>
|
||||
</FormItem>
|
||||
</section>
|
||||
|
||||
<section class="auth__island">
|
||||
<header class="auth__groupHead">
|
||||
<h3>اطلاعات فروشگاه</h3>
|
||||
<p>برای تأیید و ارسال سفارشها لازم است</p>
|
||||
</header>
|
||||
|
||||
<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" />
|
||||
<Input v-model:value="formModel.store_name" placeholder="نام تابلو یا واحد صنفی" />
|
||||
</FormItem>
|
||||
|
||||
<button type="submit" class="auth__cta">ثبت اطلاعات</button>
|
||||
</Form>
|
||||
</section>
|
||||
<FormItem name="phone" label="تلفن فروشگاه" :rules="[required('تلفن فروشگاه')]">
|
||||
<Input
|
||||
v-model:value="formModel.phone"
|
||||
type="tel"
|
||||
inputmode="numeric"
|
||||
dir="ltr"
|
||||
placeholder="021xxxxxxx"
|
||||
class="auth__ltr"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="city" label="شهر" :rules="required('شهر')">
|
||||
<Input v-model:value="formModel.city" placeholder="مثلاً تهران" />
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="address" label="آدرس پستی" :rules="required('آدرس')" class="auth__itemLast">
|
||||
<Textarea
|
||||
v-model:value="formModel.address"
|
||||
:rows="3"
|
||||
placeholder="آدرس کامل برای هماهنگی ارسال"
|
||||
/>
|
||||
</FormItem>
|
||||
</section>
|
||||
|
||||
<button type="submit" class="auth__cta" :disabled="profileData.loading">
|
||||
ثبت درخواست عضویت
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
<p class="auth__switch">
|
||||
اگر حساب کاربری دارید
|
||||
<button type="button" @click="emit('register', false)">وارد شوید</button>
|
||||
قبلاً ثبتنام کردهاید؟
|
||||
<button type="button" @click="emit('register', false)">ورود به حساب</button>
|
||||
</p>
|
||||
</container>
|
||||
</Spinner>
|
||||
@@ -74,8 +112,8 @@
|
||||
<img :src="registerDelayImage" alt="" class="opacity-80" />
|
||||
</template>
|
||||
<template v-slot:title>
|
||||
<div class="p-5">
|
||||
ثبت نام شما با موفقیت انجام شد. منتظر تایید اطلاعات ثبت نامی شما توسط کارشناسان بمانید.
|
||||
<div>
|
||||
ثبتنام شما ثبت شد. پس از تأیید کارشناسان میتوانید با همین شماره وارد شوید.
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:button>
|
||||
@@ -86,11 +124,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { brand } from '../../brand'
|
||||
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'
|
||||
@@ -149,81 +187,168 @@ function serverMessage(e: unknown): string | null {
|
||||
|
||||
<style scoped lang="less">
|
||||
.auth {
|
||||
background: @page;
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(120% 60% at 50% -10%, alpha(@primary, 8%), transparent 55%),
|
||||
@page;
|
||||
}
|
||||
|
||||
.auth__body {
|
||||
padding-top: 86px;
|
||||
padding-bottom: 90px;
|
||||
padding-bottom: 48px;
|
||||
}
|
||||
|
||||
.auth__intro {
|
||||
.auth__hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding-bottom: 24px;
|
||||
padding: 4px 8px 20px;
|
||||
}
|
||||
|
||||
.auth__mark {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: @wine;
|
||||
background: @wineWash;
|
||||
.auth__logo {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
object-fit: contain;
|
||||
border-radius: 20px;
|
||||
background: @canvas;
|
||||
box-shadow:
|
||||
0 1px 0 rgba(15, 23, 42, 0.04),
|
||||
0 12px 28px rgba(15, 23, 42, 0.08);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.auth__title {
|
||||
margin: 14px 0 0;
|
||||
font-size: 16px;
|
||||
margin: 16px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.auth__sub {
|
||||
margin: 6px 0 0;
|
||||
margin: 8px 0 0;
|
||||
max-width: 28rem;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.7;
|
||||
color: @inkFaint;
|
||||
}
|
||||
|
||||
.auth__card {
|
||||
padding: 18px 16px 20px;
|
||||
.auth__island {
|
||||
margin-bottom: 12px;
|
||||
padding: 16px 16px 14px;
|
||||
border-radius: 24px;
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 20px;
|
||||
background: @canvasSoft;
|
||||
box-shadow:
|
||||
0 1px 0 rgba(15, 23, 42, 0.04),
|
||||
0 14px 36px rgba(15, 23, 42, 0.07);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth__groupHead {
|
||||
margin-bottom: 12px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: @ink;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 4px 0 0;
|
||||
font-size: 11.5px;
|
||||
color: @inkFaint;
|
||||
}
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item) {
|
||||
margin-bottom: 12px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.auth__form :deep(.auth__itemLast.ant-form-item),
|
||||
.auth__form :deep(.ant-form-item.auth__itemLast) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item-control),
|
||||
.auth__form :deep(.ant-form-item-control-input),
|
||||
.auth__form :deep(.ant-form-item-control-input-content) {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-form-item-label > label) {
|
||||
font-size: 12px;
|
||||
color: @inkMuted;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: @inkSoft;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-input),
|
||||
.auth__form :deep(textarea.ant-input) {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 14px;
|
||||
border-color: @hairline;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
background: @canvasSoft;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-input) {
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.auth__form :deep(textarea.ant-input) {
|
||||
min-height: 92px;
|
||||
padding-top: 12px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.auth__form :deep(.ant-input:focus),
|
||||
.auth__form :deep(.ant-input-focused),
|
||||
.auth__form :deep(textarea.ant-input:focus) {
|
||||
border-color: @primary;
|
||||
box-shadow: 0 0 0 3px alpha(@primary, 12%);
|
||||
}
|
||||
|
||||
.auth__ltr {
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.auth__cta {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
margin-top: 6px;
|
||||
min-height: 50px;
|
||||
padding: 13px 18px;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
border-radius: 16px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @primary;
|
||||
transition: transform 0.15s ease;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.auth__cta:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.auth__cta:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.auth__switch {
|
||||
margin: 20px 0 0;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-size: 12.5px;
|
||||
color: @inkMuted;
|
||||
}
|
||||
|
||||
@@ -231,10 +356,10 @@ function serverMessage(e: unknown): string | null {
|
||||
border: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0 3px;
|
||||
padding: 0 4px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 12.5px;
|
||||
font-weight: 800;
|
||||
color: @wine;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user