admin user and ui panel
This commit is contained in:
@@ -138,7 +138,7 @@ import type { HomeCategory } from '../stores/configStore'
|
||||
import { useCartStore } from '../stores/cartStore'
|
||||
import { useFiltersStore } from '../stores/filtersStore'
|
||||
import { api } from '../api/client'
|
||||
import type { GoldPriceQuote, ProductSummary } from '../api/client'
|
||||
import type { AdminSlide, GoldPriceQuote, ProductSummary } from '../api/client'
|
||||
|
||||
const router = useRouter()
|
||||
const memory = useConfigStore()
|
||||
@@ -149,6 +149,10 @@ const loading = ref(true)
|
||||
const refreshing = ref(false)
|
||||
const products = ref<ProductSummary[]>([])
|
||||
const goldPrice = ref<GoldPriceQuote | null>(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.
|
||||
const adminSlides = ref<AdminSlide[] | null>(null)
|
||||
|
||||
const productDrawer = ref(false)
|
||||
const activeProductId = ref<string | null>(null)
|
||||
@@ -157,9 +161,12 @@ 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))
|
||||
|
||||
// The hero is built from REAL categories, not a curated banner payload (none
|
||||
// exists). Each slide deep-links into search filtered by that category, so the
|
||||
// artwork is decorative but the destination is genuine.
|
||||
// The hero prefers the admin-uploaded banner (manage/Const.vue "اسلایدر
|
||||
// موبایل") when one has been configured; otherwise it falls back to a hero
|
||||
// built from REAL categories, since no curated banner payload existed before
|
||||
// that admin screen was wired up. Each fallback slide deep-links into search
|
||||
// filtered by that category, so the artwork is decorative but the
|
||||
// destination is genuine.
|
||||
const SLIDE_COPY = [
|
||||
{ eyebrow: 'مجموعه منتزر', cta: 'دیدن مجموعه' },
|
||||
{ eyebrow: 'انتخاب سردبیر', cta: 'مشاهده' },
|
||||
@@ -167,6 +174,20 @@ const SLIDE_COPY = [
|
||||
]
|
||||
|
||||
const slides = computed<ShowcaseSlide[]>(() => {
|
||||
if (adminSlides.value?.length) {
|
||||
return adminSlides.value
|
||||
.filter((s) => s.image)
|
||||
.map((s, i) => ({
|
||||
key: `${i}-${s.image}`,
|
||||
eyebrow: 'مجموعه منتزر',
|
||||
title: '',
|
||||
subtitle: '',
|
||||
cta: '',
|
||||
image: s.image,
|
||||
link: s.link || undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
const cats = memory.cats.slice(0, 3)
|
||||
if (!cats.length) return []
|
||||
return cats.map((cat, i) => ({
|
||||
@@ -196,6 +217,17 @@ function onCategory(cat: HomeCategory) {
|
||||
}
|
||||
|
||||
function onSlide(slide: ShowcaseSlide) {
|
||||
// Admin-uploaded slides (manage/Const.vue) carry their own destination
|
||||
// instead of a categoryId — an absolute URL opens in a new tab, anything
|
||||
// else is treated as an in-app path.
|
||||
if (slide.link) {
|
||||
if (/^https?:\/\//i.test(slide.link)) {
|
||||
window.open(slide.link, '_blank', 'noopener')
|
||||
} else {
|
||||
router.push(slide.link)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (slide.categoryId) {
|
||||
filters.query.cats = [slide.categoryId]
|
||||
}
|
||||
@@ -221,6 +253,14 @@ async function loadData() {
|
||||
} catch {
|
||||
goldPrice.value = null
|
||||
}
|
||||
|
||||
try {
|
||||
adminSlides.value = await api.getHomeSlider()
|
||||
} catch {
|
||||
// No admin slides configured, or the endpoint failed — the category-
|
||||
// derived hero below covers this silently, same as before this existed.
|
||||
adminSlides.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function onRefresh() {
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { PullRefresh } from 'vant'
|
||||
import Icon from '../../components/icon/Icon.vue'
|
||||
import PageBar from '../../components/ui/PageBar.vue'
|
||||
@@ -246,6 +247,22 @@ function onBarClick(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
// The filter sheet deliberately hides the bottom tab bar while it is open, so
|
||||
// its own fixed action bar ("اعمال فیلتر" / "حذف فیلترها") stays clickable —
|
||||
// the tab bar sits at the same bottom edge with a higher z-index. But leaving
|
||||
// the page with the sheet still open (browser/swipe back, or tapping a
|
||||
// collection tile) never ran hideFilters, so that hide leaked: showNav stayed
|
||||
// false, and on any destination outside the five tab routes the user was left
|
||||
// with no tab bar at all until a full page refresh reset the store.
|
||||
//
|
||||
// Restoring it here rather than in onBeforeUnmount is deliberate: this guard
|
||||
// runs BEFORE router.afterEach, which then writes the correct visibility for
|
||||
// the destination and gets the last word. onBeforeUnmount runs after it, and
|
||||
// would wrongly re-show the bar on full-page routes like /product/:id.
|
||||
onBeforeRouteLeave(() => {
|
||||
if (showFilter.value) nav.setNavVisibility(true)
|
||||
})
|
||||
|
||||
onMounted(loadProducts)
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user