карты пвз
All checks were successful
Deploy / build (push) Successful in 55s

This commit is contained in:
alsaze 2025-11-12 13:19:47 +03:00
parent e5420edc64
commit bd4edfdade
7 changed files with 120 additions and 72 deletions

View File

@ -11,8 +11,13 @@
</li> </li>
<li class="delivery-info__item"> <li class="delivery-info__item">
<Icon class="delivery-info__icon" name="lucide:shirt" size="xl" /> <Icon
<span class="delivery-info__text">{{ checkoutPickupPoint?.pickup_services?.is_fitting_allowed ? 'с примеркой' : 'без примерки' }}</span> class="delivery-info__icon"
:name="checkoutPickupPoint?.pickup_services?.is_fitting_allowed ? 'lucide:shirt' : 'i-lucide-ban'" size="xl"
/>
<span class="delivery-info__text">
{{ checkoutPickupPoint?.pickup_services?.is_fitting_allowed ? 'с примеркой' : 'без примерки' }}
</span>
</li> </li>
<li class="delivery-info__item"> <li class="delivery-info__item">

View File

@ -24,12 +24,12 @@
v-for="pickupPoint in filteredPoints" v-for="pickupPoint in filteredPoints"
:key="pickupPoint.id" :key="pickupPoint.id"
class="pickup-point-card" class="pickup-point-card"
@click="checkoutPickupPoint = pickupPoint" @click="onSelectPoint(pickupPoint)"
> >
<div class="pickup-point-card__content"> <div class="pickup-point-card__content">
<h3>Yandex</h3> <h3>Yandex</h3>
<p>{{ `${pickupPoint?.address?.street} ${pickupPoint?.address?.house}` }}</p> <p>{{ `${pickupPoint?.address?.street} ${pickupPoint?.address?.house}` }}</p>
<h3>с day month бесплатно</h3> <h3>Как определить стоимость ?</h3>
</div> </div>
<Icon class="pickup-point-card__action" name="lucide:chevron-right" /> <Icon class="pickup-point-card__action" name="lucide:chevron-right" />
</div> </div>
@ -38,13 +38,24 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PropType } from 'vue'
import type { PickupPoint } from '~/server/shared/types/yandex_pvz' import type { PickupPoint } from '~/server/shared/types/yandex_pvz'
import { useCheckout } from '~/composables/useCheckout' import { defineEmits } from 'vue'
defineProps<{ filteredPoints: PickupPoint[] }>() defineProps<{ filteredPoints: PickupPoint[] }>()
const emit = defineEmits(['update:checkout-pickup-point'])
const { checkoutPickupPoint } = useCheckout() const checkoutPickupPoint = defineModel('checkoutPickupPoint', {
const searchTerm = ref('') type: Object as PropType<PickupPoint>,
default: () => undefined,
})
const searchTerm = defineModel('searchTerm', { type: String, default: '' })
const onSelectPoint = (pickupPoint: PickupPoint) => {
checkoutPickupPoint.value = pickupPoint
emit('update:checkout-pickup-point', pickupPoint)
}
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -1,21 +1,30 @@
<template> <template>
<div class="control"> <UTabs v-model="fitting" :content="false" :items="tabs" size="sm" />
<UCheckbox
v-model="modelValue"
size="xl"
label="с примеркой"
/>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const modelValue = defineModel<boolean>({ required: true }) import type { TabsItem } from '@nuxt/ui'
</script> import type { PropType } from 'vue'
import { IPvzMapFittingTabs } from '#shared/types'
import { computed } from 'vue'
<style scoped lang="scss"> const fitting = defineModel('fitting', { type: Object as PropType<IPvzMapFittingTabs>, default: () => IPvzMapFittingTabs.ALL })
.control {
background: var(--ui-bg-elevated); const tabs = computed<TabsItem[]>(() => [
padding: 8px 18px; {
border-radius: 10px; value: IPvzMapFittingTabs.ALL,
} label: fitting.value === IPvzMapFittingTabs.ALL ? 'все' : '',
</style> icon: 'i-lucide-globe',
},
{
value: IPvzMapFittingTabs.ALLOW,
label: fitting.value === IPvzMapFittingTabs.ALLOW ? 'с примеркой' : '',
icon: 'i-lucide-shirt',
},
{
value: IPvzMapFittingTabs.FORBID,
label: fitting.value === IPvzMapFittingTabs.FORBID ? 'без примерки' : '',
icon: 'i-lucide-ban',
},
])
</script>

View File

@ -1,36 +1,27 @@
<template> <template>
<UTabs v-model="activeTab" :content="false" :items="tabs"> <UTabs v-model="activeTab" :content="false" :items="tabs" size="sm" />
<template #map>
{{ activeTab === '0' ? 'Карта' : activeTab === '1' ? 'Список' : 'Другое' }}
</template>
</UTabs>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { TabsItem } from '@nuxt/ui' import type { TabsItem } from '@nuxt/ui'
import type { PropType } from 'vue'
import { IPvzMapTabs } from '#shared/types' import { IPvzMapTabs } from '#shared/types'
import { computed } from 'vue' import { computed } from 'vue'
const activeTab = defineModel('activeTab', { type: String, default: IPvzMapTabs.MAP }) const activeTab = defineModel('activeTab', { type: Object as PropType<IPvzMapTabs>, default: () => IPvzMapTabs.MAP })
const tabs = computed<TabsItem[]>(() => const tabs = computed<TabsItem[]>(() =>
[ [
{ {
value: IPvzMapTabs.MAP, value: IPvzMapTabs.MAP,
label: activeTab.value === 'map' ? 'Карта' : '', label: activeTab.value === IPvzMapTabs.MAP ? 'Карта' : '',
icon: 'lucide:map-pin', icon: 'lucide:map-pin',
slot: 'map' as const,
}, },
{ {
value: IPvzMapTabs.LIST, value: IPvzMapTabs.LIST,
label: activeTab.value === 'list' ? 'Список' : '', label: activeTab.value === IPvzMapTabs.LIST ? 'Список' : '',
icon: 'i-lucide-list', icon: 'i-lucide-list',
slot: 'list' as const,
}, },
], ],
) )
</script> </script>
<style scoped lang="scss">
</style>

View File

@ -24,7 +24,7 @@
:key="pickupPoint.id" :key="pickupPoint.id"
position="top-center left-center" position="top-center left-center"
:settings="{ coordinates: [pickupPoint.position.longitude, pickupPoint.position.latitude] }" :settings="{ coordinates: [pickupPoint.position.longitude, pickupPoint.position.latitude] }"
@click="$emit('update:modelValue', pickupPoint)" @click="onSelectPoint(pickupPoint)"
> >
<div class="marker"> <div class="marker">
<Icon name="i-lucide-map-pin" class="marker__icon" /> <Icon name="i-lucide-map-pin" class="marker__icon" />
@ -38,15 +38,15 @@
</template> </template>
</YandexMapClusterer> </YandexMapClusterer>
<YandexMapControls :settings="{ position: 'bottom left', orientation: 'vertical' }"> <YandexMapControls :settings="{ position: isMobile ? 'bottom left' : 'top left', orientation: 'vertical' }">
<YandexMapControl> <YandexMapControl>
<MapControlFitting v-model="isFitting" /> <MapControlFitting v-model:fitting="fitting" />
</YandexMapControl> </YandexMapControl>
</YandexMapControls> </YandexMapControls>
<YandexMapControls :settings="{ position: 'bottom right', orientation: 'vertical' }"> <YandexMapControls v-if="isMobile" :settings="{ position: 'bottom right', orientation: 'vertical' }">
<YandexMapControl> <YandexMapControl>
<MapControlTabs v-model="activeTab" /> <MapControlTabs v-model:active-tab="activeTab" />
</YandexMapControl> </YandexMapControl>
</YandexMapControls> </YandexMapControls>
</YandexMap> </YandexMap>
@ -56,10 +56,11 @@
import type { LngLatBounds, YMap } from '@yandex/ymaps3-types' import type { LngLatBounds, YMap } from '@yandex/ymaps3-types'
import type { YMapLocationRequest } from '@yandex/ymaps3-types/imperative/YMap' import type { YMapLocationRequest } from '@yandex/ymaps3-types/imperative/YMap'
import type { YMapClusterer } from '@yandex/ymaps3-types/packages/clusterer' import type { YMapClusterer } from '@yandex/ymaps3-types/packages/clusterer'
import type { PropType } from 'vue'
import type { PickupPoint } from '~/server/shared/types/yandex_pvz' import type { PickupPoint } from '~/server/shared/types/yandex_pvz'
import { IPvzMapTabs } from '#shared/types' import { IPvzMapFittingTabs, IPvzMapTabs } from '#shared/types'
import { useGeolocation } from '@vueuse/core' import { useGeolocation, useMediaQuery } from '@vueuse/core'
import { computed, shallowRef } from 'vue' import { computed, defineEmits, shallowRef } from 'vue'
import { import {
YandexMap, YandexMap,
YandexMapClusterer, YandexMapClusterer,
@ -72,26 +73,30 @@ import {
import MapControlFitting from '~/components/MapControlFitting.vue' import MapControlFitting from '~/components/MapControlFitting.vue'
import MapControlTabs from '~/components/MapControlTabs.vue' import MapControlTabs from '~/components/MapControlTabs.vue'
const props = defineProps<{ modelValue: PickupPoint, pickupPoints: PickupPoint[] }>() defineProps<{ pickupPoints: PickupPoint[] }>()
const emit = defineEmits(['update:checkout-pickup-point'])
defineEmits<{ const checkoutPickupPoint = defineModel('checkoutPickupPoint', { type: Object as PropType<PickupPoint>, default: () => undefined })
(e: 'update:modelValue', value: PickupPoint | undefined): void 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 activeTab = defineModel('activeTab', { type: String, default: IPvzMapTabs.MAP })
const isFitting = defineModel('isFitting', { type: Boolean, default: false })
const { coords } = useGeolocation() const { coords } = useGeolocation()
const isMobile = useMediaQuery('(max-width: 1280px)')
const clusterer = shallowRef<YMapClusterer | null>(null) const clusterer = shallowRef<YMapClusterer | null>(null)
const trueBounds = ref<LngLatBounds>([[0, 0], [0, 0]]) const trueBounds = ref<LngLatBounds>([[0, 0], [0, 0]])
const map = shallowRef<null | YMap>(null) const map = shallowRef<null | YMap>(null)
const hasCoords = computed(() => coords.value?.latitude !== Infinity && coords.value?.longitude !== Infinity) const hasCoords = computed(() => coords.value.latitude !== Infinity && coords.value.longitude !== Infinity)
const location = ref<YMapLocationRequest>({ const location = ref<YMapLocationRequest>({
zoom: 2, zoom: 2,
}) })
const onSelectPoint = (pickupPoint: PickupPoint) => {
checkoutPickupPoint.value = pickupPoint
emit('update:checkout-pickup-point', pickupPoint)
}
watch(coords, (newCoords) => { watch(coords, (newCoords) => {
if (newCoords && hasCoords.value) { if (newCoords && hasCoords.value) {
location.value = { location.value = {
@ -102,7 +107,10 @@ watch(coords, (newCoords) => {
} }
}, { once: true }) }, { once: true })
watch(() => props.modelValue, (newPickupPoint) => { watch(() => checkoutPickupPoint.value, (newPickupPoint) => {
if (!newPickupPoint)
return
location.value = { location.value = {
center: [newPickupPoint.position.longitude, newPickupPoint.position.latitude], center: [newPickupPoint.position.longitude, newPickupPoint.position.latitude],
zoom: 18, zoom: 18,
@ -152,6 +160,11 @@ watch(() => props.modelValue, (newPickupPoint) => {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
&:hover &__icon {
color: var(--ui-primary);
transform: scale(1.1);
}
} }
.cluster { .cluster {

View File

@ -4,7 +4,13 @@
<div v-if="!isMobile" class="delivery__sidebar"> <div v-if="!isMobile" class="delivery__sidebar">
<DeliveryInfo v-if="isPickupPointSelected" /> <DeliveryInfo v-if="isPickupPointSelected" />
<DeliverySearch v-else :filtered-points="filteredPoints" /> <DeliverySearch
v-else
v-model:checkout-pickup-point="checkoutPickupPoint"
v-model:search-term="searchTerm"
:filtered-points="filteredPoints"
@update:checkout-pickup-point="updatePoint()"
/>
</div> </div>
<!-- Mobile --> <!-- Mobile -->
@ -28,18 +34,20 @@
<UModal <UModal
v-model:open="openDeliverySearch" v-model:open="openDeliverySearch"
fullscreen fullscreen
:dismissible="false"
:ui="{ :ui="{
body: 'overflow-hidden', body: 'overflow-hidden',
container: 'p-0',
overlay: 'bg-black/50', overlay: 'bg-black/50',
base: 'rounded-none shadow-none',
header: 'hidden', header: 'hidden',
}" }"
> >
<template #body> <template #body>
<DeliverySearch <DeliverySearch
v-if="activeTab === IPvzMapTabs.LIST" v-if="activeTab === IPvzMapTabs.LIST"
v-model:checkout-pickup-point="checkoutPickupPoint"
v-model:search-term="searchTerm"
:filtered-points="filteredPoints" :filtered-points="filteredPoints"
@update:checkout-pickup-point="updatePoint()"
/> />
<UDrawer <UDrawer
@ -62,7 +70,7 @@
<template #footer> <template #footer>
<div class="d-flex flex-row w-full justify-between"> <div class="d-flex flex-row w-full justify-between">
<MapControlFitting v-model="isFitting" /> <MapControlFitting v-model:fitting="fitting" />
<MapControlTabs v-model:active-tab="activeTab" /> <MapControlTabs v-model:active-tab="activeTab" />
</div> </div>
@ -71,19 +79,18 @@
<!-- Desktop Mobile --> <!-- Desktop Mobile -->
<PvzMap <PvzMap
v-model="checkoutPickupPoint" v-model:checkout-pickup-point="checkoutPickupPoint"
v-model:active-tab="activeTab" v-model:active-tab="activeTab"
v-model:is-fitting="isFitting" v-model:fitting="fitting"
:pickup-points="filteredPoints" :pickup-points="filteredPoints"
@update:model-value="updatePoint()" @update:checkout-pickup-point="updatePoint()"
@update:is-fitting="value => isFitting = value"
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PickupPoint, YandexPvzResponse } from '~/server/shared/types/yandex_pvz' import type { PickupPoint, YandexPvzResponse } from '~/server/shared/types/yandex_pvz'
import { IPvzMapTabs } from '#shared/types' import { IPvzMapFittingTabs, IPvzMapTabs } from '#shared/types'
import { useGeolocation, useMediaQuery } from '@vueuse/core' import { useGeolocation, useMediaQuery } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import DeliveryInfo from '~/components/DeliveryInfo.vue' import DeliveryInfo from '~/components/DeliveryInfo.vue'
@ -97,9 +104,9 @@ const open = ref(false)
const isMobile = useMediaQuery('(max-width: 1280px)') const isMobile = useMediaQuery('(max-width: 1280px)')
const yandexPvz = ref<YandexPvzResponse>() const yandexPvz = ref<YandexPvzResponse>()
const searchTerm = ref('') const searchTerm = ref('')
const activeTab = ref(IPvzMapTabs.MAP) const activeTab = ref()
const isFitting = ref() const fitting = ref(IPvzMapFittingTabs.ALL)
const openDeliverySearch = ref(false) const openDeliverySearch = ref()
const waitForCoords = () => const waitForCoords = () =>
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
@ -139,14 +146,21 @@ onMounted(async () => {
yandexPvz.value = yandexPvzApi.value yandexPvz.value = yandexPvzApi.value
}) })
// TODO сделать отдельный компонент UISearch
const filteredPoints = computed<PickupPoint[]>(() => { const filteredPoints = computed<PickupPoint[]>(() => {
if (!searchTerm.value || searchTerm.value === '') { const points = yandexPvz.value?.points || []
return yandexPvz.value?.points || [] const term = searchTerm.value?.toLowerCase() || ''
}
return yandexPvz.value?.points?.filter(point => point?.address?.full_address?.toLowerCase().includes(searchTerm.value.toLowerCase()) return points.filter((point) => {
|| isFitting.value === point.pickup_services.is_fitting_allowed) || [] const matchesSearch = !term || point.address.full_address.toLowerCase().includes(term)
const isAllowed = point.pickup_services.is_fitting_allowed
const matchesFitting
= fitting.value === IPvzMapFittingTabs.ALL
|| (fitting.value === IPvzMapFittingTabs.ALLOW && isAllowed)
|| (fitting.value === IPvzMapFittingTabs.FORBID && !isAllowed)
return matchesSearch && matchesFitting
})
}) })
function updatePoint() { function updatePoint() {

View File

@ -2,3 +2,8 @@ export enum IPvzMapTabs {
MAP = 'map', MAP = 'map',
LIST = 'list', LIST = 'list',
} }
export enum IPvzMapFittingTabs {
ALL = 'all',
ALLOW = 'allow',
FORBID = 'Forbid',
}