Add entity color scheme and improve map hover effect
All checks were successful
Build Docker Image / build (push) Successful in 3m6s
All checks were successful
Build Docker Image / build (push) Successful in 3m6s
- Add color scheme: product/offer=orange, supplier=blue, hub=green - Remove 'location' filter (same as hub) - Quantity filter appears only after product is selected - Map hover shows 'target' ring effect (outer white ring) - Tokens in header use entity-specific colors
This commit is contained in:
@@ -208,12 +208,7 @@ const initClientClusteringLayers = (map: MapboxMapType) => {
|
|||||||
filter: ['!', ['has', 'point_count']],
|
filter: ['!', ['has', 'point_count']],
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': 12,
|
'circle-radius': 12,
|
||||||
'circle-color': [
|
'circle-color': props.pointColor,
|
||||||
'case',
|
|
||||||
['==', ['get', 'uuid'], props.hoveredItemId || ''],
|
|
||||||
'#facc15', // yellow when hovered
|
|
||||||
props.pointColor
|
|
||||||
],
|
|
||||||
'circle-stroke-width': 3,
|
'circle-stroke-width': 3,
|
||||||
'circle-stroke-color': '#ffffff'
|
'circle-stroke-color': '#ffffff'
|
||||||
}
|
}
|
||||||
@@ -260,6 +255,36 @@ const initClientClusteringLayers = (map: MapboxMapType) => {
|
|||||||
map.on('mouseenter', 'unclustered-point', () => { map.getCanvas().style.cursor = 'pointer' })
|
map.on('mouseenter', 'unclustered-point', () => { map.getCanvas().style.cursor = 'pointer' })
|
||||||
map.on('mouseleave', 'unclustered-point', () => { map.getCanvas().style.cursor = '' })
|
map.on('mouseleave', 'unclustered-point', () => { map.getCanvas().style.cursor = '' })
|
||||||
|
|
||||||
|
// Hovered point layer (on top of everything) - "target" effect with border
|
||||||
|
map.addSource(hoveredSourceId.value, {
|
||||||
|
type: 'geojson',
|
||||||
|
data: hoveredPointGeoJson.value
|
||||||
|
})
|
||||||
|
// Outer ring (white)
|
||||||
|
map.addLayer({
|
||||||
|
id: 'hovered-point-ring',
|
||||||
|
type: 'circle',
|
||||||
|
source: hoveredSourceId.value,
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 20,
|
||||||
|
'circle-color': 'transparent',
|
||||||
|
'circle-stroke-width': 3,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Inner point (same as entity color)
|
||||||
|
map.addLayer({
|
||||||
|
id: 'hovered-point-layer',
|
||||||
|
type: 'circle',
|
||||||
|
source: hoveredSourceId.value,
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 14,
|
||||||
|
'circle-color': props.pointColor,
|
||||||
|
'circle-stroke-width': 3,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Auto-fit bounds to all items
|
// Auto-fit bounds to all items
|
||||||
if (!didFitBounds.value && props.items.length > 0) {
|
if (!didFitBounds.value && props.items.length > 0) {
|
||||||
const bounds = new LngLatBounds()
|
const bounds = new LngLatBounds()
|
||||||
@@ -312,12 +337,7 @@ const initServerClusteringLayers = (map: MapboxMapType) => {
|
|||||||
filter: ['==', ['get', 'count'], 1],
|
filter: ['==', ['get', 'count'], 1],
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': 12,
|
'circle-radius': 12,
|
||||||
'circle-color': [
|
'circle-color': props.pointColor,
|
||||||
'case',
|
|
||||||
['==', ['get', 'id'], props.hoveredItemId || ''],
|
|
||||||
'#facc15', // yellow when hovered
|
|
||||||
props.pointColor
|
|
||||||
],
|
|
||||||
'circle-stroke-width': 3,
|
'circle-stroke-width': 3,
|
||||||
'circle-stroke-color': '#ffffff'
|
'circle-stroke-color': '#ffffff'
|
||||||
}
|
}
|
||||||
@@ -365,18 +385,31 @@ const initServerClusteringLayers = (map: MapboxMapType) => {
|
|||||||
map.on('mouseenter', 'server-points', () => { map.getCanvas().style.cursor = 'pointer' })
|
map.on('mouseenter', 'server-points', () => { map.getCanvas().style.cursor = 'pointer' })
|
||||||
map.on('mouseleave', 'server-points', () => { map.getCanvas().style.cursor = '' })
|
map.on('mouseleave', 'server-points', () => { map.getCanvas().style.cursor = '' })
|
||||||
|
|
||||||
// Hovered point layer (on top of everything)
|
// Hovered point layer (on top of everything) - "target" effect with border
|
||||||
map.addSource(hoveredSourceId.value, {
|
map.addSource(hoveredSourceId.value, {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: hoveredPointGeoJson.value
|
data: hoveredPointGeoJson.value
|
||||||
})
|
})
|
||||||
|
// Outer ring (white)
|
||||||
|
map.addLayer({
|
||||||
|
id: 'hovered-point-ring',
|
||||||
|
type: 'circle',
|
||||||
|
source: hoveredSourceId.value,
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 20,
|
||||||
|
'circle-color': 'transparent',
|
||||||
|
'circle-stroke-width': 3,
|
||||||
|
'circle-stroke-color': '#ffffff'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Inner point (same as entity color)
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: 'hovered-point-layer',
|
id: 'hovered-point-layer',
|
||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: hoveredSourceId.value,
|
source: hoveredSourceId.value,
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': 14,
|
'circle-radius': 14,
|
||||||
'circle-color': '#3b82f6',
|
'circle-color': props.pointColor,
|
||||||
'circle-stroke-width': 3,
|
'circle-stroke-width': 3,
|
||||||
'circle-stroke-color': '#ffffff'
|
'circle-stroke-color': '#ffffff'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
<div
|
<div
|
||||||
v-for="token in activeTokens"
|
v-for="token in activeTokens"
|
||||||
:key="token.type"
|
:key="token.type"
|
||||||
class="badge badge-lg badge-primary gap-1.5 cursor-pointer hover:badge-secondary transition-colors flex-shrink-0"
|
class="badge badge-lg gap-1.5 cursor-pointer hover:opacity-80 transition-all flex-shrink-0 text-white"
|
||||||
|
:style="{ backgroundColor: getTokenColor(token.type) }"
|
||||||
@click.stop="$emit('edit-token', token.type)"
|
@click.stop="$emit('edit-token', token.type)"
|
||||||
>
|
>
|
||||||
<Icon :name="token.icon" size="14" />
|
<Icon :name="token.icon" size="14" />
|
||||||
@@ -204,6 +205,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SelectMode } from '~/composables/useCatalogSearch'
|
import type { SelectMode } from '~/composables/useCatalogSearch'
|
||||||
|
import { entityColors } from '~/composables/useCatalogSearch'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
sessionChecked?: boolean
|
sessionChecked?: boolean
|
||||||
@@ -276,5 +278,9 @@ const selectModeIcon = computed(() => {
|
|||||||
if (props.selectMode === 'hub') return 'lucide:map-pin'
|
if (props.selectMode === 'hub') return 'lucide:map-pin'
|
||||||
return 'lucide:search'
|
return 'lucide:search'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getTokenColor = (type: string) => {
|
||||||
|
return entityColors[type as keyof typeof entityColors] || entityColors.product
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type DisplayMode =
|
|||||||
| 'grid-offers'
|
| 'grid-offers'
|
||||||
|
|
||||||
export interface SearchFilter {
|
export interface SearchFilter {
|
||||||
type: 'product' | 'supplier' | 'hub' | 'location' | 'quantity'
|
type: 'product' | 'supplier' | 'hub' | 'quantity'
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
@@ -22,16 +22,22 @@ export interface SearchState {
|
|||||||
product: { id: string; name: string } | null
|
product: { id: string; name: string } | null
|
||||||
supplier: { id: string; name: string } | null
|
supplier: { id: string; name: string } | null
|
||||||
hub: { id: string; name: string } | null
|
hub: { id: string; name: string } | null
|
||||||
location: { id: string; name: string } | null
|
|
||||||
quantity: string | null
|
quantity: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Color scheme for entity types
|
||||||
|
export const entityColors = {
|
||||||
|
product: '#f97316', // orange
|
||||||
|
supplier: '#3b82f6', // blue
|
||||||
|
hub: '#22c55e', // green
|
||||||
|
offer: '#f97316' // orange (same as product context)
|
||||||
|
} as const
|
||||||
|
|
||||||
// Filter labels cache (to show names instead of UUIDs)
|
// Filter labels cache (to show names instead of UUIDs)
|
||||||
const filterLabels = ref<Record<string, Record<string, string>>>({
|
const filterLabels = ref<Record<string, Record<string, string>>>({
|
||||||
product: {},
|
product: {},
|
||||||
supplier: {},
|
supplier: {},
|
||||||
hub: {},
|
hub: {}
|
||||||
location: {}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useCatalogSearch() {
|
export function useCatalogSearch() {
|
||||||
@@ -51,7 +57,6 @@ export function useCatalogSearch() {
|
|||||||
const productId = computed(() => route.query.product as string | undefined)
|
const productId = computed(() => route.query.product as string | undefined)
|
||||||
const supplierId = computed(() => route.query.supplier as string | undefined)
|
const supplierId = computed(() => route.query.supplier as string | undefined)
|
||||||
const hubId = computed(() => route.query.hub as string | undefined)
|
const hubId = computed(() => route.query.hub as string | undefined)
|
||||||
const locationId = computed(() => route.query.location as string | undefined)
|
|
||||||
const quantity = computed(() => route.query.qty as string | undefined)
|
const quantity = computed(() => route.query.qty as string | undefined)
|
||||||
|
|
||||||
// Get label for a filter (from cache or fallback to ID)
|
// Get label for a filter (from cache or fallback to ID)
|
||||||
@@ -96,14 +101,6 @@ export function useCatalogSearch() {
|
|||||||
icon: 'lucide:map-pin'
|
icon: 'lucide:map-pin'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (locationId.value) {
|
|
||||||
tokens.push({
|
|
||||||
type: 'location',
|
|
||||||
id: locationId.value,
|
|
||||||
label: getLabel('location', locationId.value) || t('catalog.filters.location'),
|
|
||||||
icon: 'lucide:navigation'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (quantity.value) {
|
if (quantity.value) {
|
||||||
tokens.push({
|
tokens.push({
|
||||||
type: 'quantity',
|
type: 'quantity',
|
||||||
@@ -129,10 +126,8 @@ export function useCatalogSearch() {
|
|||||||
if (!hubId.value && selectMode.value !== 'hub') {
|
if (!hubId.value && selectMode.value !== 'hub') {
|
||||||
chips.push({ type: 'hub', label: t('catalog.filters.hub') })
|
chips.push({ type: 'hub', label: t('catalog.filters.hub') })
|
||||||
}
|
}
|
||||||
if (!locationId.value) {
|
// Quantity only available after product is selected
|
||||||
chips.push({ type: 'location', label: t('catalog.filters.location') })
|
if (productId.value && !quantity.value) {
|
||||||
}
|
|
||||||
if (!quantity.value) {
|
|
||||||
chips.push({ type: 'quantity', label: t('catalog.filters.quantity') })
|
chips.push({ type: 'quantity', label: t('catalog.filters.quantity') })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,10 +221,12 @@ export function useCatalogSearch() {
|
|||||||
productId,
|
productId,
|
||||||
supplierId,
|
supplierId,
|
||||||
hubId,
|
hubId,
|
||||||
locationId,
|
|
||||||
quantity,
|
quantity,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
entityColors,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
activeTokens,
|
activeTokens,
|
||||||
availableChips,
|
availableChips,
|
||||||
|
|||||||
@@ -143,11 +143,14 @@ const clusterNodeType = computed(() => {
|
|||||||
return 'logistics'
|
return 'logistics'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Import entity colors
|
||||||
|
const { entityColors } = useCatalogSearch()
|
||||||
|
|
||||||
const mapPointColor = computed(() => {
|
const mapPointColor = computed(() => {
|
||||||
if (cardType.value === 'supplier') return '#3b82f6'
|
if (cardType.value === 'supplier') return entityColors.supplier // blue
|
||||||
if (cardType.value === 'hub') return '#10b981'
|
if (cardType.value === 'hub') return entityColors.hub // green
|
||||||
if (cardType.value === 'offer') return '#22c55e'
|
if (cardType.value === 'offer') return entityColors.offer // orange
|
||||||
return '#f59e0b'
|
return entityColors.product // orange
|
||||||
})
|
})
|
||||||
|
|
||||||
const headerText = computed(() => {
|
const headerText = computed(() => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"product": "Product",
|
"product": "Product",
|
||||||
"supplier": "Supplier",
|
"supplier": "Supplier",
|
||||||
"hub": "Hub",
|
"hub": "Hub",
|
||||||
"location": "Location",
|
|
||||||
"quantity": "Quantity"
|
"quantity": "Quantity"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"product": "Товар",
|
"product": "Товар",
|
||||||
"supplier": "Поставщик",
|
"supplier": "Поставщик",
|
||||||
"hub": "Хаб",
|
"hub": "Хаб",
|
||||||
"location": "Локация",
|
|
||||||
"quantity": "Количество"
|
"quantity": "Количество"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
|||||||
Reference in New Issue
Block a user