feat: catalog read cache (api + user apps) and clearer Zarrin accounting errors

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
s.esmaeili
2026-10-11 14:31:32 +03:30
co-authored by Cursor
parent 561addc676
commit 574fa1b4aa
53 changed files with 1241 additions and 401 deletions
+28 -30
View File
@@ -94,8 +94,10 @@ import ProductCarousel from '../components/home/ProductCarousel.vue'
import { useConfigStore } from '../stores/configStore'
import type { HomeCategory } from '../stores/configStore'
import { useFiltersStore } from '../stores/filtersStore'
import { api } from '../api/client'
import type { AdminSlide, GoldPriceQuote, ProductSummary } from '../api/client'
import { useCachedReads } from '../api/cachedReads'
import { useCatalogStore } from '../stores/catalogStore'
import { KEYS } from '../stores/queryCache'
// Module scope, so it survives this view being unmounted and remounted as the
// user moves between tabs, and resets only on a full app reload.
@@ -104,27 +106,29 @@ let hasRevealed = false
const router = useRouter()
const memory = useConfigStore()
const filters = useFiltersStore()
const catalog = useCatalogStore()
const reads = useCachedReads()
const loading = ref(true)
const loading = ref(!catalog.loaded)
const refreshing = ref(false)
const products = ref<ProductSummary[]>([])
const goldPrice = ref<GoldPriceQuote | null>(null)
const products = computed(() => catalog.products)
const goldPrice = ref<GoldPriceQuote | null>(reads.peek<GoldPriceQuote>(KEYS.goldPrice) ?? null)
// Admin-managed banner slides (manage/Const.vue "اسلایدر موبایل"). null while
// unfetched/unavailable — distinct from "fetched, empty" — so `slides` below
// knows whether to wait or fall back to the category-derived hero.
// Kept in step with user-app, which has had this since the admin screen was
// wired up; without it, slides configured in the admin panel never showed here.
const adminSlides = ref<AdminSlide[] | null>(null)
const adminSlides = ref<AdminSlide[] | null>(reads.peek<AdminSlide[]>(KEYS.homeSlider) ?? null)
// First-paint entrance. Flipped once, after the first data load, so the
// sections settle into place instead of appearing all at once; pull-to-refresh
// does not replay it.
//
// MasterLayout renders its route without keep-alive, so this view remounts on
// every return to the Home tab. The flag therefore lives outside the component
// — the choreography is a first-run flourish, and replaying it on each tab
// switch would fight the page transition rather than add to it.
// MasterLayout keeps this view alive between tab switches, but it still
// remounts after a visit to a full-page route. The flag therefore lives outside
// the component — the choreography is a first-run flourish, and replaying it
// would fight the page transition rather than add to it.
const revealed = ref(hasRevealed)
// Each section starts a beat after the one above it. Kept small — the whole
@@ -208,31 +212,25 @@ function goNewest() {
router.push({ name: 'newestProduct' })
}
async function loadData() {
try {
const [list] = await Promise.all([api.listProducts(), memory.init()])
products.value = list
} catch (e) {
console.log(e)
}
// The three reads are independent, so they run side by side. Each one is
// cached until pull-to-refresh asks the server again.
async function loadData(force = false) {
const [catalogResult, priceResult, slidesResult] = await Promise.allSettled([
// memory.init() reads the same cached catalogue, so this is one request.
memory.init(force),
reads.goldPrice({ force }),
reads.homeSlider({ force }),
])
try {
goldPrice.value = await api.getLatestGoldPrice()
} catch {
goldPrice.value = null
}
try {
adminSlides.value = await api.getHomeSlider()
} catch {
// No admin slides configured, or the endpoint failed — the category-
// derived hero covers this silently, same as before this existed.
adminSlides.value = null
}
if (catalogResult.status === 'rejected') console.log(catalogResult.reason)
goldPrice.value = priceResult.status === 'fulfilled' ? priceResult.value : null
// No admin slides configured, or the endpoint failed — the category-derived
// hero covers this silently, same as before this existed.
adminSlides.value = slidesResult.status === 'fulfilled' ? slidesResult.value : null
}
async function onRefresh() {
await loadData()
await loadData(true)
refreshing.value = false
}