fix(desktop): карта ПВЗ на Яндекс.Картах вместо нерабочего OpenLayers

На странице пунктов выдачи (стол администратора) диалог карты падал с
«Failed to resolve component: ol-map»: boot/map.ts с регистрацией
vue3-openlayers никогда не был подключён в quasar.config (массив boot),
поэтому ol-* не резолвились и карта не рисовалась.

OpenLayers в проекте больше нигде не используется — переводим shared/ui/Map
на Yandex Maps JS API (как рабочий виджет KUMapWithList) и сносим OpenLayers:

- shared/ui/Map/Map.vue: single-point карта на ymaps (метка — pin.svg),
  тот же публичный API long/lat; деградированный баннер без ключа.
- shared/lib/yandexMaps: загрузчик SDK вынесен из widgets/KUMapWithList в
  shared (DRY) — общий для виджета списка ПВЗ и single-point карты.
- удалён boot/map.ts (мёртвый, не в boot) и зависимость vue3-openlayers.

Lockfile подровняется при следующей установке (vue3-openlayers пока остаётся
orphan-записью).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ant
2026-05-30 07:06:15 +00:00
parent 34e515f48c
commit cf6ad7c76d
6 changed files with 111 additions and 53 deletions
-1
View File
@@ -86,7 +86,6 @@
"vue-router": "^4.0.12",
"vue-window-size": "^2.0.0",
"vue3-dropzone": "^2.2.1",
"vue3-openlayers": "^10.0.1",
"windows-1251": "^3.0.4"
},
"devDependencies": {
-7
View File
@@ -1,7 +0,0 @@
import { boot } from 'quasar/wrappers';
import OpenLayersMap from 'vue3-openlayers';
import 'vue3-openlayers/dist/main.css';
export default boot(({ app }) => {
app.use(OpenLayersMap);
});
@@ -0,0 +1 @@
export { loadYandexMaps } from './loadYandexMaps'
@@ -1,9 +1,12 @@
/**
* Динамический загрузчик Yandex Maps JS API (Story 2.3).
* Динамический загрузчик Yandex Maps JS API.
*
* Идемпотентен: повторный вызов возвращает закэшированный `Promise<typeof ymaps>`.
* Если `apiKey` пуст возвращает Promise.reject; компонент должен показать
* деградированный UI (список без карты).
* Если `apiKey` пуст возвращает Promise.reject; компонент-потребитель должен
* показать деградированный UI (без карты).
*
* Единая точка загрузки SDK для всех карт desktop (виджет списка ПВЗ,
* single-point карта в диалогах и т.д.) поэтому живёт в shared/lib.
*/
let cached: Promise<unknown> | null = null
+103 -41
View File
@@ -1,52 +1,114 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'
import { env } from 'src/shared/config'
import { loadYandexMaps } from 'src/shared/lib/yandexMaps'
import PinIcon from 'src/assets/pin.svg'
const props = defineProps({
long: {
type: Number,
required: true,
},
lat: {
type: Number,
required: true,
/**
* Single-point карта на Yandex Maps JS API: показывает одну метку по
* координатам. Используется в диалогах просмотра адреса (например, карточка
* ПВЗ на столе администратора).
*
* SDK грузится лениво через общий {@link loadYandexMaps}; если ключ
* YANDEX_MAPS_API_KEY не задан — показывается деградированный баннер.
*/
const props = defineProps<{
long: number
lat: number
}>()
const apiKey = env.YANDEX_MAPS_API_KEY
const mapContainer = ref<HTMLElement | null>(null)
let mapInstance: any = null
let ymapsRef: any = null
let resizeObserver: ResizeObserver | null = null
function center(): [number, number] {
// Yandex принимает координаты в порядке [широта, долгота].
return [Number(props.lat), Number(props.long)]
}
function renderPlacemark(): void {
if (!mapInstance || !ymapsRef) return
mapInstance.geoObjects.removeAll()
const placemark = new ymapsRef.Placemark(
center(),
{},
{
iconLayout: 'default#image',
iconImageHref: PinIcon,
iconImageSize: [24, 32],
iconImageOffset: [-12, -32],
},
)
mapInstance.geoObjects.add(placemark)
}
async function initMap(): Promise<void> {
if (!apiKey || !mapContainer.value) return
try {
ymapsRef = await loadYandexMaps(apiKey)
await nextTick()
if (!mapContainer.value) return
mapInstance = new ymapsRef.Map(mapContainer.value, {
center: center(),
zoom: 17,
controls: ['zoomControl', 'fullscreenControl'],
})
renderPlacemark()
// Внутри диалога контейнер на момент монтирования может иметь нулевой
// размер (анимация раскрытия) — подгоняем карту под вьюпорт по ресайзу.
resizeObserver = new ResizeObserver(() => {
mapInstance?.container.fitToViewport()
})
resizeObserver.observe(mapContainer.value)
} catch (err) {
console.warn('[Map] Yandex Maps init упал:', err)
}
}
watch(
() => [props.lat, props.long],
() => {
if (mapInstance) {
mapInstance.setCenter(center(), 17)
renderPlacemark()
}
},
)
onMounted(() => {
void initMap()
})
const projection = ref('EPSG:4326')
const zoom = ref(18)
const rotation = ref(0)
const pinCenter = computed(() => {
return [Number(props.long), Number(props.lat)]
onBeforeUnmount(() => {
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
if (mapInstance) {
mapInstance.destroy()
mapInstance = null
}
})
</script>
<template lang="pug">
div.q-mt-lg
ol-map(
:load-tiles-while-animating="true"
:load-tiles-while-interacting="true"
style="height: 300px; border: 1px solid grey;"
)
ol-view(
ref="view"
:center="pinCenter"
:enable-rotation="false"
:zoom="zoom"
:projection="projection"
:rotation="rotation"
)
ol-tile-layer
ol-source-osm
ol-vector-layer
ol-source-vector
ol-feature
ol-geom-point(v-if="pinCenter" :coordinates="pinCenter")
ol-style
ol-style-stroke(color="red" :width="2")
ol-style-fill(color="rgba(255,255,255,0.1)")
ol-style-icon(:src="PinIcon" :scale="1.5")
.ya-map(v-if="apiKey" ref="mapContainer")
.banner.banner--warn(v-else)
q-icon.banner__icon(name="warning" size="18px")
.banner__body Карта недоступна: не задан ключ Яндекс.Карт.
</template>
<style scoped lang="scss">
.ya-map {
width: 100%;
height: 300px;
border: 1px solid var(--p-line);
border-radius: var(--p-r-md, 12px);
overflow: hidden;
}
</style>
@@ -27,7 +27,7 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { env } from 'src/shared/config'
import { loadYandexMaps } from '../lib/loadYandexMaps'
import { loadYandexMaps } from 'src/shared/lib/yandexMaps'
import type { IMarketplaceKUDetails } from 'src/entities/MarketplaceKUDetails'
const props = withDefaults(