Simplify catalog UI - remove chips, add drawer for list
All checks were successful
Build Docker Image / build (push) Successful in 3m59s
All checks were successful
Build Docker Image / build (push) Successful in 3m59s
- Remove product/hub chips from QuoteForm.vue (duplicate of toggle) - Add drawer state to useCatalogSearch.ts (isDrawerOpen, selectDrawerItem, applyDrawerFilter) - Convert SelectionPanel to drawer with header, scrollable content, and footer - Add "Список" button to CatalogPage.vue to open drawer - Add "Применить фильтр" button in drawer footer - Add slide animations for drawer (left on desktop, up on mobile) - Update translations: catalog.list, catalog.applyFilter
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<MapPanel>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="font-semibold text-base text-base-content">{{ title }}</h3>
|
||||
<button class="btn btn-ghost btn-xs btn-circle text-base-content/60 hover:text-base-content" @click="emit('close')">
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Header -->
|
||||
<div class="flex-shrink-0 p-4 border-b border-white/10">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-semibold text-base text-white">{{ title }}</h3>
|
||||
<button class="btn btn-ghost btn-xs btn-circle text-white/60 hover:text-white" @click="closeDrawer">
|
||||
<Icon name="lucide:x" size="16" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -11,83 +12,96 @@
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="searchPlaceholder"
|
||||
class="input input-sm w-full bg-white/50 border-base-300/50 text-base-content placeholder:text-base-content/50"
|
||||
class="input input-sm w-full bg-white/10 border-white/20 text-white placeholder:text-white/50"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-md" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="filteredItems.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
||||
<p>{{ $t('catalog.empty.noResults') }}</p>
|
||||
</div>
|
||||
<!-- Content (scrollable) -->
|
||||
<div class="flex-1 overflow-y-auto p-4">
|
||||
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||
<span class="loading loading-spinner loading-md text-white" />
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<!-- Products -->
|
||||
<template v-if="selectMode === 'product'">
|
||||
<div v-else-if="filteredItems.length === 0" class="text-center py-8 text-white/60">
|
||||
<Icon name="lucide:search-x" size="32" class="mb-2" />
|
||||
<p>{{ $t('catalog.empty.noResults') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
<!-- Products -->
|
||||
<template v-if="selectMode === 'product'">
|
||||
<div
|
||||
v-for="(item, index) in filteredItems"
|
||||
:key="item.uuid ?? index"
|
||||
@mouseenter="emit('hover', item.uuid ?? null)"
|
||||
@mouseleave="emit('hover', null)"
|
||||
>
|
||||
<ProductCard
|
||||
:product="item"
|
||||
selectable
|
||||
compact
|
||||
:is-selected="drawerSelectedItem?.uuid === item.uuid"
|
||||
@select="onDrawerSelect(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Hubs -->
|
||||
<template v-else-if="selectMode === 'hub'">
|
||||
<div
|
||||
v-for="(item, index) in filteredItems"
|
||||
:key="item.uuid ?? index"
|
||||
@mouseenter="emit('hover', item.uuid ?? null)"
|
||||
@mouseleave="emit('hover', null)"
|
||||
>
|
||||
<HubCard
|
||||
:hub="item"
|
||||
selectable
|
||||
:is-selected="drawerSelectedItem?.uuid === item.uuid"
|
||||
@select="onDrawerSelect(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Suppliers -->
|
||||
<template v-else-if="selectMode === 'supplier'">
|
||||
<div
|
||||
v-for="(item, index) in filteredItems"
|
||||
:key="item.uuid ?? index"
|
||||
@mouseenter="emit('hover', item.uuid ?? null)"
|
||||
@mouseleave="emit('hover', null)"
|
||||
>
|
||||
<SupplierCard
|
||||
:supplier="item"
|
||||
selectable
|
||||
:is-selected="drawerSelectedItem?.uuid === item.uuid"
|
||||
@select="onDrawerSelect(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Infinite scroll sentinel -->
|
||||
<div
|
||||
v-for="(item, index) in filteredItems"
|
||||
:key="item.uuid ?? index"
|
||||
@mouseenter="emit('hover', item.uuid ?? null)"
|
||||
@mouseleave="emit('hover', null)"
|
||||
v-if="hasMore && !searchQuery"
|
||||
ref="loadMoreSentinel"
|
||||
class="flex items-center justify-center py-4"
|
||||
>
|
||||
<ProductCard
|
||||
:product="item"
|
||||
selectable
|
||||
compact
|
||||
:is-selected="selectedId === item.uuid"
|
||||
@select="onSelect(item)"
|
||||
/>
|
||||
<span v-if="loadingMore" class="loading loading-spinner loading-sm text-white/60" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Hubs -->
|
||||
<template v-else-if="selectMode === 'hub'">
|
||||
<div
|
||||
v-for="(item, index) in filteredItems"
|
||||
:key="item.uuid ?? index"
|
||||
@mouseenter="emit('hover', item.uuid ?? null)"
|
||||
@mouseleave="emit('hover', null)"
|
||||
>
|
||||
<HubCard
|
||||
:hub="item"
|
||||
selectable
|
||||
:is-selected="selectedId === item.uuid"
|
||||
@select="onSelect(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Suppliers -->
|
||||
<template v-else-if="selectMode === 'supplier'">
|
||||
<div
|
||||
v-for="(item, index) in filteredItems"
|
||||
:key="item.uuid ?? index"
|
||||
@mouseenter="emit('hover', item.uuid ?? null)"
|
||||
@mouseleave="emit('hover', null)"
|
||||
>
|
||||
<SupplierCard
|
||||
:supplier="item"
|
||||
selectable
|
||||
:is-selected="selectedId === item.uuid"
|
||||
@select="onSelect(item)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Infinite scroll sentinel -->
|
||||
<div
|
||||
v-if="hasMore && !searchQuery"
|
||||
ref="loadMoreSentinel"
|
||||
class="flex items-center justify-center py-4"
|
||||
>
|
||||
<span v-if="loadingMore" class="loading loading-spinner loading-sm text-white/60" />
|
||||
</div>
|
||||
</div>
|
||||
</MapPanel>
|
||||
|
||||
<!-- Footer with Apply button -->
|
||||
<div class="flex-shrink-0 p-4 border-t border-white/10">
|
||||
<button
|
||||
class="btn btn-primary w-full"
|
||||
:disabled="!drawerSelectedItem"
|
||||
@click="onApplyFilter"
|
||||
>
|
||||
{{ $t('catalog.applyFilter') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -104,21 +118,26 @@ const props = defineProps<{
|
||||
products?: Item[]
|
||||
hubs?: Item[]
|
||||
suppliers?: Item[]
|
||||
selectedId?: string
|
||||
loading?: boolean
|
||||
loadingMore?: boolean
|
||||
hasMore?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select': [type: string, item: Item]
|
||||
'close': []
|
||||
'load-more': []
|
||||
'hover': [uuid: string | null]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Get drawer functions from useCatalogSearch
|
||||
const {
|
||||
drawerSelectedItem,
|
||||
selectDrawerItem,
|
||||
applyDrawerFilter,
|
||||
closeDrawer
|
||||
} = useCatalogSearch()
|
||||
|
||||
const searchQuery = ref('')
|
||||
const loadMoreSentinel = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -187,9 +206,15 @@ const filteredItems = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const onSelect = (item: Item) => {
|
||||
if (props.selectMode && item.uuid) {
|
||||
emit('select', props.selectMode, item)
|
||||
// Select item in drawer (doesn't apply filter yet)
|
||||
const onDrawerSelect = (item: Item) => {
|
||||
if (item.uuid && item.name) {
|
||||
selectDrawerItem(item.uuid, item.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filter and close drawer
|
||||
const onApplyFilter = () => {
|
||||
applyDrawerFilter()
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user