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
@@ -38,11 +38,13 @@
|
||||
<template #loading><Loading size="18" /></template>
|
||||
<template #success><span /></template>
|
||||
<template v-if="cart.items.length">
|
||||
<div class="cart__items">
|
||||
<div class="cart__items" :class="{ 'is-revealed': revealed }">
|
||||
<CartItem
|
||||
v-for="item in cart.items"
|
||||
v-for="(item, i) in cart.items"
|
||||
:key="item.pid + item.vid"
|
||||
:value="item"
|
||||
class="reveal"
|
||||
:style="revealDelay(i)"
|
||||
@note="
|
||||
cartData.tempRow = item;
|
||||
cartData.showPopUp = true;
|
||||
@@ -119,7 +121,7 @@
|
||||
</template>
|
||||
<template v-slot:title>آیا مطمئن به ثبت سفارش هستید ؟</template>
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-[#1C2D56]" @click="submitOrder">بله</div>
|
||||
<div class="text-center text-[#23493c]" @click="submitOrder">بله</div>
|
||||
<div class="text-center text-[#6b7280]" @click="cartData.notify = false">بی خیال</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
@@ -132,7 +134,7 @@
|
||||
<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-[#23493c]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
|
||||
<div class="text-center text-danger" @click="loginNotify = false">بیخیال</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
@@ -153,6 +155,7 @@ import DialogVant from '../components/DialogVant.vue'
|
||||
import CartItem from '../components/product/CartItem.vue'
|
||||
import { useCartStore, type CartItem as CartItemType } from '../stores/cartStore'
|
||||
import { useSessionStore } from '../stores/session'
|
||||
import { useReveal } from '../composables/useReveal'
|
||||
import { api } from '../api/client'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -162,6 +165,10 @@ const session = useSessionStore()
|
||||
const loginNotify = ref(false)
|
||||
const refreshing = ref(false)
|
||||
|
||||
// First-paint entrance for the basket rows, the same choreography HomeView
|
||||
// uses with a slightly shorter beat, since a basket can hold many rows.
|
||||
const { revealed, revealDelay, reveal } = useReveal('cart', 60)
|
||||
|
||||
const cartData = reactive({
|
||||
loadingPage: false,
|
||||
showPopUp: false,
|
||||
@@ -227,6 +234,7 @@ async function onRefresh() {
|
||||
|
||||
onMounted(async () => {
|
||||
await cart.init()
|
||||
reveal()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -274,7 +282,7 @@ onMounted(async () => {
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
}
|
||||
|
||||
.cart__done {
|
||||
@@ -330,7 +338,7 @@ onMounted(async () => {
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
}
|
||||
|
||||
.cart__bar {
|
||||
@@ -390,7 +398,7 @@ onMounted(async () => {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
@@ -419,6 +427,6 @@ onMounted(async () => {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -40,20 +40,27 @@
|
||||
<template #loosing><Loading size="18" /></template>
|
||||
<template #loading><Loading size="18" /></template>
|
||||
<template #success><span /></template>
|
||||
<container class="home__body">
|
||||
<RateStrip :quote="goldPrice" class="home__rate" />
|
||||
<container class="home__body" :class="{ 'is-revealed': revealed }">
|
||||
<RateStrip :quote="goldPrice" class="home__rate reveal" :style="revealDelay(0)" />
|
||||
|
||||
<ShowcaseSlider v-if="slides.length" :slides="slides" class="home__hero" @select="onSlide" />
|
||||
<ShowcaseSlider
|
||||
v-if="slides.length"
|
||||
:slides="slides"
|
||||
class="home__hero reveal"
|
||||
:style="revealDelay(1)"
|
||||
@select="onSlide"
|
||||
/>
|
||||
|
||||
<CategoryRail
|
||||
v-if="memory.cats.length"
|
||||
:categories="memory.cats"
|
||||
:active-id="null"
|
||||
class="home__rail"
|
||||
class="home__rail reveal"
|
||||
:style="revealDelay(2)"
|
||||
@select="onCategory"
|
||||
/>
|
||||
|
||||
<section v-if="featured" class="home__section">
|
||||
<section v-if="featured" class="home__section reveal" :style="revealDelay(3)">
|
||||
<SectionHead
|
||||
title="برگزیده منتزر"
|
||||
subtitle="انتخابشده از میان مجموعههای موجود"
|
||||
@@ -67,7 +74,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="newestProducts.length" class="home__section">
|
||||
<section v-if="newestProducts.length" class="home__section reveal" :style="revealDelay(4)">
|
||||
<SectionHead
|
||||
title="تازهها"
|
||||
subtitle="آخرین افزودههای مجموعه"
|
||||
@@ -85,12 +92,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="!loading && !products.length" class="home__empty">
|
||||
<section v-if="!loading && !products.length" class="home__empty reveal" :style="revealDelay(3)">
|
||||
<Icon icon="vuesax-linear:box" size="26" />
|
||||
<p>هنوز محصولی در مجموعه ثبت نشده است.</p>
|
||||
</section>
|
||||
|
||||
<div class="home__trust">
|
||||
<div class="home__trust reveal" :style="revealDelay(5)">
|
||||
<div class="home__trustItem">
|
||||
<Icon icon="vuesax-linear:shield-tick" size="18" />
|
||||
<span>خرید امن</span>
|
||||
@@ -137,6 +144,7 @@ import { useConfigStore } from '../stores/configStore'
|
||||
import type { HomeCategory } from '../stores/configStore'
|
||||
import { useCartStore } from '../stores/cartStore'
|
||||
import { useFiltersStore } from '../stores/filtersStore'
|
||||
import { useReveal } from '../composables/useReveal'
|
||||
import { api } from '../api/client'
|
||||
import type { AdminSlide, GoldPriceQuote, ProductSummary } from '../api/client'
|
||||
|
||||
@@ -157,6 +165,11 @@ const adminSlides = ref<AdminSlide[] | null>(null)
|
||||
const productDrawer = ref(false)
|
||||
const activeProductId = ref<string | null>(null)
|
||||
|
||||
// First-paint entrance: played once, after the first data load, so the
|
||||
// sections settle into place instead of popping in piecemeal. Pull-to-refresh
|
||||
// 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))
|
||||
@@ -271,6 +284,7 @@ async function onRefresh() {
|
||||
onMounted(async () => {
|
||||
await loadData()
|
||||
loading.value = false
|
||||
reveal()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -405,7 +405,7 @@ onBeforeUnmount(() => observer?.disconnect())
|
||||
padding: 13px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -336,7 +336,7 @@ onMounted(load)
|
||||
padding: 13px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -296,8 +296,8 @@ onBeforeUnmount(() => observer?.disconnect())
|
||||
}
|
||||
|
||||
.rep__tab.is-on {
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
background: @primary;
|
||||
border-color: @primary;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ onBeforeUnmount(() => observer?.disconnect())
|
||||
padding: 13px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
|
||||
@@ -53,7 +53,14 @@
|
||||
</template>
|
||||
</PageBar>
|
||||
|
||||
<Spinner :spinning="loadingPage" tip="درحال دریافت اطلاعات">
|
||||
<!--
|
||||
No full-screen spinner here: it covered the page with the brand logo and
|
||||
then dropped the finished results in all at once. The first load shows a
|
||||
skeleton of the result grid instead, the same size as the real cards, so
|
||||
the results replace it in place; a pull-to-refresh shows only the pull
|
||||
indicator, over the results already on screen.
|
||||
-->
|
||||
<div>
|
||||
<Filters
|
||||
v-model:filterKeys="filterKeys"
|
||||
v-show="showFilter"
|
||||
@@ -68,6 +75,22 @@
|
||||
<template #loading><Loading size="18" /></template>
|
||||
<template #success><span /></template>
|
||||
<container class="search__body">
|
||||
<div v-if="loadingPage && !allProducts.length" class="search__skeleton" aria-hidden="true">
|
||||
<div class="search__skHead">
|
||||
<span class="skeleton search__skLine search__skLine--title" />
|
||||
<span class="skeleton search__skLine search__skLine--sub" />
|
||||
</div>
|
||||
<div class="search__grid">
|
||||
<div v-for="n in SKELETON_CARDS" :key="n" class="search__skCard">
|
||||
<div class="skeleton search__skArt" />
|
||||
<div class="search__skBody">
|
||||
<span class="skeleton search__skLine search__skLine--name" />
|
||||
<span class="skeleton search__skLine search__skLine--meta" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="availableCollections.length" class="search__collections">
|
||||
<button
|
||||
v-for="item in availableCollections"
|
||||
@@ -100,20 +123,23 @@
|
||||
<Spinner :spinning="true" :indicator="true" :fixed="false" tip="" cls="my-[10px]" />
|
||||
</div>
|
||||
|
||||
<section v-else-if="!loadingPage" class="search__empty">
|
||||
<!-- Its own condition, not a v-else of the sentinel above: chained to
|
||||
it, the box also showed under the results whenever they all fit
|
||||
in the first window. Same fix as user-app-v2. -->
|
||||
<section v-if="!filteredProducts.length && !loadingPage" class="search__empty">
|
||||
<Icon icon="vuesax-linear:search-normal-1" size="26" />
|
||||
<p>محصولی با این مشخصات پیدا نشد.</p>
|
||||
</section>
|
||||
</container>
|
||||
</PullRefresh>
|
||||
</Spinner>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { PullRefresh } from 'vant'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
import Spinner from '../../components/Spinner.vue'
|
||||
@@ -128,7 +154,12 @@ import { api, type ProductSummary } from '../../api/client'
|
||||
const filters = useFiltersStore()
|
||||
const nav = useNavStore()
|
||||
|
||||
const loadingPage = ref(false)
|
||||
// Starts true: the first render happens before onMounted kicks off the fetch,
|
||||
// and with false the "nothing found" box flashed up for a frame before the
|
||||
// loading state replaced it.
|
||||
const loadingPage = ref(true)
|
||||
// Two rows of the two-column grid — enough to fill the screen under the bar.
|
||||
const SKELETON_CARDS = 6
|
||||
const refreshing = ref(false)
|
||||
const allProducts = ref<ProductSummary[]>([])
|
||||
const showFilter = ref(false)
|
||||
@@ -316,8 +347,8 @@ onMounted(loadProducts)
|
||||
|
||||
.search__chip.is-on {
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
background: @primary;
|
||||
border-color: @primary;
|
||||
}
|
||||
|
||||
.search__chip--filter.is-on {
|
||||
@@ -372,6 +403,58 @@ onMounted(loadProducts)
|
||||
gap: 11px;
|
||||
}
|
||||
|
||||
// First-load placeholder. Mirrors SectionHead + ProductCard's box model
|
||||
// (square art, 11/12/13px body padding, an 18px-radius hairline card) so
|
||||
// the real results land exactly where the placeholders were.
|
||||
.search__skHead {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
padding-bottom: 13px;
|
||||
}
|
||||
|
||||
.search__skCard {
|
||||
background: @canvas;
|
||||
border: 1px solid @hairline;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search__skArt {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.search__skBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 11px;
|
||||
padding: 15px 12px 16px;
|
||||
}
|
||||
|
||||
.search__skLine {
|
||||
display: block;
|
||||
height: 11px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.search__skLine--title {
|
||||
width: 72px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.search__skLine--sub {
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.search__skLine--name {
|
||||
width: 78%;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.search__skLine--meta {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.search__more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<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'
|
||||
@@ -106,7 +106,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
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ function finish() {
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -49,10 +49,14 @@
|
||||
</header>
|
||||
|
||||
<PullRefresh v-model="refreshing" @refresh="onRefresh">
|
||||
<container class="dash__body">
|
||||
<template #pulling><Loading size="18" /></template>
|
||||
<template #loosing><Loading size="18" /></template>
|
||||
<template #loading><Loading size="18" /></template>
|
||||
<template #success><span /></template>
|
||||
<container class="dash__body" :class="{ 'is-revealed': revealed }">
|
||||
<!-- Signed in: real position from GET /customers/:id -->
|
||||
<template v-if="customer">
|
||||
<section class="dash__balance">
|
||||
<section class="dash__balance reveal" :style="revealDelay(0)">
|
||||
<div class="dash__balanceHead">
|
||||
<div>
|
||||
<div class="dash__eyebrow">{{ remainText }}</div>
|
||||
@@ -79,7 +83,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Signed out: a quiet invitation, no fabricated numbers -->
|
||||
<section v-else class="dash__invite">
|
||||
<section v-else class="dash__invite reveal" :style="revealDelay(0)">
|
||||
<div class="dash__inviteIcon" aria-hidden="true">
|
||||
<Icon icon="vuesax-linear:security-user" size="20" />
|
||||
</div>
|
||||
@@ -93,7 +97,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Nav grid -->
|
||||
<section class="dash__section">
|
||||
<section class="dash__section reveal" :style="revealDelay(1)">
|
||||
<SectionHead title="دسترسیها" subtitle="بخشهای حساب شما" />
|
||||
<div class="dash__grid">
|
||||
<button
|
||||
@@ -113,7 +117,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Live 18k rate — same strip the home screen uses -->
|
||||
<section class="dash__section">
|
||||
<section class="dash__section reveal" :style="revealDelay(2)">
|
||||
<SectionHead title="نرخ روز" subtitle="آخرین نرخ اعلامشده" />
|
||||
<RateStrip :quote="goldPrice" />
|
||||
</section>
|
||||
@@ -126,7 +130,7 @@
|
||||
</template>
|
||||
<template v-slot:title>آیا می خواهید از برنامه خارج شوید؟</template>
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-[#1C2D56]" @click="logOut">بله</div>
|
||||
<div class="text-center text-[#23493c]" @click="logOut">بله</div>
|
||||
<div class="text-center text-[#6b7280]" @click="data.notify = false">بی خیال</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
@@ -142,7 +146,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:button>
|
||||
<div class="text-center text-[#1C2D56]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
|
||||
<div class="text-center text-[#23493c]" @click="router.push({ name: 'login' })">ورود/ثبت نام</div>
|
||||
<div class="text-center text-danger" @click="data.loginNotify = false">بیخیال</div>
|
||||
</template>
|
||||
</DialogVant>
|
||||
@@ -152,7 +156,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { PullRefresh } from 'vant'
|
||||
import { Loading, PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import SectionHead from '../../components/home/SectionHead.vue'
|
||||
import RateStrip from '../../components/home/RateStrip.vue'
|
||||
@@ -161,6 +165,7 @@ import exitImage from '../../assets/img/empty/dialogEmpty5.svg'
|
||||
import loginDialogImage from '../../assets/img/empty/dialogEmpty1.svg'
|
||||
import { price } from '../../utils/filters'
|
||||
import { useSessionStore } from '../../stores/session'
|
||||
import { useReveal } from '../../composables/useReveal'
|
||||
import { api, type CustomerDetail, type GoldPriceQuote } from '../../api/client'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -243,7 +248,14 @@ async function onRefresh() {
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
onMounted(loadDashboard)
|
||||
// First-paint entrance, the same choreography HomeView uses: played once,
|
||||
// after the first load; pull-to-refresh and tab returns don't replay it.
|
||||
const { revealed, revealDelay, reveal } = useReveal('dashboard')
|
||||
|
||||
onMounted(async () => {
|
||||
await loadDashboard()
|
||||
reveal()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -311,7 +323,7 @@ onMounted(loadDashboard)
|
||||
.dash__balance {
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
color: #fff;
|
||||
box-shadow: 0 14px 30px -22px rgba(20, 22, 26, 0.95);
|
||||
}
|
||||
@@ -455,7 +467,7 @@ onMounted(loadDashboard)
|
||||
font-size: 11.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
}
|
||||
|
||||
.dash__section {
|
||||
|
||||
@@ -105,6 +105,6 @@ onMounted(async () => {
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -286,7 +281,7 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
@@ -338,7 +333,7 @@ const btnText = computed(() => (body.step === 1 ? 'دریافت رمز عبور'
|
||||
|
||||
.otp-input:focus {
|
||||
outline: none;
|
||||
border-color: @ink;
|
||||
border-color: @primary;
|
||||
}
|
||||
|
||||
.otp-input.error {
|
||||
|
||||
@@ -261,8 +261,8 @@ onMounted(loadOrders)
|
||||
|
||||
.ord__chip.is-on {
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
background: @primary;
|
||||
border-color: @primary;
|
||||
}
|
||||
|
||||
.ord__chip:active {
|
||||
@@ -393,8 +393,8 @@ onMounted(loadOrders)
|
||||
|
||||
.detail__step.is-done .detail__stepDot {
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
border-color: @ink;
|
||||
background: @primary;
|
||||
border-color: @primary;
|
||||
}
|
||||
|
||||
.detail__step.is-active .detail__stepDot {
|
||||
|
||||
@@ -247,7 +247,7 @@ onMounted(async () => {
|
||||
padding: 14px;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -190,7 +190,7 @@ const register = async () => {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: @ink;
|
||||
background: @primary;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user