This commit is contained in:
2026-09-13 15:36:48 +03:30
parent d6e424137d
commit d02c27fef4
98 changed files with 1886 additions and 234 deletions
+349 -103
View File
@@ -1,165 +1,411 @@
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import PageHeader from '@/components/layout/PageHeader.vue'
import SearchField from '@/components/ui/SearchField.vue'
import IconSearch from '@/components/icons/IconSearch.vue'
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
import PageSkeleton from '@/components/ui/PageSkeleton.vue'
import { useCatalogStore } from '@/stores/catalog'
import { useCrmStore } from '@/stores/crm'
import { useGoBack } from '@/composables/useGoBack'
import type { Catalog } from '@/types'
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
import type { CatalogProductCard } from '@/types/crm'
import {
buildCatalogGallery,
filterCatalogGallery,
type CatalogGalleryFilter,
} from '@/utils/catalogGallery'
import { formatNumber } from '@/utils/format'
const route = useRoute()
const router = useRouter()
const { goBack } = useGoBack('/')
const catalogStore = useCatalogStore()
const crm = useCrmStore()
const catalogs = ref<Catalog[]>([])
const products = ref<CatalogProductCard[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const searchOpen = ref(false)
const search = ref('')
const filter = ref<CatalogGalleryFilter>('all')
const filters: { id: CatalogGalleryFilter; label: string }[] = [
{ id: 'all', label: 'همه' },
{ id: 'newest', label: 'جدیدترین' },
{ id: 'popular', label: 'محبوب‌ترین' },
{ id: 'season', label: 'بر اساس فصل' },
]
const gallery = computed(() => buildCatalogGallery(catalogs.value, products.value))
const cards = computed(() => filterCatalogGallery(gallery.value, filter.value, search.value))
onMounted(async () => {
try {
const companies = await catalogStore.loadCompanies()
const lists = await Promise.all(companies.map((c) => catalogStore.loadCatalogs(c.id)))
const [lists, browse] = await Promise.all([
Promise.all(companies.map((company) => catalogStore.loadCatalogs(company.id))),
crm.loadBrowseProducts().catch(() => [] as CatalogProductCard[]),
])
catalogs.value = lists.flat()
products.value = browse
const focusId = String(route.query.id ?? '')
if (focusId) {
const hit = catalogs.value.find((c) => c.id === focusId)
if (hit) {
await nextTick()
openCatalog(hit)
}
if (focusId && catalogs.value.some((item) => item.id === focusId)) {
router.replace({ name: 'catalog-detail', params: { catalogId: focusId } })
}
} catch (e) {
error.value = e instanceof Error ? e.message : 'بارگذاری کالیته‌ها ناموفق بود.'
} catch {
catalogs.value = []
} finally {
loading.value = false
}
})
function openCatalog(item: Catalog) {
router.push({
path: '/products',
query: { catalogId: item.id, companyId: item.companyId },
})
function openCatalog(id: string) {
router.push({ name: 'catalog-detail', params: { catalogId: id } })
}
function companyName(companyId: string) {
return catalogStore.getCompany(companyId)?.name ?? 'برند'
function toggleSearch() {
searchOpen.value = !searchOpen.value
if (!searchOpen.value) search.value = ''
}
function metaLine(card: (typeof gallery.value)[number]) {
const extra =
card.season === 'winter'
? 'مناسب زمستان'
: card.season === 'summer'
? 'مناسب تابستان'
: 'مناسب چهار فصل'
return `${formatNumber(card.patternCount)} طرح • ${extra}`
}
</script>
<template>
<div class="page">
<PageHeader title="کالیته‌ها" show-back centered @back="router.push('/')" />
<div class="page__scroll page__scroll--with-nav catalog-hub">
<button type="button" class="hub-card" @click="router.push('/catalog-request')">
<strong>درخواست کالیته فیزیکی</strong>
<span>ثبت اقلام و پیگیری وضعیت</span>
<div class="page calite-hub">
<header class="calite-hub__nav">
<button type="button" class="icon-btn" aria-label="بازگشت" @click="goBack">
<IconChevronBack :size="20" />
</button>
<button type="button" class="hub-card" @click="router.push('/catalog-requests')">
<strong>وضعیت درخواست‌ها</strong>
<span>پیگیری کالیته‌های ثبت‌شده</span>
</button>
<button type="button" class="hub-card" @click="router.push('/categories')">
<strong>مشاهده دسته‌بندی‌ها</strong>
<span>انتخاب بر اساس کاربرد و فصل</span>
</button>
<button type="button" class="hub-card" @click="router.push('/products')">
<strong>همه محصولات</strong>
<span>مشاهده کاتالوگ و ثبت سفارش</span>
</button>
<PageSkeleton v-if="loading" variant="grid" />
<p v-else-if="error">{{ error }}</p>
<template v-else>
<h2 class="section-title">کالیته‌های موجود</h2>
<h1>کالیته‌ها</h1>
<div class="calite-hub__nav-end">
<AuthThemeToggle variant="inline" class="mobile-only" />
<button
v-for="item in catalogs"
type="button"
class="icon-btn"
:aria-label="searchOpen ? 'بستن جستجو' : 'جستجو'"
:aria-pressed="searchOpen"
@click="toggleSearch"
>
<IconSearch :size="20" />
</button>
</div>
</header>
<div class="page__scroll page__scroll--with-nav calite-hub__scroll">
<p class="calite-hub__lead">مجموعه‌ای از بهترین پارچه‌ها، در یک نگاه</p>
<SearchField
v-if="searchOpen"
:model-value="search"
placeholder="جستجوی نام کالیته..."
@update:model-value="search = $event"
/>
<div class="calite-hub__chips" role="tablist" aria-label="مرتب‌سازی کالیته‌ها">
<button
v-for="item in filters"
:key="item.id"
type="button"
class="calite-item"
:class="{ 'calite-item--focus': String(route.query.id ?? '') === item.id }"
@click="openCatalog(item)"
class="calite-chip"
:class="{ 'is-on': filter === item.id }"
@click="filter = item.id"
>
<div class="calite-item__text">
<strong>{{ item.name }}</strong>
<span>کد {{ item.code }}</span>
<em>{{ companyName(item.companyId) }}</em>
</div>
<IconChevronLeft :size="18" color="#9aa3a0" />
{{ item.label }}
</button>
<p v-if="!catalogs.length" class="empty-hint">کالیته‌ای یافت نشد.</p>
</template>
</div>
<PageSkeleton v-if="loading" variant="cards" :rows="4" />
<p v-else-if="!cards.length" class="calite-hub__empty">کالیته‌ای یافت نشد.</p>
<div v-else class="calite-hub__list">
<button
v-for="card in cards"
:key="card.id"
type="button"
class="calite-card"
dir="ltr"
@click="openCatalog(card.id)"
>
<span
class="calite-card__photo"
:style="card.imageUrl ? { backgroundImage: `url(${card.imageUrl})` } : undefined"
/>
<span v-if="card.badge" class="calite-card__badge">
{{ card.badge === 'new' ? 'جدید' : 'پرفروش' }}
</span>
<div class="calite-card__copy" dir="rtl">
<svg class="calite-card__arrow" width="12" height="16" viewBox="0 0 8 12" fill="none"
aria-hidden="true">
<path
d="M1.5 1.5 6 6l-4.5 4.5"
stroke="#fff"
stroke-width="1.4"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div class="mr-4 text-white">
<div class="text-lg font-bold">{{ card.title }}</div>
<div class="text-md">{{ card.subtitle }}</div>
<div class="text-xs mt-4">{{ metaLine(card) }}</div>
</div>
</div>
</button>
</div>
</div>
</div>
</template>
<style scoped>
.catalog-hub {
padding: 12px var(--page-padding) 24px;
display: flex;
flex-direction: column;
gap: 10px;
.calite-hub {
background: #f4f0ed;
color: #2a2622;
}
.section-title {
margin: 8px 0 0;
font-size: 0.95rem;
}
.hub-card,
.calite-item {
border: none;
text-align: start;
background: var(--color-surface);
border-radius: var(--radius-md);
padding: 14px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
cursor: pointer;
width: 100%;
}
.calite-item {
flex-direction: row;
.calite-hub__nav {
display: grid;
grid-template-columns: 40px 1fr auto;
align-items: center;
justify-content: space-between;
padding: 6px 12px 0;
background: #f4f0ed;
}
.calite-hub__nav-end {
display: flex;
align-items: center;
}
.calite-hub__nav h1 {
margin: 0;
text-align: center;
font-size: 1.125rem;
font-weight: 700;
}
.calite-hub__scroll {
padding: 2px 16px 28px;
display: flex;
flex-direction: column;
gap: 12px;
}
.calite-item--focus {
outline: 2px solid var(--color-primary);
.calite-hub__lead {
margin: 0;
text-align: center;
color: #8a847c;
font-size: 0.78rem;
line-height: 1.7;
}
.calite-item__text {
.icon-btn {
width: 40px;
height: 40px;
border: none;
background: transparent;
color: inherit;
display: grid;
place-items: center;
cursor: pointer;
}
.calite-hub__chips {
display: flex;
gap: 8px;
padding: 2px 0 4px;
scrollbar-width: none;
}
.calite-hub__chips::-webkit-scrollbar {
display: none;
}
.calite-chip {
flex-shrink: 0;
height: 36px;
padding: 0 16px;
border: 1px solid #eae6e2;
border-radius: 999px;
background: #eae6e2;
color: #615f5c;
font: inherit;
font-size: 0.78rem;
font-weight: 600;
cursor: pointer;
}
.calite-chip.is-on {
border-color: var(--color-primary);
background: var(--color-primary);
color: #fff;
}
.calite-hub__list {
display: flex;
flex-direction: column;
gap: 12px;
}
.calite-card {
position: relative;
display: block;
width: 100%;
min-height: 132px;
padding: 0;
overflow: hidden;
border: none;
border-radius: 22px;
background: #e7ddd4;
cursor: pointer;
text-align: start;
color: #2c2824;
}
.calite-card__photo {
position: absolute;
inset: 0;
z-index: 0;
background: linear-gradient(160deg, #c4b7a8, #8a7b6c);
background-size: cover;
background-position: center;
}
.calite-card__photo::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent 28%, rgba(231, 221, 212, 0.42) 58%, rgba(231, 221, 212, 0.88) 100%);
}
.calite-card__badge {
position: absolute;
z-index: 2;
top: 12px;
left: 12px;
padding: 5px 12px;
border-radius: 999px;
background: #e7ddd4;
color: #000000;
font-size: 1rem;
font-weight: 500;
line-height: 1.2;
}
.calite-card__copy {
position: relative;
align-items: center;
z-index: 1;
display: flex;
gap: 4px;
min-width: 0;
min-height: 132px;
padding: 16px 16px 16px 42%;
}
.calite-item strong {
display: block;
font-size: 0.95rem;
.calite-card__copy strong {
font-size: 1.0625rem;
font-weight: 700;
line-height: 1.5;
color: var(--color-text);
line-height: 1.4;
color: #2c2824;
}
.hub-card span,
.calite-item span,
.calite-item em {
.calite-card__sub {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 2px;
color: #5c5752;
}
.calite-card__arrow {
flex-shrink: 0;
display: block;
color: var(--color-text-muted);
font-size: 0.8rem;
line-height: 1.5;
font-style: normal;
}
.empty-hint {
color: var(--color-text-muted);
.calite-card__copy em {
font-style: normal;
font-size: 0.75rem;
font-weight: 500;
line-height: 1.45;
color: #5c5752;
}
.calite-card__copy small {
font-size: 0.6875rem;
font-weight: 500;
line-height: 1.45;
color: #7a746c;
}
.calite-hub__empty {
margin: 16px 0 0;
text-align: center;
margin: 12px 0 0;
color: #8a847c;
}
html.dark .calite-hub,
html.dark .calite-hub__nav {
background: #0b0f14;
color: #eef2f8;
}
html.dark .calite-hub__lead,
html.dark .calite-hub__empty {
color: #9aa8bc;
}
html.dark .calite-chip {
border-color: #2a3444;
background: #1a2230;
color: #c5d0de;
}
html.dark .calite-chip.is-on {
border-color: #e8e4df;
background: #e8e4df;
color: #121821;
}
html.dark .calite-card {
background: #1a2230;
color: #eef2f8;
}
html.dark .calite-card__photo {
background: linear-gradient(160deg, #3a4454, #151b24);
}
html.dark .calite-card__photo::after {
background: linear-gradient(
90deg,
transparent 22%,
rgba(11, 15, 20, 0.42) 55%,
rgba(11, 15, 20, 0.88) 100%
);
}
html.dark .calite-card__badge {
background: #e8e4df;
color: #121821;
}
html.dark .calite-card__copy strong,
html.dark .calite-card__sub,
html.dark .calite-card__copy em,
html.dark .calite-card__copy small {
color: #eef2f8;
}
html.dark .calite-card__copy small {
color: #c5d0de;
}
</style>