change UI & fix bugs
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -442,6 +442,11 @@ input[type='number'] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.desktop-only-flex,
|
||||
.desktop-only-grid {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-hidden {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { MAIN_NAV_ITEMS, isNavItemActive } from '@/utils/navigation'
|
||||
import { MAIN_NAV_ITEMS, isNavItemActive, resolveCartNavPath } from '@/utils/navigation'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -14,9 +14,9 @@ function navigate(path: string, event: Event) {
|
||||
const target = event.currentTarget
|
||||
if (target instanceof HTMLElement) target.blur()
|
||||
|
||||
// «ثبت سفارش»: سبد خالی → دستهبندیها؛ در غیر این صورت سبد خرید
|
||||
if (path === '/cart' && cartStore.itemCount === 0) {
|
||||
router.push('/categories')
|
||||
// «ثبت سفارش»: سبد خالی → کالیتهها؛ در غیر این صورت سبد خرید
|
||||
if (path === '/cart') {
|
||||
router.push(resolveCartNavPath(cartStore.itemCount))
|
||||
return
|
||||
}
|
||||
router.push(path)
|
||||
|
||||
@@ -45,7 +45,7 @@ function goProfile() {
|
||||
function logout() {
|
||||
showUserMenu.value = false
|
||||
authStore.logout()
|
||||
router.replace('/login')
|
||||
router.replace('/welcome')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useCartStore } from '@/stores/cart'
|
||||
import { useOrderStore } from '@/stores/order'
|
||||
import {
|
||||
DESKTOP_NAV_ITEMS,
|
||||
isNavItemActive,
|
||||
resolveCartNavPath,
|
||||
resolveNavPath,
|
||||
} from '@/utils/navigation'
|
||||
import IconHeadset from '@/components/icons/IconHeadset.vue'
|
||||
@@ -14,15 +14,17 @@ import IconQemashMark from '@/components/icons/IconQemashMark.vue'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const cartStore = useCartStore()
|
||||
const orderStore = useOrderStore()
|
||||
|
||||
const cartCount = computed(() => cartStore.itemCount)
|
||||
const draftCheckCount = computed(() => orderStore.draftChecks.length)
|
||||
const activeCompanyId = computed(
|
||||
() => cartStore.selectedCompanyId || cartStore.activeCart?.companyId || null,
|
||||
)
|
||||
|
||||
function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
if (item.name === 'cart') {
|
||||
router.push(resolveCartNavPath(cartStore.itemCount))
|
||||
return
|
||||
}
|
||||
router.push(resolveNavPath(item, activeCompanyId.value))
|
||||
}
|
||||
</script>
|
||||
@@ -53,9 +55,6 @@ function navigate(item: (typeof DESKTOP_NAV_ITEMS)[number]) {
|
||||
<span v-if="item.name === 'cart' && cartCount > 0" class="desktop-sidebar__badge">
|
||||
{{ cartCount }}
|
||||
</span>
|
||||
<span v-if="item.name === 'checks' && draftCheckCount > 0" class="desktop-sidebar__badge">
|
||||
{{ draftCheckCount }}
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
|
||||
+4
-1
@@ -250,7 +250,10 @@ router.beforeEach((to) => {
|
||||
const authed = isAuthenticated()
|
||||
|
||||
if (to.meta.requiresAuth && !authed) {
|
||||
return { name: 'login', query: { redirect: to.fullPath } }
|
||||
if (to.name === 'home' || to.path === '/') {
|
||||
return { name: 'welcome' }
|
||||
}
|
||||
return { name: 'welcome', query: { redirect: to.fullPath } }
|
||||
}
|
||||
|
||||
if (to.meta.guest && authed) {
|
||||
|
||||
@@ -38,6 +38,7 @@ export type BrowseProductDto = {
|
||||
weightGrams?: number
|
||||
weaveType?: string
|
||||
colors?: string[]
|
||||
colorImages?: Array<string | null>
|
||||
tags?: string[]
|
||||
isNew?: boolean
|
||||
subtitle?: string
|
||||
@@ -63,14 +64,42 @@ async function agentRequest<T>(path: string, options?: Parameters<typeof apiRequ
|
||||
export async function fetchCatalogCategories() {
|
||||
try {
|
||||
return await apiRequest<{
|
||||
usage: Array<{ id: string; title: string; subtitle: string; imageUrl?: string }>
|
||||
seasons: Array<{ id: string; title: string; description: string; imageUrl?: string }>
|
||||
usage: Array<{
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
imageUrl?: string
|
||||
showTitle?: boolean
|
||||
showSubtitle?: boolean
|
||||
}>
|
||||
seasons: Array<{
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
imageUrl?: string
|
||||
showTitle?: boolean
|
||||
showDescription?: boolean
|
||||
}>
|
||||
}>('/catalog/categories')
|
||||
} catch {
|
||||
try {
|
||||
return await apiRequest<{
|
||||
usage: Array<{ id: string; title: string; subtitle: string; imageUrl?: string }>
|
||||
seasons: Array<{ id: string; title: string; description: string; imageUrl?: string }>
|
||||
usage: Array<{
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
imageUrl?: string
|
||||
showTitle?: boolean
|
||||
showSubtitle?: boolean
|
||||
}>
|
||||
seasons: Array<{
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
imageUrl?: string
|
||||
showTitle?: boolean
|
||||
showDescription?: boolean
|
||||
}>
|
||||
}>('/products/meta/categories')
|
||||
} catch {
|
||||
return getCatalogCategories()
|
||||
@@ -103,6 +132,9 @@ export type AppBannerDto = {
|
||||
ctaLabel: string
|
||||
ctaLink: string
|
||||
imageUrl: string
|
||||
showTitle?: boolean
|
||||
showSubtitle?: boolean
|
||||
showCta?: boolean
|
||||
}
|
||||
|
||||
export async function fetchBanners(slot?: string) {
|
||||
|
||||
+5
-1
@@ -38,6 +38,7 @@ function mapProduct(p: {
|
||||
weightGrams?: number
|
||||
weaveType?: string
|
||||
colors?: string[]
|
||||
colorImages?: Array<string | null>
|
||||
tags?: string[]
|
||||
isNew?: boolean
|
||||
subtitle?: string
|
||||
@@ -56,9 +57,11 @@ function mapProduct(p: {
|
||||
weightGrams: p.weightGrams,
|
||||
weaveType: p.weaveType,
|
||||
colors: p.colors?.length ? p.colors : ['#6b7280'],
|
||||
colorImages: p.colorImages,
|
||||
tags: p.tags ?? [],
|
||||
isNew: !!p.isNew,
|
||||
imageUrl: p.imageUrl || p.imageUrls?.[0],
|
||||
imageUrl: p.imageUrl || p.colorImages?.find(Boolean) || p.imageUrls?.[0],
|
||||
imageUrls: p.imageUrls,
|
||||
subtitle: p.subtitle,
|
||||
rollMeters: p.rollMeters ?? 50,
|
||||
companyId: p.companyId,
|
||||
@@ -121,6 +124,7 @@ export const useCrmStore = defineStore('crm', () => {
|
||||
weightGrams?: number
|
||||
weaveType?: string
|
||||
colors?: string[]
|
||||
colorImages?: Array<string | null>
|
||||
tags?: string[]
|
||||
isNew?: boolean
|
||||
subtitle?: string
|
||||
|
||||
@@ -63,6 +63,10 @@ export interface ProductCategory {
|
||||
title: string
|
||||
subtitle: string
|
||||
imageUrl?: string
|
||||
icon?: string
|
||||
showTitle?: boolean
|
||||
showSubtitle?: boolean
|
||||
showIcon?: boolean
|
||||
}
|
||||
|
||||
export interface SeasonCategory {
|
||||
@@ -70,6 +74,12 @@ export interface SeasonCategory {
|
||||
title: string
|
||||
description: string
|
||||
imageUrl?: string
|
||||
ctaLabel?: string
|
||||
icon?: string
|
||||
showTitle?: boolean
|
||||
showDescription?: boolean
|
||||
showCta?: boolean
|
||||
showIcon?: boolean
|
||||
}
|
||||
|
||||
export interface CatalogProductCard {
|
||||
@@ -83,6 +93,8 @@ export interface CatalogProductCard {
|
||||
weightGrams?: number
|
||||
weaveType?: string
|
||||
colors: string[]
|
||||
colorImages?: Array<string | null>
|
||||
imageUrls?: string[]
|
||||
tags: string[]
|
||||
isNew: boolean
|
||||
imageUrl?: string
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface CatalogPatternCard {
|
||||
imageUrl?: string
|
||||
colorCount: number
|
||||
colors: string[]
|
||||
colorImages: Array<string | undefined>
|
||||
kind: PatternKind | 'other'
|
||||
items: CatalogProductCard[]
|
||||
}
|
||||
@@ -87,12 +88,20 @@ export function groupCatalogPatterns(products: CatalogProductCard[]): CatalogPat
|
||||
),
|
||||
),
|
||||
]
|
||||
const colorImages = colors.map((name) => {
|
||||
for (const item of items) {
|
||||
const idx = item.colors.indexOf(name)
|
||||
if (idx >= 0 && item.colorImages?.[idx]) return item.colorImages[idx] ?? undefined
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
return {
|
||||
id: items[0].id,
|
||||
name: items[0].name,
|
||||
imageUrl: items.find((item) => item.imageUrl)?.imageUrl,
|
||||
imageUrl: colorImages.find(Boolean) || items.find((item) => item.imageUrl)?.imageUrl,
|
||||
colorCount: Math.max(colors.length, items.length),
|
||||
colors: colors.slice(0, 5),
|
||||
colorImages: colorImages.slice(0, 5),
|
||||
kind: patternKindOf(items[0]),
|
||||
items,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Component } from 'vue'
|
||||
import IconBolt from '@/components/icons/IconBolt.vue'
|
||||
import IconFeather from '@/components/icons/IconFeather.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import IconShoppingBag from '@/components/icons/IconShoppingBag.vue'
|
||||
import IconIdCard from '@/components/icons/IconIdCard.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import IconHeadset from '@/components/icons/IconHeadset.vue'
|
||||
import IconHome from '@/components/icons/IconHome.vue'
|
||||
|
||||
export const CATEGORY_ICON_MAP: Record<string, Component> = {
|
||||
bolt: IconBolt,
|
||||
feather: IconFeather,
|
||||
diamond: IconDiamond,
|
||||
grid: IconLayoutGrid,
|
||||
bag: IconShoppingBag,
|
||||
'id-card': IconIdCard,
|
||||
catalog: IconCatalog,
|
||||
headset: IconHeadset,
|
||||
home: IconHome,
|
||||
}
|
||||
|
||||
export function categoryIconOf(icon?: string, fallback: Component = IconDiamond) {
|
||||
return (icon && CATEGORY_ICON_MAP[icon]) || fallback
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
export function isCssColorToken(value: string) {
|
||||
const token = value.trim()
|
||||
if (!token || /[\u0600-\u06FF]/.test(token)) return false
|
||||
return (
|
||||
token.startsWith('#') ||
|
||||
/^(rgb|hsl)a?\(/i.test(token) ||
|
||||
/^(transparent|currentcolor|black|white|navy|beige|gray|grey|red|blue|green|brown|gold|silver|ivory|khaki|olive|teal|maroon|purple|orange|pink|cyan|magenta|lime|aqua|fuchsia|indigo|violet|tan|wheat|snow)$/i.test(
|
||||
token,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
export function colorSwatchStyle(color: string, imageUrl?: string | null) {
|
||||
if (imageUrl) {
|
||||
return {
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}
|
||||
}
|
||||
if (isCssColorToken(color)) return { background: color }
|
||||
return { background: '#c5cdc9' }
|
||||
}
|
||||
|
||||
export function colorImageAt(
|
||||
colors: string[],
|
||||
colorImages: Array<string | null | undefined> | undefined,
|
||||
imageUrls: string[] | undefined,
|
||||
index: number,
|
||||
fallback?: string,
|
||||
) {
|
||||
return colorImages?.[index] || imageUrls?.[index] || fallback || ''
|
||||
}
|
||||
+16
-22
@@ -4,7 +4,6 @@ import IconCart from '@/components/icons/IconCart.vue'
|
||||
import IconOrders from '@/components/icons/IconOrders.vue'
|
||||
import IconProfile from '@/components/icons/IconProfile.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import IconCheckBook from '@/components/icons/IconCheckBook.vue'
|
||||
import IconPlus from '@/components/icons/IconPlus.vue'
|
||||
|
||||
export interface NavItem {
|
||||
@@ -15,7 +14,7 @@ export interface NavItem {
|
||||
dynamicPath?: boolean
|
||||
}
|
||||
|
||||
/** Mobile bottom navigation — mockup shell with center FAB */
|
||||
/** Mobile bottom navigation — center FAB for سفارش */
|
||||
export const MAIN_NAV_ITEMS: NavItem[] = [
|
||||
{ name: 'home', path: '/', label: 'خانه', icon: IconHome },
|
||||
{ name: 'catalogs', path: '/catalogs', label: 'کالیتهها', icon: IconCatalog },
|
||||
@@ -24,13 +23,15 @@ export const MAIN_NAV_ITEMS: NavItem[] = [
|
||||
{ name: 'profile', path: '/profile', label: 'پروفایل', icon: IconProfile },
|
||||
]
|
||||
|
||||
/** Desktop sidebar — matches mockup */
|
||||
/**
|
||||
* Desktop sidebar — same destinations as mobile.
|
||||
* FAB becomes a normal «سبد / ثبت سفارش» item (better for mouse UI).
|
||||
*/
|
||||
export const DESKTOP_NAV_ITEMS: NavItem[] = [
|
||||
{ name: 'home', path: '/', label: 'خانه', icon: IconHome },
|
||||
{ name: 'ordering', path: '/', label: 'سفارشگذاری', icon: IconCatalog, dynamicPath: true },
|
||||
{ name: 'cart', path: '/cart', label: 'سبد خرید', icon: IconCart },
|
||||
{ name: 'catalogs', path: '/catalogs', label: 'کالیتهها', icon: IconCatalog },
|
||||
{ name: 'cart', path: '/cart', label: 'ثبت سفارش', icon: IconCart },
|
||||
{ name: 'orders', path: '/orders', label: 'سفارشها', icon: IconOrders },
|
||||
{ name: 'checks', path: '/profile/checks', label: 'لیست چکها', icon: IconCheckBook },
|
||||
{ name: 'profile', path: '/profile', label: 'پروفایل', icon: IconProfile },
|
||||
]
|
||||
|
||||
@@ -51,14 +52,6 @@ const CATALOG_PATHS = [
|
||||
]
|
||||
|
||||
export function isNavItemActive(name: string, path: string, currentPath: string): boolean {
|
||||
if (name === 'ordering') {
|
||||
return (
|
||||
currentPath.startsWith('/categories') ||
|
||||
currentPath.startsWith('/products') ||
|
||||
currentPath.startsWith('/product') ||
|
||||
currentPath.startsWith('/catalogs')
|
||||
)
|
||||
}
|
||||
if (name === 'home') {
|
||||
return currentPath === '/'
|
||||
}
|
||||
@@ -67,24 +60,25 @@ export function isNavItemActive(name: string, path: string, currentPath: string)
|
||||
(p) => currentPath === p || currentPath.startsWith(`${p}/`),
|
||||
)
|
||||
}
|
||||
if (name === 'order-fab') {
|
||||
if (name === 'order-fab' || name === 'cart') {
|
||||
return currentPath === '/cart' || currentPath.startsWith('/cart/')
|
||||
}
|
||||
if (name === 'checks') {
|
||||
return currentPath.startsWith('/profile/checks')
|
||||
}
|
||||
if (name === 'profile') {
|
||||
return currentPath === '/profile'
|
||||
return currentPath === '/profile' || currentPath.startsWith('/profile/')
|
||||
}
|
||||
if (path === '/profile') return currentPath === '/profile'
|
||||
if (path === '/') return currentPath === '/'
|
||||
return currentPath.startsWith(path)
|
||||
}
|
||||
|
||||
export function resolveNavPath(
|
||||
item: NavItem,
|
||||
activeCompanyId: string | null | undefined,
|
||||
_activeCompanyId: string | null | undefined,
|
||||
): string {
|
||||
if (item.name === 'ordering') return getOrderingPath(activeCompanyId)
|
||||
return item.path
|
||||
}
|
||||
|
||||
/** Empty cart: mirror mobile FAB → browse catalogs */
|
||||
export function resolveCartNavPath(cartItemCount: number): string {
|
||||
if (cartItemCount === 0) return '/catalogs'
|
||||
return '/cart'
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ const desktopFilterTabs: { id: DesktopOrderFilterId; label: string }[] = [
|
||||
@set-desktop-filter="setDesktopFilter"
|
||||
@select="goToOrder"
|
||||
@set-page="setPage"
|
||||
@new-order="router.push('/categories')"
|
||||
@new-order="router.push('/catalogs')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,7 +24,7 @@ const {
|
||||
} = useCartPage()
|
||||
|
||||
function goToOrdering() {
|
||||
router.push('/categories')
|
||||
router.push('/catalogs')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useRouter } from 'vue-router'
|
||||
import CheckoutStepper from '@/components/layout/CheckoutStepper.vue'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
import IconTrash from '@/components/icons/IconTrash.vue'
|
||||
import IconBuilding from '@/components/icons/IconBuilding.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconOrders from '@/components/icons/IconOrders.vue'
|
||||
import IconClipboardEmpty from '@/components/icons/IconClipboardEmpty.vue'
|
||||
@@ -41,18 +40,18 @@ const router = useRouter()
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
title: 'انتخاب شرکت',
|
||||
desc: 'شروع سفارش از انتخاب شرکت',
|
||||
icon: IconBuilding,
|
||||
title: 'کالیتهها',
|
||||
desc: 'مرور کالیته و طرحها',
|
||||
icon: IconLayoutGrid,
|
||||
tone: 'blue',
|
||||
action: () => emit('selectCompany'),
|
||||
action: () => router.push('/catalogs'),
|
||||
},
|
||||
{
|
||||
title: 'سفارشگذاری',
|
||||
desc: 'مشاهده کاتالوگ و افزودن کالا',
|
||||
icon: IconCart,
|
||||
title: 'دستهبندی',
|
||||
desc: 'مشاهده محصولات بر اساس کاربرد',
|
||||
icon: IconDiamond,
|
||||
tone: 'green',
|
||||
action: () => emit('back'),
|
||||
action: () => router.push('/categories'),
|
||||
},
|
||||
{
|
||||
title: 'سفارشهای من',
|
||||
@@ -92,12 +91,12 @@ const quickLinks = [
|
||||
</p>
|
||||
<div class="cart-empty-card__actions">
|
||||
<button type="button" class="cart-btn cart-btn--primary" @click="emit('selectCompany')">
|
||||
<IconBuilding :size="18" />
|
||||
مشاهده محصولات
|
||||
<IconLayoutGrid :size="18" />
|
||||
مشاهده کالیتهها
|
||||
</button>
|
||||
<button type="button" class="cart-btn cart-btn--outline" @click="emit('back')">
|
||||
<IconChevronLeft :size="16" />
|
||||
بازگشت به سفارشگذاری
|
||||
<button type="button" class="cart-btn cart-btn--outline" @click="router.push('/categories')">
|
||||
<IconDiamond :size="16" />
|
||||
دستهبندی محصولات
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type PatternKind,
|
||||
} from '@/utils/catalogGallery'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { colorSwatchStyle } from '@/utils/colorSwatch'
|
||||
import { demoProductsFor, findMockCalite } from '@/data/caliteCollections'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -375,7 +376,7 @@ async function addAllToInquiry() {
|
||||
<i
|
||||
v-for="(color, idx) in pattern.colors"
|
||||
:key="idx"
|
||||
:style="{ background: color }"
|
||||
:style="colorSwatchStyle(color, pattern.colorImages[idx])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -709,6 +710,8 @@ async function addAllToInquiry() {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.inquiry-bar {
|
||||
|
||||
@@ -43,25 +43,30 @@ function getProductCount(catalogId: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Mobile -->
|
||||
<div class="page catalog-mobile-page mobile-only">
|
||||
<div class="page catalog-list-page">
|
||||
<PageHeader
|
||||
class="mobile-only"
|
||||
:title="company?.name ?? 'کالیتهها'"
|
||||
subtitle="کالیته مورد نظر را انتخاب کنید"
|
||||
show-back
|
||||
centered
|
||||
@back="router.push('/')"
|
||||
@back="router.push('/companies')"
|
||||
/>
|
||||
|
||||
<header class="catalog-list-page__desk-head desktop-only">
|
||||
<h1>{{ company?.name ?? 'کالیتهها' }}</h1>
|
||||
<p>کالیته مورد نظر را انتخاب کنید</p>
|
||||
</header>
|
||||
|
||||
<SearchField
|
||||
v-model="search"
|
||||
placeholder="جستجو بر اساس کد یا نام کالیته..."
|
||||
class="catalog-page__search"
|
||||
class="catalog-list-page__search"
|
||||
/>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav catalog-mobile-scroll">
|
||||
<div class="page__scroll page__scroll--with-nav catalog-list-page__scroll">
|
||||
<PageSkeleton v-if="loading" variant="cards" :rows="5" />
|
||||
<div v-else-if="catalogs.length > 0" class="catalog-mobile-list">
|
||||
<div v-else-if="catalogs.length > 0" class="catalog-list">
|
||||
<button
|
||||
v-for="catalog in catalogs"
|
||||
:key="catalog.id"
|
||||
@@ -84,64 +89,43 @@ function getProductCount(catalogId: string) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop -->
|
||||
<div class="desktop-page catalog-desktop-page desktop-only">
|
||||
<header class="desktop-page__head">
|
||||
<h1 class="desktop-page__title">{{ company?.name ?? 'کالیتهها' }}</h1>
|
||||
<p class="desktop-page__subtitle">کالیته مورد نظر را انتخاب کنید</p>
|
||||
</header>
|
||||
|
||||
<SearchField
|
||||
v-model="search"
|
||||
placeholder="جستجو بر اساس کد یا نام کالیته..."
|
||||
class="catalog-page__search"
|
||||
/>
|
||||
|
||||
<div v-if="catalogs.length > 0" class="desktop-grid-3">
|
||||
<button
|
||||
v-for="catalog in catalogs"
|
||||
:key="catalog.id"
|
||||
type="button"
|
||||
class="catalog-card catalog-card--desktop"
|
||||
@click="openCatalog(catalog.id)"
|
||||
>
|
||||
<span class="catalog-card__icon"><IconCatalog :size="24" /></span>
|
||||
<div class="catalog-card__info">
|
||||
<h3>کالیته کد {{ catalog.code }}</h3>
|
||||
<p>{{ catalog.name }}</p>
|
||||
</div>
|
||||
<span class="catalog-card__count">{{ getProductCount(catalog.id) }} محصول</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<EmptyState
|
||||
v-else
|
||||
:variant="search ? 'search' : 'catalogs'"
|
||||
:message="search ? 'با این جستجو کالیتهای پیدا نشد' : 'برای این شرکت هنوز کالیتهای ثبت نشده است'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.catalog-mobile-page {
|
||||
.catalog-list-page {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
padding: 12px 0 0;
|
||||
}
|
||||
|
||||
.catalog-page__search {
|
||||
.catalog-list-page__desk-head {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.catalog-mobile-scroll {
|
||||
.catalog-list-page__desk-head h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.catalog-list-page__desk-head p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.catalog-list-page__search {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.catalog-list-page__scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.catalog-mobile-list {
|
||||
.catalog-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
@@ -160,13 +144,7 @@ function getProductCount(catalogId: string) {
|
||||
color: var(--color-text);
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.catalog-card--desktop {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 140px;
|
||||
padding: 20px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.catalog-card:hover {
|
||||
@@ -183,10 +161,12 @@ function getProductCount(catalogId: string) {
|
||||
background: var(--color-primary-light);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.catalog-card__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.catalog-card__info h3 {
|
||||
@@ -205,5 +185,30 @@ function getProductCount(catalogId: string) {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.catalog-list-page {
|
||||
background: transparent;
|
||||
padding: 8px 0 28px;
|
||||
}
|
||||
|
||||
.catalog-list-page__scroll {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.catalog-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.catalog-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-height: 148px;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,14 +7,10 @@ import IconBell from '@/components/icons/IconBell.vue'
|
||||
import IconSearch from '@/components/icons/IconSearch.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconShoppingBag from '@/components/icons/IconShoppingBag.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import IconIdCard from '@/components/icons/IconIdCard.vue'
|
||||
import IconHeadset from '@/components/icons/IconHeadset.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import IconBolt from '@/components/icons/IconBolt.vue'
|
||||
import IconFeather from '@/components/icons/IconFeather.vue'
|
||||
import { categoryIconOf } from '@/utils/categoryIcons'
|
||||
import { useGoBack } from '@/composables/useGoBack'
|
||||
import AuthThemeToggle from '@/components/ui/AuthThemeToggle.vue'
|
||||
import PageSkeleton from '@/components/ui/PageSkeleton.vue'
|
||||
@@ -27,16 +23,12 @@ const seasonCategories = ref<SeasonCategory[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const usageIcons: Record<string, unknown> = {
|
||||
shirt: IconIdCard,
|
||||
suit: IconShoppingBag,
|
||||
trousers: IconLayoutGrid,
|
||||
function usageIconKey(item: ProductCategory) {
|
||||
return item.icon || (item.id === 'shirt' ? 'id-card' : item.id === 'suit' ? 'bag' : 'grid')
|
||||
}
|
||||
|
||||
const seasonIcons: Record<string, unknown> = {
|
||||
summer: IconBolt,
|
||||
winter: IconFeather,
|
||||
all_season: IconDiamond,
|
||||
function seasonIconKey(item: SeasonCategory) {
|
||||
return item.icon || (item.id === 'summer' ? 'bolt' : item.id === 'winter' ? 'feather' : 'diamond')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -98,14 +90,14 @@ function openSeason(id: string) {
|
||||
:class="`usage-card__image--${item.id}`"
|
||||
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : undefined"
|
||||
>
|
||||
<span class="usage-card__icon">
|
||||
<component :is="usageIcons[item.id] || IconLayoutGrid" :size="18" />
|
||||
<span v-if="item.showIcon !== false" class="usage-card__icon">
|
||||
<component :is="categoryIconOf(usageIconKey(item), IconCatalog)" :size="18" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="usage-card__body">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span class="usage-card__link">
|
||||
مشاهده محصولات
|
||||
<strong v-if="item.showTitle !== false">{{ item.title }}</strong>
|
||||
<span v-if="item.showSubtitle !== false" class="usage-card__link">
|
||||
{{ item.subtitle || 'مشاهده محصولات' }}
|
||||
<IconChevronLeft :size="14" />
|
||||
</span>
|
||||
</div>
|
||||
@@ -122,26 +114,26 @@ function openSeason(id: string) {
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="season-card"
|
||||
:class="`season-card--${item.id}`"
|
||||
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : undefined"
|
||||
@click="openSeason(item.id)"
|
||||
>
|
||||
<div class="season-card__body">
|
||||
<div class="season-card__title-row">
|
||||
<span class="season-card__icon">
|
||||
<component :is="seasonIcons[item.id] || IconDiamond" :size="16" />
|
||||
</span>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<div class="season-card__content">
|
||||
<div
|
||||
v-if="item.showTitle !== false || item.showIcon !== false"
|
||||
class="season-card__title-row"
|
||||
>
|
||||
<span v-if="item.showIcon !== false" class="season-card__icon">
|
||||
<component :is="categoryIconOf(seasonIconKey(item), IconDiamond)" :size="16" />
|
||||
</span>
|
||||
<strong v-if="item.showTitle !== false">{{ item.title }}</strong>
|
||||
</div>
|
||||
<p>{{ item.description }}</p>
|
||||
<span class="usage-card__link">
|
||||
مشاهده محصولات
|
||||
<p v-if="item.showDescription !== false">{{ item.description }}</p>
|
||||
<span v-if="item.showCta !== false" class="season-card__link">
|
||||
{{ item.ctaLabel || 'مشاهده محصولات' }}
|
||||
<IconChevronLeft :size="14" />
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="season-card__image"
|
||||
:class="`season-card__image--${item.id}`"
|
||||
:style="item.imageUrl ? { backgroundImage: `url(${item.imageUrl})` } : undefined"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
@@ -251,6 +243,13 @@ function openSeason(id: string) {
|
||||
}
|
||||
|
||||
.usage-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
@@ -258,25 +257,36 @@ function openSeason(id: string) {
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: normal;
|
||||
font-family: inherit;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.usage-card__image {
|
||||
position: relative;
|
||||
height: 96px;
|
||||
background: linear-gradient(140deg, #6b7280, #1f2937) center / cover no-repeat;
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
height: auto;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
background-color: #e8eceb;
|
||||
background-image: linear-gradient(140deg, #6b7280, #1f2937);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.usage-card__image--suit {
|
||||
background: linear-gradient(150deg, #2c3038, #111318) center / cover no-repeat;
|
||||
background-image: linear-gradient(150deg, #2c3038, #111318);
|
||||
}
|
||||
|
||||
.usage-card__image--shirt {
|
||||
background: linear-gradient(150deg, #e8eef3, #9aa8b5) center / cover no-repeat;
|
||||
background-image: linear-gradient(150deg, #e8eef3, #9aa8b5);
|
||||
}
|
||||
|
||||
.usage-card__image--trousers {
|
||||
background: linear-gradient(150deg, #1a1d24, #0b0d12) center / cover no-repeat;
|
||||
background-image: linear-gradient(150deg, #1a1d24, #0b0d12);
|
||||
}
|
||||
|
||||
.usage-card__icon {
|
||||
@@ -299,6 +309,7 @@ function openSeason(id: string) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.usage-card__body strong {
|
||||
@@ -310,7 +321,7 @@ function openSeason(id: string) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
color: var(--color-info);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
@@ -321,67 +332,103 @@ function openSeason(id: string) {
|
||||
}
|
||||
|
||||
.season-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 42%;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
min-height: 124px;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
padding: 16px 18px;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
box-shadow: var(--shadow-sm);
|
||||
background-color: #d6c3a8;
|
||||
background-image: linear-gradient(160deg, #d6c3a8, #8b7355);
|
||||
background-size: cover;
|
||||
background-position: center left;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.season-card__image {
|
||||
min-height: 124px;
|
||||
background: linear-gradient(160deg, #d6c3a8, #8b7355) center / cover no-repeat;
|
||||
.season-card--summer {
|
||||
background-image: linear-gradient(160deg, #f3e6c8, #c4a574);
|
||||
}
|
||||
|
||||
.season-card__image--summer {
|
||||
background: linear-gradient(160deg, #f3e6c8, #c4a574) center / cover no-repeat;
|
||||
.season-card--winter {
|
||||
background-image: linear-gradient(160deg, #cfd6de, #7d8b99);
|
||||
}
|
||||
|
||||
.season-card__image--winter {
|
||||
background: linear-gradient(160deg, #cfd6de, #7d8b99) center / cover no-repeat;
|
||||
.season-card--all_season {
|
||||
background-image: linear-gradient(160deg, #efe6d6, #b9a48a);
|
||||
}
|
||||
|
||||
.season-card__image--all_season {
|
||||
background: linear-gradient(160deg, #efe6d6, #b9a48a) center / cover no-repeat;
|
||||
.season-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
to left,
|
||||
color-mix(in srgb, var(--color-surface) 90%, transparent) 0%,
|
||||
color-mix(in srgb, var(--color-surface) 72%, transparent) 38%,
|
||||
color-mix(in srgb, var(--color-surface) 22%, transparent) 68%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.season-card__body {
|
||||
padding: 12px;
|
||||
.season-card__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 58%;
|
||||
}
|
||||
|
||||
.season-card__body strong {
|
||||
.season-card__content strong {
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.season-card__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.season-card__icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 999px;
|
||||
background: var(--color-primary-soft);
|
||||
background: color-mix(in srgb, var(--color-surface) 80%, transparent);
|
||||
color: var(--color-primary);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.season-card__body p {
|
||||
margin: 0 0 8px;
|
||||
.season-card__content p {
|
||||
margin: 0 0 10px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.season-card__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feature-row {
|
||||
@@ -396,7 +443,8 @@ function openSeason(id: string) {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
background: #f3f4f6;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 10px 8px;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useCartStore } from '@/stores/cart'
|
||||
import IconParLogo from '@/components/icons/IconParLogo.vue'
|
||||
import IconFamBaftLogo from '@/components/icons/IconFamBaftLogo.vue'
|
||||
import MobileCompanySelect from '@/views/companies/components/MobileCompanySelect.vue'
|
||||
import DesktopHomeDashboard from '@/views/companies/components/DesktopHomeDashboard.vue'
|
||||
import type { Company } from '@/types'
|
||||
import { useGoBack } from '@/composables/useGoBack'
|
||||
import PageSkeleton from '@/components/ui/PageSkeleton.vue'
|
||||
@@ -54,24 +53,13 @@ function companyLogo(slug: string) {
|
||||
<div v-if="loading" class="page page__scroll">
|
||||
<PageSkeleton variant="cards" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<MobileCompanySelect
|
||||
class="mobile-only"
|
||||
:companies="companies"
|
||||
:card-variant="cardVariant"
|
||||
:company-description="companyDescription"
|
||||
:company-logo="companyLogo"
|
||||
@select="selectCompany"
|
||||
@back="goBack"
|
||||
/>
|
||||
|
||||
<DesktopHomeDashboard
|
||||
class="desktop-only"
|
||||
:companies="companies"
|
||||
:card-variant="cardVariant"
|
||||
:company-description="companyDescription"
|
||||
:company-logo="companyLogo"
|
||||
@select="selectCompany"
|
||||
/>
|
||||
</template>
|
||||
<MobileCompanySelect
|
||||
v-else
|
||||
:companies="companies"
|
||||
:card-variant="cardVariant"
|
||||
:company-description="companyDescription"
|
||||
:company-logo="companyLogo"
|
||||
@select="selectCompany"
|
||||
@back="goBack"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { Company } from '@/types'
|
||||
import type { Component } from 'vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
|
||||
defineProps<{
|
||||
companies: Company[]
|
||||
@@ -17,19 +18,24 @@ defineEmits<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="company-mobile-page">
|
||||
<header class="company-mobile-page__header">
|
||||
<div class="company-mobile-page__nav">
|
||||
<div class="company-select-page">
|
||||
<header class="company-select-page__header mobile-only">
|
||||
<div class="company-select-page__nav">
|
||||
<button type="button" class="nav-btn" aria-label="بازگشت" @click="$emit('back')">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
<h1 class="company-mobile-page__title">انتخاب شرکت</h1>
|
||||
<h1 class="company-select-page__title">انتخاب شرکت</h1>
|
||||
<span class="nav-btn nav-btn--spacer" aria-hidden="true" />
|
||||
</div>
|
||||
<p class="company-mobile-page__subtitle">شرکت مورد نظر خود را انتخاب کنید</p>
|
||||
<p class="company-select-page__subtitle">شرکت مورد نظر خود را انتخاب کنید</p>
|
||||
</header>
|
||||
|
||||
<div class="company-mobile-page__list">
|
||||
<header class="company-select-page__desk-head desktop-only">
|
||||
<h1>انتخاب شرکت</h1>
|
||||
<p>شرکت موردنظر را انتخاب کنید تا کاتالوگ و سفارشگذاری شروع شود</p>
|
||||
</header>
|
||||
|
||||
<div class="company-select-page__list">
|
||||
<button
|
||||
v-for="company in companies"
|
||||
:key="company.id"
|
||||
@@ -43,13 +49,17 @@ defineEmits<{
|
||||
</span>
|
||||
<h2 class="company-brand-card__name">{{ company.name }}</h2>
|
||||
<p class="company-brand-card__desc">{{ companyDescription(company) }}</p>
|
||||
<span class="company-brand-card__cta desktop-only">
|
||||
ورود به کاتالوگها
|
||||
<IconChevronLeft :size="16" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.company-mobile-page {
|
||||
.company-select-page {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -58,12 +68,12 @@ defineEmits<{
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.company-mobile-page__header {
|
||||
.company-select-page__header {
|
||||
padding: 8px var(--page-padding) 12px;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.company-mobile-page__nav {
|
||||
.company-select-page__nav {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 40px;
|
||||
align-items: center;
|
||||
@@ -87,76 +97,121 @@ defineEmits<{
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.company-mobile-page__title {
|
||||
.company-select-page__title {
|
||||
margin: 0;
|
||||
font-size: 1.0625rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.company-mobile-page__subtitle {
|
||||
.company-select-page__subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.company-mobile-page__list {
|
||||
.company-select-page__desk-head {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.company-select-page__desk-head h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.company-select-page__desk-head p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.company-select-page__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-y: auto;
|
||||
padding-inline: var(--page-padding);
|
||||
padding-bottom: 24px;
|
||||
gap: 14px;
|
||||
padding: 12px var(--page-padding) 24px;
|
||||
}
|
||||
|
||||
.company-brand-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
padding: 32px 24px;
|
||||
gap: 8px;
|
||||
padding: 28px 20px 24px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.company-brand-card--par {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow-md);
|
||||
background: linear-gradient(145deg, #1a4a52 0%, #0a353c 70%);
|
||||
}
|
||||
|
||||
.company-brand-card--fam-baft {
|
||||
background: var(--color-brand-warm);
|
||||
color: var(--color-brand-warm-text);
|
||||
box-shadow: var(--shadow-card);
|
||||
border: 1px solid var(--color-border);
|
||||
background: linear-gradient(145deg, #2a5f68 0%, #123f46 70%);
|
||||
}
|
||||
|
||||
.company-brand-card__logo {
|
||||
margin-bottom: 16px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.company-brand-card__name {
|
||||
margin: 0 0 8px;
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.company-brand-card__desc {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.9;
|
||||
font-size: 0.8125rem;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.company-brand-card--par .company-brand-card__desc {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
.company-brand-card__cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 10px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.company-brand-card--fam-baft .company-brand-card__desc {
|
||||
color: var(--color-text-muted);
|
||||
@media (min-width: 768px) {
|
||||
.company-select-page {
|
||||
background: transparent;
|
||||
padding: 8px 0 28px;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.company-select-page__list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.company-brand-card {
|
||||
min-height: 260px;
|
||||
justify-content: center;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.company-brand-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -172,16 +172,20 @@ function cycleStatusFilter() {
|
||||
<div class="customer-card__body">
|
||||
<div class="customer-card__top">
|
||||
<strong>{{ item.name }}</strong>
|
||||
<span
|
||||
class="status"
|
||||
:class="item.status === 'active' ? 'status--ok' : 'status--muted'"
|
||||
>
|
||||
{{ statusLabel(item.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<p dir="ltr">{{ item.phone }}</p>
|
||||
<p>{{ item.phone }}</p>
|
||||
<p>مجموع خرید: {{ formatNumber(item.totalPurchaseToman) }} تومان</p>
|
||||
</div>
|
||||
<span
|
||||
class="status"
|
||||
:class="{
|
||||
'status--ok': item.status === 'active',
|
||||
'status--off': item.status === 'inactive',
|
||||
'status--new': item.status === 'new',
|
||||
}"
|
||||
>
|
||||
{{ statusLabel(item.status) }}
|
||||
</span>
|
||||
<IconChevronLeft :size="18" class="chevron" />
|
||||
</button>
|
||||
|
||||
@@ -339,7 +343,7 @@ function cycleStatusFilter() {
|
||||
|
||||
.customer-card {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr 20px;
|
||||
grid-template-columns: 48px 1fr auto 18px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
border: none;
|
||||
@@ -349,6 +353,7 @@ function cycleStatusFilter() {
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-sm);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.customer-card__avatar {
|
||||
@@ -375,23 +380,33 @@ function cycleStatusFilter() {
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 0.72rem;
|
||||
padding: 2px 8px;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status--ok {
|
||||
background: var(--color-primary-soft);
|
||||
color: var(--color-primary);
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.status--muted {
|
||||
background: #eef0ef;
|
||||
color: var(--color-text-muted);
|
||||
.status--off {
|
||||
background: #f5f5f5;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.status--new {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.promo {
|
||||
|
||||
+305
-56
@@ -7,12 +7,12 @@ import { useFavoritesStore } from '@/stores/favorites'
|
||||
import { fetchAgentProfile, fetchBanners, type AppBannerDto } from '@/services/agentApi'
|
||||
import IconBell from '@/components/icons/IconBell.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconMedal from '@/components/icons/IconMedal.vue'
|
||||
import levelMedalIcon from '@/assets/images/level-medal.webp'
|
||||
import quickClubCrownIcon from '@/assets/images/quick-club-crown.webp'
|
||||
import quickCategoriesGridIcon from '@/assets/images/quick-categories-grid.webp'
|
||||
import quickCatalogRequestIcon from '@/assets/images/quick-catalog-request.webp'
|
||||
import quickNewProductsIcon from '@/assets/images/quick-new-products.webp'
|
||||
import IconHeart from '@/components/icons/IconHeart.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
import IconDiamond from '@/components/icons/IconDiamond.vue'
|
||||
import IconLayoutGrid from '@/components/icons/IconLayoutGrid.vue'
|
||||
import IconProfile from '@/components/icons/IconProfile.vue'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import type { CatalogProductCard } from '@/types/crm'
|
||||
@@ -38,18 +38,26 @@ const agentLevel = ref<{
|
||||
progressPercent: number
|
||||
} | null>(null)
|
||||
|
||||
const customerActions: Array<{ id: string; label: string; to: string; icon: Component }> = [
|
||||
{ id: 'qa-club', label: 'باشگاه مشتریان', to: '/profile', icon: IconProfile },
|
||||
{ id: 'qa-cat', label: 'دسته بندی', to: '/categories', icon: IconLayoutGrid },
|
||||
{ id: 'qa-new', label: 'محصولات جدید', to: '/products', icon: IconDiamond },
|
||||
{ id: 'qa-swatch', label: 'درخواست کالیته', to: '/catalog-request', icon: IconCatalog },
|
||||
type QuickAction = {
|
||||
id: string
|
||||
label: string
|
||||
to: string
|
||||
icon?: Component
|
||||
image?: string
|
||||
}
|
||||
|
||||
const customerActions: QuickAction[] = [
|
||||
{ id: 'qa-club', label: 'باشگاه مشتریان', to: '/profile', image: quickClubCrownIcon },
|
||||
{ id: 'qa-cat', label: 'دسته بندی', to: '/categories', image: quickCategoriesGridIcon },
|
||||
{ id: 'qa-new', label: 'محصولات جدید', to: '/products', image: quickNewProductsIcon },
|
||||
{ id: 'qa-swatch', label: 'درخواست کالیته', to: '/catalog-request', image: quickCatalogRequestIcon },
|
||||
]
|
||||
|
||||
const agentActions: Array<{ id: string; label: string; to: string; icon: Component }> = [
|
||||
{ id: 'qa-customers', label: 'مشتریان من', to: '/customers', icon: IconProfile },
|
||||
{ id: 'qa-reminders', label: 'یادآورها', to: '/reminders', icon: IconBell },
|
||||
{ id: 'qa-cat', label: 'دسته بندی', to: '/categories', icon: IconLayoutGrid },
|
||||
{ id: 'qa-swatch', label: 'درخواست کالیته', to: '/catalog-request', icon: IconCatalog },
|
||||
const agentActions: QuickAction[] = [
|
||||
{ id: 'qa-customers', label: 'باشگاه مشتریان', to: '/customers', image: quickClubCrownIcon },
|
||||
{ id: 'qa-new', label: 'محصولات جدید', to: '/products', image: quickNewProductsIcon },
|
||||
{ id: 'qa-cat', label: 'دسته بندی', to: '/categories', image: quickCategoriesGridIcon },
|
||||
{ id: 'qa-swatch', label: 'درخواست کالیته', to: '/catalog-request', image: quickCatalogRequestIcon },
|
||||
]
|
||||
|
||||
const actions = computed(() => (auth.isAgent ? agentActions : customerActions))
|
||||
@@ -62,26 +70,42 @@ const FALLBACK_BANNER: AppBannerDto = {
|
||||
ctaLabel: 'مشاهده کالکشن',
|
||||
ctaLink: '/categories',
|
||||
imageUrl: '',
|
||||
showTitle: true,
|
||||
showSubtitle: true,
|
||||
showCta: true,
|
||||
}
|
||||
|
||||
const banners = ref<AppBannerDto[]>([FALLBACK_BANNER])
|
||||
const bannerIndex = ref(0)
|
||||
const heroAnim = ref<'hero-next' | 'hero-prev'>('hero-next')
|
||||
let bannerTimer: number | undefined
|
||||
|
||||
const banner = computed(() => banners.value[bannerIndex.value] ?? FALLBACK_BANNER)
|
||||
|
||||
const heroStyle = computed(() => {
|
||||
const image = banner.value.imageUrl
|
||||
if (!image) return undefined
|
||||
return { '--hero-image': `url("${image}")` }
|
||||
})
|
||||
function slideStyle(item: AppBannerDto) {
|
||||
if (!item.imageUrl) return undefined
|
||||
return { '--hero-image': `url("${item.imageUrl}")` }
|
||||
}
|
||||
|
||||
function moveBanner(index: number, dir?: 1 | -1) {
|
||||
const len = banners.value.length
|
||||
if (!len) return
|
||||
const next = ((index % len) + len) % len
|
||||
if (next === bannerIndex.value) return
|
||||
if (dir === 1) heroAnim.value = 'hero-next'
|
||||
else if (dir === -1) heroAnim.value = 'hero-prev'
|
||||
else if (bannerIndex.value === len - 1 && next === 0) heroAnim.value = 'hero-next'
|
||||
else if (bannerIndex.value === 0 && next === len - 1) heroAnim.value = 'hero-prev'
|
||||
else heroAnim.value = next > bannerIndex.value ? 'hero-next' : 'hero-prev'
|
||||
bannerIndex.value = next
|
||||
}
|
||||
|
||||
function startBannerRotation() {
|
||||
stopBannerRotation()
|
||||
if (banners.value.length < 2) return
|
||||
bannerTimer = window.setInterval(() => {
|
||||
bannerIndex.value = (bannerIndex.value + 1) % banners.value.length
|
||||
}, 5000)
|
||||
moveBanner(bannerIndex.value + 1, 1)
|
||||
}, 5500)
|
||||
}
|
||||
|
||||
function stopBannerRotation() {
|
||||
@@ -90,7 +114,7 @@ function stopBannerRotation() {
|
||||
}
|
||||
|
||||
function setBanner(index: number) {
|
||||
bannerIndex.value = index
|
||||
moveBanner(index)
|
||||
startBannerRotation()
|
||||
}
|
||||
|
||||
@@ -166,7 +190,7 @@ async function toggleFavorite(productId: string) {
|
||||
|
||||
<template>
|
||||
<div class="page home-page">
|
||||
<header class="home-header">
|
||||
<header class="home-header mobile-only">
|
||||
<button type="button" class="icon-btn" aria-label="بازگشت" @click="goBack">
|
||||
<IconChevronBack :size="20" />
|
||||
</button>
|
||||
@@ -192,29 +216,45 @@ async function toggleFavorite(productId: string) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<header class="home-desk-head desktop-only">
|
||||
<div>
|
||||
<h1 class="home-desk-head__title">خانه</h1>
|
||||
<p class="home-desk-head__sub">
|
||||
سلام {{ displayFirst }} {{ displayLast }} — خوش آمدید
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="page__scroll page__scroll--with-nav home-scroll">
|
||||
<p v-if="error" class="home-error">{{ error }}</p>
|
||||
<PageSkeleton v-else-if="loading" variant="home" />
|
||||
<template v-else>
|
||||
<section
|
||||
class="hero-banner"
|
||||
:aria-label="banner.title"
|
||||
:style="heroStyle"
|
||||
>
|
||||
<div class="hero-banner__content">
|
||||
<h2 class="hero-banner__title">
|
||||
{{ banner.title }}
|
||||
<span v-if="banner.subtitle">{{ banner.subtitle }}</span>
|
||||
</h2>
|
||||
<button
|
||||
v-if="banner.ctaLabel"
|
||||
type="button"
|
||||
class="hero-banner__cta"
|
||||
@click="goBanner"
|
||||
>
|
||||
{{ banner.ctaLabel }}
|
||||
</button>
|
||||
</div>
|
||||
<section class="hero-banner" :aria-label="banner.title">
|
||||
<TransitionGroup :name="heroAnim" tag="div" class="hero-banner__stage">
|
||||
<article :key="banner.id" class="hero-slide" :style="slideStyle(banner)">
|
||||
<div class="hero-slide__media" aria-hidden="true" />
|
||||
<div class="hero-banner__content">
|
||||
<h2 v-if="banner.showTitle !== false" class="hero-banner__title">
|
||||
{{ banner.title }}
|
||||
<span v-if="banner.showSubtitle !== false && banner.subtitle">{{ banner.subtitle }}</span>
|
||||
</h2>
|
||||
<p
|
||||
v-else-if="banner.showSubtitle !== false && banner.subtitle"
|
||||
class="hero-banner__title"
|
||||
>
|
||||
<span>{{ banner.subtitle }}</span>
|
||||
</p>
|
||||
<button
|
||||
v-if="banner.showCta !== false && banner.ctaLabel"
|
||||
type="button"
|
||||
class="hero-banner__cta"
|
||||
@click="goBanner"
|
||||
>
|
||||
{{ banner.ctaLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
</TransitionGroup>
|
||||
<div v-if="banners.length > 1" class="hero-banner__dots" role="tablist" aria-label="بنرها">
|
||||
<button
|
||||
v-for="(item, index) in banners"
|
||||
@@ -238,7 +278,8 @@ async function toggleFavorite(productId: string) {
|
||||
@click="go(action.to)"
|
||||
>
|
||||
<span class="quick-card__icon" aria-hidden="true">
|
||||
<component :is="action.icon" :size="20" />
|
||||
<img v-if="action.image" class="quick-card__image" :src="action.image" alt="" />
|
||||
<component v-else :is="action.icon" :size="20" />
|
||||
</span>
|
||||
<span class="quick-card__label">{{ action.label }}</span>
|
||||
</button>
|
||||
@@ -247,7 +288,14 @@ async function toggleFavorite(productId: string) {
|
||||
<section class="level-card" aria-label="سطح عضویت">
|
||||
<div class="level-card__top">
|
||||
<div class="level-card__identity">
|
||||
<IconMedal :size="48" />
|
||||
<img
|
||||
class="level-card__medal"
|
||||
:src="levelMedalIcon"
|
||||
alt=""
|
||||
width="48"
|
||||
height="48"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div class="level-card__text">
|
||||
<span class="level-card__eyebrow">سطح شما</span>
|
||||
<strong class="level-card__name">{{ homeLevel.label }}</strong>
|
||||
@@ -415,14 +463,106 @@ async function toggleFavorite(productId: string) {
|
||||
position: relative;
|
||||
min-height: 160px;
|
||||
border-radius: var(--radius-lg);
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.hero-banner__stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.hero-slide {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 20px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-slide__media {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background:
|
||||
linear-gradient(120deg, rgba(var(--color-accent-navy-rgb), 0.72), rgba(var(--color-primary-rgb), 0.45)),
|
||||
var(--hero-image);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
var(--hero-image) center / cover no-repeat;
|
||||
}
|
||||
|
||||
.hero-banner__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
animation: hero-copy 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
.hero-next-enter-active,
|
||||
.hero-next-leave-active,
|
||||
.hero-prev-enter-active,
|
||||
.hero-prev-leave-active {
|
||||
transition:
|
||||
opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.85s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.hero-next-leave-active,
|
||||
.hero-prev-leave-active {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-next-enter-active,
|
||||
.hero-prev-enter-active {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hero-next-enter-from {
|
||||
opacity: 0;
|
||||
transform: translate3d(-16%, 0, 0);
|
||||
}
|
||||
|
||||
.hero-next-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(12%, 0, 0);
|
||||
}
|
||||
|
||||
.hero-prev-enter-from {
|
||||
opacity: 0;
|
||||
transform: translate3d(16%, 0, 0);
|
||||
}
|
||||
|
||||
.hero-prev-leave-to {
|
||||
opacity: 0;
|
||||
transform: translate3d(-12%, 0, 0);
|
||||
}
|
||||
|
||||
@keyframes hero-copy {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 12px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-banner__content,
|
||||
.hero-next-enter-active,
|
||||
.hero-next-leave-active,
|
||||
.hero-prev-enter-active,
|
||||
.hero-prev-leave-active {
|
||||
animation: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.hero-next-enter-from,
|
||||
.hero-next-leave-to,
|
||||
.hero-prev-enter-from,
|
||||
.hero-prev-leave-to {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-banner__title {
|
||||
@@ -456,6 +596,7 @@ async function toggleFavorite(productId: string) {
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.dot {
|
||||
@@ -466,6 +607,7 @@ async function toggleFavorite(productId: string) {
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
cursor: pointer;
|
||||
transition: width 0.35s ease, background 0.35s ease;
|
||||
}
|
||||
|
||||
.dot--active {
|
||||
@@ -493,18 +635,25 @@ async function toggleFavorite(productId: string) {
|
||||
}
|
||||
|
||||
.quick-card__icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 12px;
|
||||
background: var(--color-primary-soft);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--color-primary);
|
||||
background: var(--color-surface);
|
||||
box-shadow: 0 2px 12px rgba(16, 24, 40, 0.06);
|
||||
}
|
||||
|
||||
.quick-card__image {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.quick-card__label {
|
||||
font-weight: bold;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -531,6 +680,14 @@ async function toggleFavorite(productId: string) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.level-card__medal {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.level-card__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -731,4 +888,96 @@ async function toggleFavorite(productId: string) {
|
||||
.product-card__fav--on {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.home-page {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.home-desk-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.home-desk-head__title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1.375rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.home-desk-head__sub {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.home-scroll {
|
||||
gap: 22px;
|
||||
padding: 8px 0 24px;
|
||||
}
|
||||
|
||||
.hero-banner {
|
||||
min-height: 220px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.hero-slide {
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.hero-banner__title {
|
||||
font-size: 1.35rem;
|
||||
max-width: 22rem;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.quick-card {
|
||||
min-height: 96px;
|
||||
padding: 18px 14px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.level-card {
|
||||
padding: 20px 22px;
|
||||
}
|
||||
|
||||
.product-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
overflow: visible;
|
||||
margin-inline: 0;
|
||||
padding-inline: 0;
|
||||
scroll-snap-type: none;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
flex: none;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.product-card__image {
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.product-row {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.hero-banner {
|
||||
min-height: 240px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useCatalogStore } from '@/stores/catalog'
|
||||
import type { CatalogProductCard } from '@/types/crm'
|
||||
import type { Product } from '@/types'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { colorImageAt, colorSwatchStyle } from '@/utils/colorSwatch'
|
||||
import IconChevronBack from '@/components/icons/IconChevronBack.vue'
|
||||
import IconCart from '@/components/icons/IconCart.vue'
|
||||
import IconCatalog from '@/components/icons/IconCatalog.vue'
|
||||
@@ -50,6 +51,12 @@ const adding = ref(false)
|
||||
const showAllSpecs = ref(false)
|
||||
|
||||
const rollMeters = computed(() => product.value?.rollMeters ?? 50)
|
||||
const currentGalleryImage = computed(() => {
|
||||
const p = product.value
|
||||
if (!p) return ''
|
||||
return colorImageAt(p.colors, p.colorImages, p.imageUrls, selectedColor.value, p.imageUrl)
|
||||
})
|
||||
const currentColorName = computed(() => product.value?.colors[selectedColor.value] ?? '')
|
||||
const orderMeters = computed(() => rolls.value * rollMeters.value)
|
||||
const minOrderMeters = computed(() => {
|
||||
const min = product.value?.minMeterage
|
||||
@@ -238,13 +245,13 @@ function shareProduct() {
|
||||
<div
|
||||
class="gallery__main"
|
||||
:style="
|
||||
product.imageUrl
|
||||
currentGalleryImage
|
||||
? {
|
||||
backgroundImage: `url(${product.imageUrl})`,
|
||||
backgroundImage: `url(${currentGalleryImage})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}
|
||||
: { background: product.colors[selectedColor] || '#6b7280' }
|
||||
: colorSwatchStyle(currentColorName)
|
||||
"
|
||||
>
|
||||
<span class="gallery__count">
|
||||
@@ -260,7 +267,7 @@ function shareProduct() {
|
||||
:class="{ 'thumb--active': selectedColor === idx }"
|
||||
@click="selectedColor = idx"
|
||||
>
|
||||
<span class="thumb__swatch" :style="{ background: color }" />
|
||||
<span class="thumb__swatch" :style="colorSwatchStyle(color, product.colorImages?.[idx] || product.imageUrls?.[idx])" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -333,7 +340,7 @@ function shareProduct() {
|
||||
type="button"
|
||||
class="color-dot"
|
||||
:class="{ 'color-dot--active': selectedColor === idx }"
|
||||
:style="{ background: color }"
|
||||
:style="colorSwatchStyle(color, product.colorImages?.[idx] || product.imageUrls?.[idx])"
|
||||
@click="selectedColor = idx"
|
||||
/>
|
||||
</div>
|
||||
@@ -670,6 +677,8 @@ function shareProduct() {
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.color-dot--active {
|
||||
|
||||
@@ -10,6 +10,7 @@ import IconFilter from '@/components/icons/IconFilter.vue'
|
||||
import IconChevronLeft from '@/components/icons/IconChevronLeft.vue'
|
||||
import IconHeart from '@/components/icons/IconHeart.vue'
|
||||
import { useFavoritesStore } from '@/stores/favorites'
|
||||
import { colorSwatchStyle } from '@/utils/colorSwatch'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -247,7 +248,7 @@ function toggleFavorite(productId: string) {
|
||||
v-for="(color, idx) in item.colors.slice(0, 6)"
|
||||
:key="idx"
|
||||
class="swatch"
|
||||
:style="{ background: color }"
|
||||
:style="colorSwatchStyle(color, item.colorImages?.[idx] || item.imageUrls?.[idx])"
|
||||
/>
|
||||
</div>
|
||||
<button type="button" class="details-link" @click.stop="openProduct(item.id)">
|
||||
@@ -456,6 +457,8 @@ function toggleFavorite(productId: string) {
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.details-link {
|
||||
|
||||
@@ -55,7 +55,7 @@ function save() {
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
router.replace('/login')
|
||||
router.replace('/welcome')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import MobileProfilePage from '@/views/profile/components/MobileProfilePage.vue'
|
||||
import DesktopProfilePage from '@/views/profile/components/DesktopProfilePage.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
router.replace('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MobileProfilePage class="mobile-only" />
|
||||
<DesktopProfilePage class="desktop-only" @logout="logout" />
|
||||
<MobileProfilePage />
|
||||
</template>
|
||||
|
||||
@@ -130,7 +130,7 @@ onMounted(async () => {
|
||||
<div class="page__scroll page__scroll--with-nav profile-scroll">
|
||||
<header class="hero">
|
||||
<div class="hero__pin">
|
||||
<div class="hero__top">
|
||||
<div class="hero__top mobile-only">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn"
|
||||
@@ -176,6 +176,17 @@ onMounted(async () => {
|
||||
سطح: {{ levelLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="hero__desk-actions desktop-only-flex">
|
||||
<button type="button" class="hero__desk-btn" @click="router.push('/profile/edit')">
|
||||
ویرایش اطلاعات
|
||||
</button>
|
||||
<button type="button" class="hero__desk-btn" @click="router.push('/profile/checks')">
|
||||
لیست چکها
|
||||
</button>
|
||||
<button type="button" class="hero__desk-btn" @click="router.push('/profile/display')">
|
||||
تنظیمات
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="profile-sheet">
|
||||
@@ -256,7 +267,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<section class="quick-section">
|
||||
<h3 class="section-title">دسترسی سریع</h3>
|
||||
<div class="quick-grid">
|
||||
<button
|
||||
@@ -799,4 +810,138 @@ onMounted(async () => {
|
||||
background: var(--color-elevated);
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.hero__desk-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.profile-page {
|
||||
background: transparent;
|
||||
padding: 8px 0 28px;
|
||||
max-width: var(--content-max-width);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.profile-scroll {
|
||||
overflow: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
border-radius: 20px;
|
||||
padding: 24px 28px;
|
||||
text-align: start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero__pin {
|
||||
position: static;
|
||||
display: block;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.hero__avatar {
|
||||
margin: 0;
|
||||
width: 88px !important;
|
||||
height: 88px !important;
|
||||
}
|
||||
|
||||
.hero__meta {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.hero__desk-btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 8px 14px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.hero__desk-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.hero__desk-actions {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.profile-sheet {
|
||||
margin-top: 18px;
|
||||
border-radius: 20px;
|
||||
padding: 22px 24px 28px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
min-height: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1100px) {
|
||||
.profile-sheet {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 0.8fr;
|
||||
grid-template-areas:
|
||||
'panel panel'
|
||||
'loyalty loyalty'
|
||||
'quick quick'
|
||||
'orders price';
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.profile-sheet > .panel {
|
||||
grid-area: panel;
|
||||
}
|
||||
|
||||
.profile-sheet > .loyalty {
|
||||
grid-area: loyalty;
|
||||
}
|
||||
|
||||
.profile-sheet > .quick-section {
|
||||
grid-area: quick;
|
||||
}
|
||||
|
||||
.profile-sheet > .orders-box {
|
||||
grid-area: orders;
|
||||
}
|
||||
|
||||
.profile-sheet > .price-section {
|
||||
grid-area: price;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user