2026-09-09 06:46:27 +03:30
|
|
|
|
<!--
|
2026-09-14 23:05:29 +03:30
|
|
|
|
Redesigned home screen (replaces the previous port of legacy home/index.vue).
|
2026-09-09 06:46:27 +03:30
|
|
|
|
|
2026-09-14 23:05:29 +03:30
|
|
|
|
Layout intent — shop first, account second:
|
|
|
|
|
|
1. a slim brand bar with search + cart
|
|
|
|
|
|
2. the live 18k rate strip (real: GET /pricing/latest)
|
|
|
|
|
|
3. an editorial showcase hero built from real categories
|
|
|
|
|
|
4. the category rail (real: derived from GET /catalog/products)
|
|
|
|
|
|
5. "برگزیده" — a wide feature card + a two-column grid
|
|
|
|
|
|
6. "تازهها" — a horizontal rail
|
|
|
|
|
|
7. the account block (real: GET /customers/:id when signed in)
|
|
|
|
|
|
|
|
|
|
|
|
Data honesty, carried over from the previous version deliberately:
|
|
|
|
|
|
- the API exposes no curated home payload, no promotions and no per-product
|
|
|
|
|
|
price, so nothing on this page prints a rial price for a piece. Weight and
|
|
|
|
|
|
making-charge percent are real per-variant fields; everything else shown
|
|
|
|
|
|
is a label, not invented data.
|
|
|
|
|
|
- categories come from the catalogue itself, so an empty catalogue renders
|
|
|
|
|
|
an empty state instead of placeholder tiles.
|
2026-09-09 06:46:27 +03:30
|
|
|
|
-->
|
|
|
|
|
|
<template>
|
2026-09-14 23:05:29 +03:30
|
|
|
|
<div class="home">
|
|
|
|
|
|
<header class="home__bar">
|
|
|
|
|
|
<div class="home__barInner">
|
|
|
|
|
|
<router-link :to="{ name: 'cart' }" class="home__iconBtn" aria-label="سبد خرید">
|
|
|
|
|
|
<Icon icon="vuesax-linear:bag-2" size="21" />
|
|
|
|
|
|
<span v-if="cart.items.length" class="home__badge">{{ cart.items.length }}</span>
|
2026-09-09 06:46:27 +03:30
|
|
|
|
</router-link>
|
2026-09-14 23:05:29 +03:30
|
|
|
|
|
|
|
|
|
|
<img src="/img/henza1.svg" alt="هنزاگلد" class="home__logo" />
|
|
|
|
|
|
|
|
|
|
|
|
<router-link :to="{ name: 'search' }" class="home__iconBtn" aria-label="جستجو">
|
|
|
|
|
|
<Icon icon="vuesax-linear:search-normal-1" size="21" />
|
|
|
|
|
|
</router-link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
2026-09-16 01:15:16 +03:30
|
|
|
|
<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>
|
2026-09-14 23:05:29 +03:30
|
|
|
|
<container class="home__body">
|
|
|
|
|
|
<RateStrip :quote="goldPrice" class="home__rate" />
|
|
|
|
|
|
|
|
|
|
|
|
<ShowcaseSlider v-if="slides.length" :slides="slides" class="home__hero" @select="onSlide" />
|
|
|
|
|
|
|
|
|
|
|
|
<CategoryRail
|
|
|
|
|
|
v-if="memory.cats.length"
|
|
|
|
|
|
:categories="memory.cats"
|
|
|
|
|
|
:active-id="null"
|
|
|
|
|
|
class="home__rail"
|
|
|
|
|
|
@select="onCategory"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-if="featured" class="home__section">
|
|
|
|
|
|
<SectionHead
|
|
|
|
|
|
title="برگزیده هنزا"
|
|
|
|
|
|
subtitle="انتخابشده از میان مجموعههای موجود"
|
|
|
|
|
|
action-label="همه محصولات"
|
|
|
|
|
|
@action="goSearch"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ShowcaseCard :value="featured" wide badge="برگزیده" @open="openProduct" />
|
|
|
|
|
|
|
|
|
|
|
|
<div v-if="gridProducts.length" class="home__grid">
|
|
|
|
|
|
<ShowcaseCard v-for="item in gridProducts" :key="item.id" :value="item" @open="openProduct" />
|
2026-09-09 06:46:27 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
2026-09-14 23:05:29 +03:30
|
|
|
|
|
|
|
|
|
|
<section v-if="newestProducts.length" class="home__section">
|
|
|
|
|
|
<SectionHead
|
|
|
|
|
|
title="تازهها"
|
|
|
|
|
|
subtitle="آخرین افزودههای مجموعه"
|
|
|
|
|
|
action-label="مشاهده همه"
|
|
|
|
|
|
@action="goNewest"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="home__reel no-scrollbar">
|
|
|
|
|
|
<ShowcaseCard
|
|
|
|
|
|
v-for="item in newestProducts"
|
|
|
|
|
|
:key="item.id"
|
|
|
|
|
|
:value="item"
|
|
|
|
|
|
class="home__reelItem"
|
|
|
|
|
|
@open="openProduct"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section v-if="!loading && !products.length" class="home__empty">
|
|
|
|
|
|
<Icon icon="vuesax-linear:box" size="26" />
|
|
|
|
|
|
<p>هنوز محصولی در مجموعه ثبت نشده است.</p>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="home__trust">
|
|
|
|
|
|
<div class="home__trustItem">
|
|
|
|
|
|
<Icon icon="vuesax-linear:shield-tick" size="18" />
|
|
|
|
|
|
<span>خرید امن</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="home__trustItem">
|
|
|
|
|
|
<Icon icon="vuesax-linear:verify" size="18" />
|
|
|
|
|
|
<span>اصالت کالا</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="home__trustItem">
|
|
|
|
|
|
<Icon icon="vuesax-linear:box-tick" size="18" />
|
|
|
|
|
|
<span>ارسال بیمهشده</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-09-09 06:46:27 +03:30
|
|
|
|
</container>
|
2026-09-16 01:15:16 +03:30
|
|
|
|
</PullRefresh>
|
2026-09-14 23:05:29 +03:30
|
|
|
|
|
|
|
|
|
|
<Drawer
|
|
|
|
|
|
v-model:visible="productDrawer"
|
|
|
|
|
|
placement="bottom"
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
class="rtl"
|
|
|
|
|
|
height="100vh"
|
|
|
|
|
|
:bodyStyle="{ padding: 0, position: 'relative' }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<SingleProduct v-if="activeProductId" :id="activeProductId" @closeDrawer="productDrawer = false" />
|
|
|
|
|
|
</Drawer>
|
|
|
|
|
|
</div>
|
2026-09-09 06:46:27 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, ref } from 'vue'
|
|
|
|
|
|
import { useRouter } from 'vue-router'
|
2026-09-14 23:05:29 +03:30
|
|
|
|
import { Drawer } from 'ant-design-vue'
|
2026-09-16 01:15:16 +03:30
|
|
|
|
import { Loading, PullRefresh } from 'vant'
|
2026-09-09 06:46:27 +03:30
|
|
|
|
import Icon from '../components/icon/Icon.vue'
|
2026-09-14 23:05:29 +03:30
|
|
|
|
import SingleProduct from '../components/product/SingleProduct.vue'
|
|
|
|
|
|
import RateStrip from '../components/home/RateStrip.vue'
|
|
|
|
|
|
import ShowcaseSlider from '../components/home/ShowcaseSlider.vue'
|
|
|
|
|
|
import type { ShowcaseSlide } from '../components/home/ShowcaseSlider.vue'
|
|
|
|
|
|
import CategoryRail from '../components/home/CategoryRail.vue'
|
|
|
|
|
|
import ShowcaseCard from '../components/home/ShowcaseCard.vue'
|
|
|
|
|
|
import SectionHead from '../components/home/SectionHead.vue'
|
2026-09-09 06:46:27 +03:30
|
|
|
|
import { useConfigStore } from '../stores/configStore'
|
2026-09-14 23:05:29 +03:30
|
|
|
|
import type { HomeCategory } from '../stores/configStore'
|
|
|
|
|
|
import { useCartStore } from '../stores/cartStore'
|
|
|
|
|
|
import { useFiltersStore } from '../stores/filtersStore'
|
|
|
|
|
|
import { api } from '../api/client'
|
2026-09-16 01:15:16 +03:30
|
|
|
|
import type { GoldPriceQuote, ProductSummary } from '../api/client'
|
2026-09-09 06:46:27 +03:30
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const memory = useConfigStore()
|
2026-09-14 23:05:29 +03:30
|
|
|
|
const cart = useCartStore()
|
|
|
|
|
|
const filters = useFiltersStore()
|
2026-09-09 06:46:27 +03:30
|
|
|
|
|
2026-09-14 23:05:29 +03:30
|
|
|
|
const loading = ref(true)
|
2026-09-16 01:15:16 +03:30
|
|
|
|
const refreshing = ref(false)
|
2026-09-09 06:46:27 +03:30
|
|
|
|
const products = ref<ProductSummary[]>([])
|
2026-09-14 23:05:29 +03:30
|
|
|
|
const goldPrice = ref<GoldPriceQuote | null>(null)
|
2026-09-09 06:46:27 +03:30
|
|
|
|
|
2026-09-14 23:05:29 +03:30
|
|
|
|
const productDrawer = ref(false)
|
|
|
|
|
|
const activeProductId = ref<string | null>(null)
|
2026-09-09 06:46:27 +03:30
|
|
|
|
|
2026-09-14 23:05:29 +03:30
|
|
|
|
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))
|
2026-09-09 06:46:27 +03:30
|
|
|
|
|
2026-09-14 23:05:29 +03:30
|
|
|
|
// 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.
|
|
|
|
|
|
const SLIDE_COPY = [
|
|
|
|
|
|
{ eyebrow: 'مجموعه هنزا', cta: 'دیدن مجموعه' },
|
|
|
|
|
|
{ eyebrow: 'انتخاب سردبیر', cta: 'مشاهده' },
|
|
|
|
|
|
{ eyebrow: 'پیشنهاد این هفته', cta: 'بررسی کنید' },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const slides = computed<ShowcaseSlide[]>(() => {
|
|
|
|
|
|
const cats = memory.cats.slice(0, 3)
|
|
|
|
|
|
if (!cats.length) return []
|
|
|
|
|
|
return cats.map((cat, i) => ({
|
|
|
|
|
|
key: cat.id,
|
|
|
|
|
|
eyebrow: SLIDE_COPY[i % SLIDE_COPY.length].eyebrow,
|
|
|
|
|
|
title: cat.title,
|
|
|
|
|
|
subtitle: 'ساختهشده از طلای ۱۸ عیار، با اجرت شفاف و اصالت تضمینشده.',
|
|
|
|
|
|
cta: SLIDE_COPY[i % SLIDE_COPY.length].cta,
|
|
|
|
|
|
image: productImageForCategory(cat.id),
|
|
|
|
|
|
categoryId: cat.id,
|
|
|
|
|
|
}))
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
function productImageForCategory(categoryId: string): string | undefined {
|
|
|
|
|
|
const match = products.value.find((p) => p.category?.id === categoryId)
|
|
|
|
|
|
return match?.images?.[0]?.url
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openProduct(product: ProductSummary) {
|
|
|
|
|
|
activeProductId.value = product.id
|
|
|
|
|
|
productDrawer.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onCategory(cat: HomeCategory) {
|
|
|
|
|
|
filters.query.cats = [cat.id]
|
|
|
|
|
|
router.push({ name: 'search' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onSlide(slide: ShowcaseSlide) {
|
|
|
|
|
|
if (slide.categoryId) {
|
|
|
|
|
|
filters.query.cats = [slide.categoryId]
|
|
|
|
|
|
}
|
|
|
|
|
|
router.push({ name: 'search' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function goSearch() {
|
|
|
|
|
|
router.push({ name: 'search' })
|
|
|
|
|
|
}
|
|
|
|
|
|
function goNewest() {
|
|
|
|
|
|
router.push({ name: 'newestProduct' })
|
|
|
|
|
|
}
|
2026-09-16 01:15:16 +03:30
|
|
|
|
async function loadData() {
|
2026-09-09 06:46:27 +03:30
|
|
|
|
try {
|
2026-09-14 23:05:29 +03:30
|
|
|
|
const [list] = await Promise.all([api.listProducts(), memory.init()])
|
|
|
|
|
|
products.value = list
|
2026-09-09 06:46:27 +03:30
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.log(e)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-14 23:05:29 +03:30
|
|
|
|
try {
|
|
|
|
|
|
goldPrice.value = await api.getLatestGoldPrice()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
goldPrice.value = null
|
|
|
|
|
|
}
|
2026-09-16 01:15:16 +03:30
|
|
|
|
}
|
2026-09-14 23:05:29 +03:30
|
|
|
|
|
2026-09-16 01:15:16 +03:30
|
|
|
|
async function onRefresh() {
|
|
|
|
|
|
await loadData()
|
|
|
|
|
|
refreshing.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await loadData()
|
|
|
|
|
|
loading.value = false
|
2026-09-09 06:46:27 +03:30
|
|
|
|
})
|
|
|
|
|
|
</script>
|
2026-09-14 23:05:29 +03:30
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
|
|
|
|
|
.home {
|
|
|
|
|
|
background: @canvas;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__bar {
|
2026-09-16 01:15:16 +03:30
|
|
|
|
position: fixed;
|
2026-09-14 23:05:29 +03:30
|
|
|
|
top: 0;
|
2026-09-16 01:15:16 +03:30
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
2026-09-14 23:05:29 +03:30
|
|
|
|
z-index: 20;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.88);
|
|
|
|
|
|
backdrop-filter: blur(14px);
|
|
|
|
|
|
border-bottom: 1px solid @hairline;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__barInner {
|
|
|
|
|
|
max-width: 768px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
padding: 11px 16px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__logo {
|
|
|
|
|
|
height: 21px;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__iconBtn {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 38px;
|
|
|
|
|
|
height: 38px;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
color: @ink;
|
|
|
|
|
|
background: @hairlineSoft;
|
|
|
|
|
|
transition: background 0.18s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__badge {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -3px;
|
|
|
|
|
|
left: -3px;
|
|
|
|
|
|
min-width: 17px;
|
|
|
|
|
|
height: 17px;
|
|
|
|
|
|
padding: 0 4px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
background: @wine;
|
|
|
|
|
|
border: 2px solid #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__body {
|
2026-09-16 01:15:16 +03:30
|
|
|
|
padding-top: 62px;
|
2026-09-14 23:05:29 +03:30
|
|
|
|
padding-bottom: 90px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__rate {
|
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__hero {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__rail {
|
|
|
|
|
|
margin-top: 18px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__section {
|
|
|
|
|
|
margin-top: 28px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: 11px;
|
|
|
|
|
|
margin-top: 11px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__reel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 11px;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
margin: 0 -16px;
|
|
|
|
|
|
padding: 0 16px;
|
|
|
|
|
|
scroll-snap-type: x proximity;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__reelItem {
|
|
|
|
|
|
flex: 0 0 154px;
|
|
|
|
|
|
scroll-snap-align: start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__empty {
|
|
|
|
|
|
margin-top: 28px;
|
|
|
|
|
|
padding: 34px 20px;
|
|
|
|
|
|
border: 1px dashed @hairline;
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
color: @inkFaint;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home__empty p {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 12.5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__trust {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
gap: 9px;
|
|
|
|
|
|
margin-top: 26px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.home__trustItem {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 7px;
|
|
|
|
|
|
padding: 14px 6px;
|
|
|
|
|
|
border-radius: 14px;
|
|
|
|
|
|
background: @hairlineSoft;
|
|
|
|
|
|
color: @inkMuted;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|