Compare commits

..

2 Commits

Author SHA1 Message Date
alsaze
55103d3778 карты пвз
All checks were successful
Deploy / build (push) Successful in 56s
2025-11-11 18:21:20 +03:00
alsaze
742ebb4e74 карты пвз 2025-11-11 18:13:13 +03:00
5 changed files with 185 additions and 77 deletions

View File

@ -0,0 +1,81 @@
<template>
<div class="delivery__search">
<UInput
v-model="searchTerm"
size="xl"
class="w-full"
placeholder="Выберите пункт выдачи"
:ui="{ trailing: 'pe-1' }"
>
<template v-if="searchTerm?.length" #trailing>
<UButton
color="neutral"
variant="link"
size="sm"
icon="i-lucide-circle-x"
aria-label="Clear input"
@click="searchTerm = ''"
/>
</template>
</UInput>
<div class="pickup-point-card__items">
<div
v-for="pickupPoint in filteredPoints"
:key="pickupPoint.id"
class="pickup-point-card"
@click="checkoutPickupPoint = pickupPoint"
>
<div class="pickup-point-card__content">
<h3>Yandex</h3>
<p>{{ `${pickupPoint?.address?.street} ${pickupPoint?.address?.house}` }}</p>
<h3>с day month бесплатно</h3>
</div>
<Icon class="pickup-point-card__action" name="lucide:chevron-right" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { PickupPoint } from '~/server/shared/types/yandex_pvz'
import { useCheckout } from '~/composables/useCheckout'
defineProps<{ filteredPoints: PickupPoint[] }>()
const { checkoutPickupPoint } = useCheckout()
const searchTerm = ref('')
</script>
<style lang="scss">
.pickup-point-card {
position: relative;
width: 100%;
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
cursor: pointer;
&__items {
height: calc(100dvh - 54px - 40px - 24px);
overflow-y: auto;
flex-shrink: 0;
}
&__content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
&__action {
flex-shrink: 0;
width: 16px;
height: 16px;
color: #999;
}
}
</style>

View File

@ -37,10 +37,35 @@
</div> </div>
</template> </template>
</YandexMapClusterer> </YandexMapClusterer>
<YandexMapControls :settings="{ position: 'bottom left', orientation: 'vertical' }">
<YandexMapControl>
<div
class="control"
>
<UCheckbox
v-model="isFitting"
size="xl"
label="с примеркой"
/>
</div>
</YandexMapControl>
</YandexMapControls>
<YandexMapControls :settings="{ position: 'bottom right', orientation: 'vertical' }">
<YandexMapControl>
<UTabs v-model="activeTab" :content="false" :items="tabs">
<template #map>
{{ activeTab === '0' ? 'Карта' : activeTab === '1' ? 'Список' : 'Другое' }}
</template>
</UTabs>
</YandexMapControl>
</YandexMapControls>
</YandexMap> </YandexMap>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'
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'
@ -50,6 +75,8 @@ import { computed, shallowRef } from 'vue'
import { import {
YandexMap, YandexMap,
YandexMapClusterer, YandexMapClusterer,
YandexMapControl,
YandexMapControls,
YandexMapDefaultFeaturesLayer, YandexMapDefaultFeaturesLayer,
YandexMapDefaultSchemeLayer, YandexMapDefaultSchemeLayer,
YandexMapMarker, YandexMapMarker,
@ -57,8 +84,9 @@ import {
const props = defineProps<{ modelValue: PickupPoint, pickupPoints: PickupPoint[] }>() const props = defineProps<{ modelValue: PickupPoint, pickupPoints: PickupPoint[] }>()
defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: PickupPoint | undefined): void (e: 'update:modelValue', value: PickupPoint | undefined): void
(e: 'activeTab', value: string): void
}>() }>()
const { coords } = useGeolocation() const { coords } = useGeolocation()
@ -66,6 +94,25 @@ 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 isFitting = ref(false)
const activeTab = ref('map')
const tabs = computed<TabsItem[]>(() =>
[
{
value: 'map',
label: activeTab.value === 'map' ? 'Карта' : '',
icon: 'lucide:map-pin',
slot: 'map' as const,
},
{
value: 'list',
label: activeTab.value === 'list' ? 'Список' : '',
icon: 'i-lucide-list',
slot: 'list' as const,
},
],
)
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>({
@ -89,6 +136,10 @@ watch(() => props.modelValue, (newPickupPoint) => {
duration: 500, duration: 500,
} }
}) })
watch(() => activeTab.value, (newActiveTab) => {
emit('activeTab', newActiveTab)
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -156,4 +207,10 @@ watch(() => props.modelValue, (newPickupPoint) => {
background-color: #1e293b; background-color: #1e293b;
} }
} }
.control {
background: var(--ui-bg-elevated);
padding: 8px 18px;
border-radius: 10px;
}
</style> </style>

View File

@ -1,9 +1,10 @@
import type { PickupPoint } from '~/server/shared/types/yandex_pvz' import type { PickupPoint } from '~/server/shared/types/yandex_pvz'
import { createSharedComposable, useStorage } from '@vueuse/core' import { createSharedComposable, useMediaQuery, useStorage } from '@vueuse/core'
export const useCheckout = createSharedComposable(() => { export const useCheckout = createSharedComposable(() => {
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const isMobile = useMediaQuery('(max-width: 1280px)')
const checkoutPickupPoint = useStorage<PickupPoint | undefined>( const checkoutPickupPoint = useStorage<PickupPoint | undefined>(
'checkout-pickupPoint', 'checkout-pickupPoint',
@ -32,15 +33,15 @@ export const useCheckout = createSharedComposable(() => {
const checkoutSteps = [ const checkoutSteps = [
{ {
step: 1, step: 1,
title: 'delivery', title: isMobile.value ? 'mobileDelivery' : 'delivery',
}, },
{ {
step: 2, step: 2,
title: 'contacts', title: isMobile.value ? 'mobileContacts' : 'contacts',
}, },
{ {
step: 3, step: 3,
title: 'summary', title: isMobile.value ? 'mobileSummary' : 'summary',
}, },
] ]
@ -48,7 +49,7 @@ export const useCheckout = createSharedComposable(() => {
= ref(checkoutSteps.find(value => value.title === route.path.split('/').pop()) || checkoutSteps[0]) = ref(checkoutSteps.find(value => value.title === route.path.split('/').pop()) || checkoutSteps[0])
function previewStep() { function previewStep() {
if (isPickupPointSelected.value) { if (isPickupPointSelected.value && !isMobile.value) {
setCheckoutPickupPoint(undefined) setCheckoutPickupPoint(undefined)
return return
} }

View File

@ -11,7 +11,10 @@
}, },
"checkoutSteps": { "checkoutSteps": {
"delivery": "Выберите адрес получения заказа", "delivery": "Выберите адрес получения заказа",
"mobileDelivery": "доставка",
"contacts": "Введите данные получателя", "contacts": "Введите данные получателя",
"summary": "Подтвердите заказ" "mobileContacts": "контакты",
"summary": "Подтвердите заказ",
"mobileSummary": "заказ"
} }
} }

View File

@ -1,66 +1,58 @@
<template> <template>
<div v-if="coords" class="delivery"> <div v-if="coords" class="delivery">
<div class="delivery__sidebar"> <!-- Desktop -->
<div v-if="!isMobile" class="delivery__sidebar">
<DeliveryInfo v-if="isPickupPointSelected" /> <DeliveryInfo v-if="isPickupPointSelected" />
<div v-else class="delivery__search"> <DeliverySearch v-else :filtered-points="filteredPoints" />
<UInput </div>
v-model="searchTerm"
size="xl" <!-- Mobile -->
class="w-full" <UDrawer
placeholder="Выберите пункт выдачи" v-if="isMobile"
:ui="{ trailing: 'pe-1' }" v-model:open="open"
fixed
:ui="{
content: 'fixed bg-default ring ring-default flex focus:outline-none overflow-hidden',
container: 'w-full flex flex-col gap-4 p-4 overflow-hidden',
body: 'flex-1 overflow-y-auto',
}"
> >
<template v-if="searchTerm?.length" #trailing> <template #content>
<UButton <div class="px-4 pb-4">
color="neutral" <DeliveryInfo v-if="isPickupPointSelected" />
variant="link" </div>
size="sm"
icon="i-lucide-circle-x"
aria-label="Clear input"
@click="searchTerm = ''"
/>
</template> </template>
</UInput> </UDrawer>
<div class="delivery__items">
<div
v-for="pickupPoint in filteredPoints"
:key="pickupPoint.id"
class="pickup-point-card"
@click="checkoutPickupPoint = pickupPoint"
>
<div class="pickup-point-card__content">
<h3>Yandex</h3>
<p>{{ `${pickupPoint?.address?.street} ${pickupPoint?.address?.house}` }}</p>
<h3>с day month бесплатно</h3>
</div>
<Icon class="pickup-point-card__action" name="lucide:chevron-right" />
</div>
</div>
</div>
</div>
activeTab
{{ activeTab }}
<!-- Desktop Mobile -->
<PvzMap <PvzMap
v-model="checkoutPickupPoint" v-model="checkoutPickupPoint"
:pickup-points="filteredPoints" :pickup-points="filteredPoints"
@update:model-value="setCheckoutPickupPoint(checkoutPickupPoint)" @active-tab="value => activeTab = value"
@update:model-value="updatePoint()"
/> />
</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 { useGeolocation } 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'
import DeliverySearch from '~/components/DeliverySearch.vue'
import PvzMap from '~/components/PvzMap.vue' import PvzMap from '~/components/PvzMap.vue'
import { useCheckout } from '~/composables/useCheckout' import { useCheckout } from '~/composables/useCheckout'
const { setCheckoutPickupPoint, isPickupPointSelected, checkoutPickupPoint } = useCheckout() const { setCheckoutPickupPoint, isPickupPointSelected, checkoutPickupPoint } = useCheckout()
const { coords } = useGeolocation() const { coords } = useGeolocation()
const open = ref(false)
const isMobile = useMediaQuery('(max-width: 1280px)')
const yandexPvz = ref<YandexPvzResponse>() const yandexPvz = ref<YandexPvzResponse>()
const searchTerm = ref('') const searchTerm = ref('')
const activeTab = ref()
const waitForCoords = () => const waitForCoords = () =>
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
@ -111,6 +103,11 @@ const filteredPoints = computed<PickupPoint[]>(() => {
) || [] ) || []
}) })
function updatePoint() {
setCheckoutPickupPoint(checkoutPickupPoint.value)
open.value = true
}
definePageMeta({ definePageMeta({
layout: 'checkout', layout: 'checkout',
}) })
@ -130,36 +127,5 @@ definePageMeta({
width: 410px; width: 410px;
padding-inline: 24px; padding-inline: 24px;
} }
&__items {
height: calc(100dvh - 54px - 40px - 24px);
overflow-y: auto;
flex-shrink: 0;
}
}
.pickup-point-card {
position: relative;
width: 100%;
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
cursor: pointer;
&__content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
&__action {
flex-shrink: 0;
width: 16px;
height: 16px;
color: #999;
}
} }
</style> </style>