feat: initial commit

This commit is contained in:
s.esmaeili
2026-09-09 06:46:27 +03:30
commit 0ed6bed028
214 changed files with 20177 additions and 0 deletions
@@ -0,0 +1,71 @@
<!--
Ported from legacy henza_pwa/src/views/search/collection/index.vue.
Filters the one flat product list client-side by collection id (route
param) — no server-side ciny_id query param exists; consistent with the
rest of this port's "small catalog, client-side filtering" approach.
-->
<template>
<div>
<AppHeader>
<nav class="grid grid-cols-10 justify-center items-center">
<div class="w-full col-span-1" @click="router.push({ name: 'search' })">
<Icon icon="vuesax-linear:arrow-right-1" class="ml-2 text-white" size="19" />
</div>
<div class="w-full col-span-8 flex mb-0 justify-center items-center h-[19px]">
<Icon icon="vuesax-linear:money-2" class="ml-2 text-white" size="19" />
<h4 class="bold mb-0 text-white">{{ route.query.page }}</h4>
</div>
<div class="w-full col-span-1"></div>
</nav>
</AppHeader>
<div class="flex flex-col gap-4 m-4 p-2 bg-gray-50 h-[90vh] rounded-lg">
<span class="text-center font-bold mt-2">برای مشاهده جزئیات محصول روی آیتم ضربه بزنید</span>
<Spin :spinning="loadingPage">
<div class="grid grid-cols-4 gap-8 overflow-y-auto mx-4 max-h-[85vh] pb-10">
<CollectionCard v-for="item in collectionProducts" :key="item.id" @click="openDrawer(item.id)" :item="item" />
</div>
</Spin>
</div>
</div>
<Drawer v-model:visible="visible" placement="bottom" :closable="false" class="rtl" height="100vh" :bodyStyle="{ padding: 0, position: 'relative' }">
<SingleProduct v-if="activeProductId" :id="activeProductId" @closeDrawer="closeDrawer" :key="activeProductId" />
</Drawer>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Drawer, Spin } from 'ant-design-vue'
import Icon from '../../components/icon/Icon.vue'
import AppHeader from '../../components/AppHeader.vue'
import CollectionCard from '../../components/product/CollectionCard.vue'
import SingleProduct from '../../components/product/SingleProduct.vue'
import { api, type ProductSummary } from '../../api/client'
const route = useRoute()
const router = useRouter()
const visible = ref(false)
const activeProductId = ref<string | null>(null)
const loadingPage = ref(false)
const allProducts = ref<ProductSummary[]>([])
const collectionProducts = computed(() => allProducts.value.filter((p) => p.collections.some((c) => c.collectionId === route.params.id)))
onMounted(async () => {
loadingPage.value = true
try {
allProducts.value = await api.listProducts()
} finally {
loadingPage.value = false
}
})
function openDrawer(id: string) {
activeProductId.value = id
visible.value = true
}
function closeDrawer() {
visible.value = false
}
</script>
@@ -0,0 +1,118 @@
<!--
Ported from legacy henza_pwa/src/views/search/searchPage.vue. All filtering
happens client-side against the one flat product list (no server-side query
params exist for any of this — consistent with NewestView's own "single
fetch" adaptation from an earlier batch).
-->
<template>
<Spinner :spinning="loadingPage" tip="درحال دریافت اطلاعات">
<header class="w-full z-10 top-0 bg-white shadow-md fixed">
<div class="p-[16px]">
<div class="pb-[10px] flex w-fit justify-center items-center text-primary cursor-pointer" v-if="showFilter" @click="hideFilters()">
<Icon icon="vuesax-linear:arrow-right-1" class="ml-1" size="25" />
بازگشت
</div>
<SearchInput @search="search" place-holder="جستجوی دسته بندی، محصولات و ..." />
</div>
<div class="no-scrollbar flex overflow-x-auto my-[16px] mt-0 pr-[16px]" v-if="!showFilter">
<Button :type="('secondary' as any)" class="btn-secondary ml-[8px] whitespace-nowrap" v-if="filterKeys.length > 0" @click="showFilters">{{ `${filterKeys.length} فیلتر` }}</Button>
<Button :type="('secondary' as any)" class="ml-[8px] whitespace-nowrap" v-else @click="showFilters">اعمال فیلتر</Button>
<div class="flex-1 flex overflow-x-auto no-scrollbar pl-[16px]">
<Button :type="('secondary' as any)" :ghost="!!filters.query.cats" class="last:ml-0 ml-[8px] whitespace-nowrap" @click="setCats(undefined)">همه</Button>
<Button
:type="('secondary' as any)"
:ghost="!(filters.query.cats && filters.query.cats[0] === item.id)"
class="last:ml-0 ml-[8px] whitespace-nowrap"
v-for="item in availableCategories"
:key="item.id"
@click="setCats(item.id)"
>{{ item.title }}</Button
>
</div>
</div>
</header>
<Filters v-model:filterKeys="filterKeys" v-show="showFilter" v-model="filters.query" :products="allProducts" @change="hideFilters" />
<ProductList class="px-[16px] pb-[66px]" padding="146px" v-show="!showFilter" @pull="() => {}" :value="filteredProducts" :ciny="availableCollections" :loading="false" :flag="filteredProducts.length === 0" />
</Spinner>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { Button } from 'ant-design-vue'
import Icon from '../../components/icon/Icon.vue'
import Spinner from '../../components/Spinner.vue'
import SearchInput from '../../components/search/SearchInput.vue'
import Filters from '../../components/search/Filters.vue'
import ProductList from '../../components/product/ProductList.vue'
import { useFiltersStore } from '../../stores/filtersStore'
import { useNavStore } from '../../stores/navStore'
import { api, type ProductSummary } from '../../api/client'
const filters = useFiltersStore()
const nav = useNavStore()
const loadingPage = ref(false)
const allProducts = ref<ProductSummary[]>([])
const showFilter = ref(false)
const filterKeys = ref<string[]>([])
const availableCategories = computed(() => {
const seen = new Map<string, { id: string; title: string }>()
for (const p of allProducts.value) {
if (p.category) seen.set(p.category.id, p.category)
}
return [...seen.values()]
})
const availableCollections = computed(() => {
const seen = new Map<string, { id: string; title: string }>()
for (const p of allProducts.value) {
for (const c of p.collections) seen.set(c.collection.id, { id: c.collection.id, title: c.collection.title })
}
return [...seen.values()]
})
const filteredProducts = computed(() => {
let list = allProducts.value
const q = filters.query
if (q.text) list = list.filter((p) => p.title.includes(q.text!))
if (q.cats && q.cats.length > 0) list = list.filter((p) => p.category && q.cats!.includes(p.category.id))
if (q.brands.length > 0) list = list.filter((p) => p.brand && q.brands.includes(p.brand.id))
if (q.limit_gram.length === 2) {
list = list.filter((p) => p.variants.some((v) => parseFloat(v.rashWeightGrams) >= q.limit_gram[0] && parseFloat(v.rashWeightGrams) <= q.limit_gram[1]))
}
return list
})
async function loadProducts() {
loadingPage.value = true
try {
allProducts.value = await api.listProducts()
} finally {
loadingPage.value = false
}
}
function search(value: string | undefined) {
filters.query.text = value
}
function setCats(id: string | undefined) {
filters.query.cats = id ? [id] : undefined
}
function showFilters() {
showFilter.value = true
nav.setNavVisibility(false)
}
function hideFilters(event?: { resetFilters: boolean }) {
showFilter.value = false
nav.setNavVisibility(true)
if (event?.resetFilters) {
filters.reset()
filterKeys.value = []
}
}
onMounted(loadProducts)
</script>