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:
s.esmaeili
2026-10-04 13:57:25 +03:30
co-authored by Claude Sonnet 5
parent 148b99c4a6
commit c05352d6fd
205 changed files with 3362 additions and 1701 deletions
+90 -7
View File
@@ -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;