alsaze bd4edfdade
All checks were successful
Deploy / build (push) Successful in 55s
карты пвз
2025-11-12 13:19:47 +03:00

193 lines
5.2 KiB
Vue

<template>
<div v-if="!hasCoords" class="p-4 text-center">
Определяем ваше местоположение...
</div>
<YandexMap
v-else
v-model="map"
class="w-full"
:settings="{ location }"
style="height: calc(100dvh - 54px)"
>
<YandexMapDefaultSchemeLayer />
<YandexMapDefaultFeaturesLayer />
<YandexMapClusterer
v-model="clusterer"
:grid-size="64"
zoom-on-cluster-click
@true-bounds="trueBounds = $event"
>
<YandexMapMarker
v-for="pickupPoint in pickupPoints"
:key="pickupPoint.id"
position="top-center left-center"
:settings="{ coordinates: [pickupPoint.position.longitude, pickupPoint.position.latitude] }"
@click="onSelectPoint(pickupPoint)"
>
<div class="marker">
<Icon name="i-lucide-map-pin" class="marker__icon" />
</div>
</YandexMapMarker>
<template #cluster="{ length }">
<div class="cluster fade-in">
{{ length }}
</div>
</template>
</YandexMapClusterer>
<YandexMapControls :settings="{ position: isMobile ? 'bottom left' : 'top left', orientation: 'vertical' }">
<YandexMapControl>
<MapControlFitting v-model:fitting="fitting" />
</YandexMapControl>
</YandexMapControls>
<YandexMapControls v-if="isMobile" :settings="{ position: 'bottom right', orientation: 'vertical' }">
<YandexMapControl>
<MapControlTabs v-model:active-tab="activeTab" />
</YandexMapControl>
</YandexMapControls>
</YandexMap>
</template>
<script setup lang="ts">
import type { LngLatBounds, YMap } from '@yandex/ymaps3-types'
import type { YMapLocationRequest } from '@yandex/ymaps3-types/imperative/YMap'
import type { YMapClusterer } from '@yandex/ymaps3-types/packages/clusterer'
import type { PropType } from 'vue'
import type { PickupPoint } from '~/server/shared/types/yandex_pvz'
import { IPvzMapFittingTabs, IPvzMapTabs } from '#shared/types'
import { useGeolocation, useMediaQuery } from '@vueuse/core'
import { computed, defineEmits, shallowRef } from 'vue'
import {
YandexMap,
YandexMapClusterer,
YandexMapControl,
YandexMapControls,
YandexMapDefaultFeaturesLayer,
YandexMapDefaultSchemeLayer,
YandexMapMarker,
} from 'vue-yandex-maps'
import MapControlFitting from '~/components/MapControlFitting.vue'
import MapControlTabs from '~/components/MapControlTabs.vue'
defineProps<{ pickupPoints: PickupPoint[] }>()
const emit = defineEmits(['update:checkout-pickup-point'])
const checkoutPickupPoint = defineModel('checkoutPickupPoint', { type: Object as PropType<PickupPoint>, default: () => undefined })
const activeTab = defineModel('activeTab', { type: Object as PropType<IPvzMapTabs>, default: () => IPvzMapTabs.MAP })
const fitting = defineModel('fitting', { type: Object as PropType<IPvzMapFittingTabs>, default: () => IPvzMapFittingTabs.ALL })
const { coords } = useGeolocation()
const isMobile = useMediaQuery('(max-width: 1280px)')
const clusterer = shallowRef<YMapClusterer | null>(null)
const trueBounds = ref<LngLatBounds>([[0, 0], [0, 0]])
const map = shallowRef<null | YMap>(null)
const hasCoords = computed(() => coords.value.latitude !== Infinity && coords.value.longitude !== Infinity)
const location = ref<YMapLocationRequest>({
zoom: 2,
})
const onSelectPoint = (pickupPoint: PickupPoint) => {
checkoutPickupPoint.value = pickupPoint
emit('update:checkout-pickup-point', pickupPoint)
}
watch(coords, (newCoords) => {
if (newCoords && hasCoords.value) {
location.value = {
center: [newCoords.longitude, newCoords.latitude],
zoom: 9,
duration: 2500,
}
}
}, { once: true })
watch(() => checkoutPickupPoint.value, (newPickupPoint) => {
if (!newPickupPoint)
return
location.value = {
center: [newPickupPoint.position.longitude, newPickupPoint.position.latitude],
zoom: 18,
duration: 500,
}
})
</script>
<style lang="scss" scoped>
.marker {
position: relative;
background-color: #0f172b;
border: 1px solid #0f172b;
border-radius: 12px;
padding: 6px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
transition:
transform 0.2s ease,
background-color 0.3s ease;
&:hover {
transform: scale(1.1);
background-color: #1e293b;
}
&::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #0f172b;
filter: drop-shadow(0 2px 1px rgba(0, 0, 0, 0.15));
}
&__icon {
color: white;
width: 20px;
height: 20px;
}
&:hover &__icon {
color: var(--ui-primary);
transform: scale(1.1);
}
}
.cluster {
background-color: #0f172b;
color: white;
border-radius: 50%;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
border: 2px solid #fff;
transition:
transform 0.2s ease,
background-color 0.3s ease;
&:hover {
transform: scale(1.1);
background-color: #1e293b;
}
}
</style>