feat: paginate customer orders and polish the dashboard hero

Add paged orders API and infinite scroll, short order numbers, and align the guest invite card with the logged-in hero shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
s.esmaeili
2026-10-17 13:11:42 +03:30
co-authored by Cursor
parent f550172567
commit afb068c643
89 changed files with 4518 additions and 1325 deletions
+33 -33
View File
@@ -27,12 +27,7 @@
</div>
</header>
<PullRefresh v-model="refreshing" @refresh="onRefresh">
<template #pulling><Loading size="18" /></template>
<template #loosing><Loading size="18" /></template>
<template #loading><Loading size="18" /></template>
<template #success><span /></template>
<AppPullRefresh v-model="refreshing" @refresh="onRefresh">
<container class="home__body" :class="{ 'is-revealed': revealed }">
<HeroCarousel
v-if="slides.length"
@@ -74,7 +69,7 @@
<p>هنوز محصولی در مجموعه ثبت نشده است.</p>
</section>
</container>
</PullRefresh>
</AppPullRefresh>
</div>
</template>
@@ -82,7 +77,7 @@
import { brand } from '../brand'
import { computed, nextTick, onActivated, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { Loading, PullRefresh } from 'vant'
import AppPullRefresh from '../components/ui/AppPullRefresh.vue'
import Icon from '../components/icon/Icon.vue'
import { openProduct as openProductMorph } from '../composables/useProductMorph'
import RateStrip from '../components/home/RateStrip.vue'
@@ -94,7 +89,7 @@ import ProductCarousel from '../components/home/ProductCarousel.vue'
import { useConfigStore } from '../stores/configStore'
import type { HomeCategory } from '../stores/configStore'
import { useFiltersStore } from '../stores/filtersStore'
import type { AdminSlide, GoldPriceQuote, ProductSummary } from '../api/client'
import { api, type AdminSlide, type GoldPriceQuote, type ProductSummary } from '../api/client'
import { useCachedReads } from '../api/cachedReads'
import { useCatalogStore } from '../stores/catalogStore'
import { KEYS } from '../stores/queryCache'
@@ -178,15 +173,18 @@ function goNewest() {
// cached until pull-to-refresh asks the server again.
async function loadData(force = false) {
const [catalogResult, catsResult, priceResult, slidesResult] = await Promise.allSettled([
// The home rails read `catalog.products`, so the catalogue is loaded
// explicitly. It used to come in as a side effect of `memory.init()`, which
// now reads the small facets endpoint instead of the whole catalogue — so
// without this call the home screen would render empty rails.
//
// Still the unpaged list: the rails slice it locally («جدیدترین», per
// category). Moving them onto paged reads is step 4 of
// docs/CATALOG-PAGINATION-PLAN.md.
catalog.load(force),
// STUB-03: rails need a small newest page. Pull-to-refresh must drop the
// cached catalogue first (same rule as before paging) or the screen keeps
// showing the previous slice while gold/slides refresh.
(async () => {
if (force) catalog.invalidate()
const page = await api.listProductsPaged({
page: 1,
limit: Math.max(REEL_LIMIT, LIST_LIMIT),
sort: 'newest',
})
catalog.replace(page.rows)
})(),
memory.init(force),
reads.goldPrice({ force }),
reads.homeSlider({ force }),
@@ -201,8 +199,13 @@ async function loadData(force = false) {
}
async function onRefresh() {
await loadData(true)
refreshing.value = false
try {
await loadData(true)
} finally {
// Always release Vant's PullRefresh, even when a request fails — otherwise
// the spinner sticks until a full reload (same bug Dashboard already fixed).
refreshing.value = false
}
}
onMounted(async () => {
@@ -252,25 +255,22 @@ onActivated(async () => {
min-height: 100vh;
}
// Same chrome as components/ui/PageBar.vue — one look for every top bar.
.home__bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 20;
background: alpha(@canvas, 82%);
backdrop-filter: blur(18px) saturate(140%);
// A hairline rule cut a hard line across the page. A very soft fade under
// the bar separates it from the content that scrolls beneath without
// drawing an edge.
border-bottom: 1px solid alpha(@wine, 5%);
box-shadow: 0 8px 20px -18px alpha(@wine, 55%);
background: alpha(@canvas, 88%);
backdrop-filter: blur(14px);
border-bottom: 1px solid @hairline;
}
.home__barInner {
max-width: 768px;
margin: 0 auto;
padding: 10px 16px;
padding: 11px 16px;
display: flex;
align-items: center;
justify-content: space-between;
@@ -278,15 +278,15 @@ onActivated(async () => {
}
.home__logo {
// Sized to match the pill's height so the two sides of the bar carry equal
// optical weight instead of the logo looking stranded next to it.
height: 25px;
// Tall enough to read at a glance; still fits the bar next to RateStrip.
height: 36px;
width: auto;
object-fit: contain;
}
.home__body {
// Clears the fixed bar (~50px) plus a little breathing room.
padding-top: 58px;
// Clears the fixed bar (same offset as PageBar / CartView).
padding-top: 76px;
padding-bottom: 90px;
}