Add Info panel for catalog with tabbed interface
Implemented Info mode для детального просмотра объектов каталога (hub/supplier/offer) с навигацией между связанными объектами. Новые компоненты: - InfoPanel.vue - панель с детальной информацией и табами для связанных объектов - useCatalogInfo.ts - composable для управления Info state и загрузки данных Изменения: - useCatalogSearch.ts - добавлен infoId state и функции openInfo/closeInfo - catalog/index.vue - интеграция InfoPanel, обработчики событий, relatedPoints для карты - CatalogPage.vue - проброс relatedPoints в CatalogMap - CatalogMap.vue - related points layer (cyan circles) для отображения связанных объектов Флоу: 1. Клик на чип → Selection → Выбор → Info открывается 2. Клик на карту → Info открывается напрямую 3. В Info показываются табы со связанными объектами (top-12) 4. Клик на связанный объект → навигация к его Info 5. Кнопка "Добавить в фильтр" - добавляет объект в chips URL sharing: ?info=type:uuid для шаринга ссылок Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,11 +10,12 @@
|
||||
:hovered-id="hoveredItemId"
|
||||
:show-panel="showPanel"
|
||||
:filter-by-bounds="filterByBounds"
|
||||
:related-points="relatedPoints"
|
||||
@select="onMapSelect"
|
||||
@bounds-change="onBoundsChange"
|
||||
@update:filter-by-bounds="filterByBounds = $event"
|
||||
>
|
||||
<!-- Panel slot - shows selection list OR quote results -->
|
||||
<!-- Panel slot - shows selection list OR info OR quote results -->
|
||||
<template #panel>
|
||||
<!-- Selection mode: show list for picking product/hub/supplier -->
|
||||
<SelectionPanel
|
||||
@@ -32,6 +33,24 @@
|
||||
@hover="onHoverItem"
|
||||
/>
|
||||
|
||||
<!-- Info mode: show detailed info about selected entity -->
|
||||
<InfoPanel
|
||||
v-else-if="infoId"
|
||||
:entity-type="infoId.type"
|
||||
:entity-id="infoId.uuid"
|
||||
:entity="entity"
|
||||
:related-products="relatedProducts"
|
||||
:related-hubs="relatedHubs"
|
||||
:related-suppliers="relatedSuppliers"
|
||||
:related-offers="relatedOffers"
|
||||
:selected-product="selectedProduct"
|
||||
:loading="infoLoading"
|
||||
@close="onInfoClose"
|
||||
@add-to-filter="onInfoAddToFilter"
|
||||
@open-info="onInfoOpenRelated"
|
||||
@select-product="selectInfoProduct"
|
||||
/>
|
||||
|
||||
<!-- Quote results: show offers after search -->
|
||||
<QuotePanel
|
||||
v-else-if="showQuoteResults"
|
||||
@@ -85,6 +104,7 @@ const onBoundsChange = (bounds: MapBounds) => {
|
||||
const {
|
||||
catalogMode,
|
||||
selectMode,
|
||||
infoId,
|
||||
productId,
|
||||
supplierId,
|
||||
hubId,
|
||||
@@ -94,9 +114,25 @@ const {
|
||||
entityColors,
|
||||
selectItem,
|
||||
cancelSelect,
|
||||
openInfo,
|
||||
closeInfo,
|
||||
setLabel
|
||||
} = useCatalogSearch()
|
||||
|
||||
// Info panel composable
|
||||
const {
|
||||
entity,
|
||||
relatedProducts,
|
||||
relatedHubs,
|
||||
relatedSuppliers,
|
||||
relatedOffers,
|
||||
selectedProduct,
|
||||
isLoading: infoLoading,
|
||||
loadInfo,
|
||||
selectProduct: selectInfoProduct,
|
||||
clearInfo
|
||||
} = useCatalogInfo()
|
||||
|
||||
// Composables for data (initialize immediately when selectMode changes)
|
||||
const {
|
||||
items: products,
|
||||
@@ -191,6 +227,69 @@ watch([filterByBounds, currentMapBounds], ([enabled, bounds]) => {
|
||||
setProductBoundsFilter(boundsToApply)
|
||||
})
|
||||
|
||||
// Watch infoId to load info data
|
||||
watch(infoId, async (info) => {
|
||||
if (info) {
|
||||
await loadInfo(info.type, info.uuid)
|
||||
} else {
|
||||
clearInfo()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Related points for Info mode (shown on map)
|
||||
const relatedPoints = computed(() => {
|
||||
if (!infoId.value) return []
|
||||
|
||||
const points: Array<{
|
||||
uuid: string
|
||||
name: string
|
||||
latitude: number
|
||||
longitude: number
|
||||
type: 'hub' | 'supplier' | 'offer'
|
||||
}> = []
|
||||
|
||||
// Add hubs
|
||||
relatedHubs.value.forEach(hub => {
|
||||
if (hub.latitude && hub.longitude) {
|
||||
points.push({
|
||||
uuid: hub.uuid,
|
||||
name: hub.name,
|
||||
latitude: hub.latitude,
|
||||
longitude: hub.longitude,
|
||||
type: 'hub'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add suppliers
|
||||
relatedSuppliers.value.forEach(supplier => {
|
||||
if (supplier.latitude && supplier.longitude) {
|
||||
points.push({
|
||||
uuid: supplier.uuid,
|
||||
name: supplier.name,
|
||||
latitude: supplier.latitude,
|
||||
longitude: supplier.longitude,
|
||||
type: 'supplier'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Add offers
|
||||
relatedOffers.value.forEach(offer => {
|
||||
if (offer.latitude && offer.longitude) {
|
||||
points.push({
|
||||
uuid: offer.uuid,
|
||||
name: offer.productName || offer.name,
|
||||
latitude: offer.latitude,
|
||||
longitude: offer.longitude,
|
||||
type: 'offer'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return points
|
||||
})
|
||||
|
||||
// Offers data for quote results
|
||||
const offers = ref<any[]>([])
|
||||
const offersLoading = ref(false)
|
||||
@@ -207,9 +306,9 @@ watch(searchTrigger, () => {
|
||||
// Loading state
|
||||
const isLoading = computed(() => offersLoading.value || selectionLoading.value)
|
||||
|
||||
// Show panel when selecting OR when showing quote results
|
||||
// Show panel when selecting OR when showing info OR when showing quote results
|
||||
const showPanel = computed(() => {
|
||||
return selectMode.value !== null || showQuoteResults.value
|
||||
return selectMode.value !== null || infoId.value !== null || showQuoteResults.value
|
||||
})
|
||||
|
||||
// Cluster node type based on map view mode
|
||||
@@ -268,21 +367,43 @@ const onMapSelect = async (item: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Default behavior for quote mode etc
|
||||
if (catalogMode.value === 'quote') {
|
||||
console.log('Selected from map:', item)
|
||||
}
|
||||
// NEW: Default behavior - open Info directly
|
||||
let infoType: 'hub' | 'supplier' | 'offer'
|
||||
if (mapViewMode.value === 'hubs') infoType = 'hub'
|
||||
else if (mapViewMode.value === 'suppliers') infoType = 'supplier'
|
||||
else infoType = 'offer'
|
||||
|
||||
openInfo(infoType, itemId)
|
||||
setLabel(infoType, itemId, itemName)
|
||||
}
|
||||
|
||||
// Handle selection from SelectionPanel
|
||||
const onSelectItem = (type: string, item: any) => {
|
||||
if (item.uuid && item.name) {
|
||||
selectItem(type, item.uuid, item.name)
|
||||
showQuoteResults.value = false
|
||||
offers.value = []
|
||||
// NEW: Open Info instead of selecting directly
|
||||
openInfo(type as 'hub' | 'supplier' | 'offer', item.uuid)
|
||||
setLabel(type, item.uuid, item.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Info panel events
|
||||
const onInfoClose = () => {
|
||||
closeInfo()
|
||||
clearInfo()
|
||||
}
|
||||
|
||||
const onInfoAddToFilter = () => {
|
||||
if (!infoId.value || !entity.value) return
|
||||
const { type, uuid } = infoId.value
|
||||
const name = entity.value.name
|
||||
selectItem(type, uuid, name)
|
||||
closeInfo()
|
||||
}
|
||||
|
||||
const onInfoOpenRelated = (type: 'hub' | 'supplier' | 'offer', uuid: string) => {
|
||||
openInfo(type, uuid)
|
||||
}
|
||||
|
||||
// Search for offers
|
||||
const onSearch = async () => {
|
||||
if (!canSearch.value) return
|
||||
|
||||
Reference in New Issue
Block a user