Загрузка данных
<template>
<div>
<div class="columns mx-1 mt-1">
<div class="column is-6-tablet is-8-widescreen is-8-fullhd main-map">
<l-map
ref="map"
:max-bounds="$options.MapSettings.maxBounds"
:min-zoom="$options.MapSettings.minZoom"
:max-zoom="$options.MapSettings.maxZoom"
:zoom="$options.MapSettings.zoom"
:center="$options.MapSettings.center"
:bounds="bounds"
@click="mapClick"
@ready="onMapReady"
>
<l-tile-layer :url="$options.MapSettings.url" />
<v-marker-cluster ref="cluster" :options="clusterOptions">
<l-marker v-for="object in list" :key="`object_${object.b_id}`" :lat-lng="[object.a_lat, object.a_lon]" @ready="onMarkerReady($event, object)">
<l-icon :class-name="`object-icon ${object.o_free ? 'border-green' : object.o_sc ? 'border-red' : 'border-orange'}`">
<b-icon class="" icon="building" size="is-small" />
</l-icon>
<l-popup>
<!-- <Object-info :selectedObject="object" mode="short" :isOperator="isOperator" @update-info="getObjectList()" /> -->
{{ object.a_name }}
<br />
<span v-for="providerId in object.competitors" :key="providerId" class="tag is-primary is-light mr-1">
{{ providers[providerId].p_name }}
</span>
<!-- <button
class="button is-info is-small is-fullwidth mt-1"
@click="
selectedObject = object;
objectDetailsModal_visible = true;
"
>
Подробнее
</button> -->
</l-popup>
</l-marker>
</v-marker-cluster>
</l-map>
</div>
<div class="column">
<div class="subtitle is-5 has-background-info-light p-2 mb-2">Список объектов на карте ({{ visibleAddresses.length }} шт.):</div>
<b-table
:data="visibleAddresses"
bordered
narrowed
hoverable
paginated
:per-page="perPage"
:current-page="currentPage"
pagination-size="is-small"
sticky-header
height="calc(100vh - 280px)"
class="smallTable is-size-7 fullHeight"
:row-class="(row) => !row.a_house_number && 'has-background-danger-light'"
detailed
detail-key="b_id"
:show-detail-icon="false"
:opened-detailed="openedDetails"
@click="
(row) => {
openedDetails = openedDetails[0] == row.b_id ? [] : [row.b_id];
// setFitBounds(row);
}
"
>
<b-table-column v-slot="props" field="id" label="№" width="20">
<div>{{ props.index + 1 }}</div>
<b-icon
v-if="!props.row.a_house_number"
icon="exclamation-circle"
type="is-danger"
title="Неверно выбрана геометка у адреса. Сверьте название по оригинальному адресу из АСТУП файла (вторая строчка в столбце с адресом) и найдите актуальный адрес из OpenStreetMap"
style="width: 12px"
/>
</b-table-column>
<b-table-column v-slot="props" field="a_name" label="Адрес" width="150">
<div class="has-text-weight-semibold" title="Адрес после геокодирования OpenStreetMap" style="cursor: default">
<span v-if="props.row.a_city">{{ props.row.a_city }},</span> {{ props.row.a_name }}
</div>
<div
:class="{ 'tag is-success': !props.row.a_house_number, 'has-text-grey': props.row.a_house_number }"
title="Оригинальный адрес из файла АСТУП"
style="cursor: default"
>
{{ `${props.row.b_eva}, ${props.row.b_number}` }}
</div>
</b-table-column>
<b-table-column v-slot="props" field="b_entrance_count" label="Подъездов" width="20">
{{ props.row.b_entrance_count }}
</b-table-column>
<b-table-column v-slot="props" field="b_floor" label="Этажей" width="20">
{{ props.row.b_floor }}
</b-table-column>
<b-table-column v-slot="props" field="b_flat" label="Квартир" width="20">
{{ props.row.b_flat }}
</b-table-column>
<b-table-column v-slot="props" field="competitors" label="Конкуренты" width="50" centered>
<span v-for="providerId in props.row.competitors" :key="providerId" class="tag is-primary is-light mr-1">
{{ providers[providerId].p_name }}
</span>
</b-table-column>
<!-- <b-table-column v-slot="props" field="o_free" label="Свободные помещения" width="30" sortable centered>
<b-icon :type="{ 'is-success': props.row.o_free, 'is-danger': !props.row.o_free }" :icon="props.row.o_free ? 'check' : 'times'" />
</b-table-column>
<b-table-column v-slot="props" field="o_description" label="Описание" width="150">
<span
style="display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis"
:title="props.row.o_description"
>
{{ props.row.o_description }}
</span>
</b-table-column> -->
<template slot="detail" slot-scope="props">
<div class="columns">
<div class="column">
<div class="box" :class="{ 'has-background-grey-light': props.row.b_deleted }">
<b-field>
<div class="title is-5" style="width: 100%">{{ `${props.row.b_eva}, ${props.row.b_number}` }}</div>
<div style="width: 30px">
<button
v-if="!props.row.b_deleted && isOperator"
class="button is-success is-outlined is-small is-fullwidth"
@click.stop="buildingModal_visible = true"
>
<b-icon icon="pen" aria-hidden="true" />
</button>
</div>
</b-field>
<b-tag v-if="props.row.b_deleted" type="is-danger"> Удален {{ new Date(props.row.b_deleted).toLocaleDateString() }} </b-tag>
<b-field class="label_30" horizontal label="Адрес OpenStreetMap">
<div>
<span v-if="props.row.a_city">{{ props.row.a_city }},</span> {{ props.row.a_name }}
</div>
</b-field>
<b-field class="label_30" horizontal label="Примечание">{{ props.row.b_note }}</b-field>
</div>
</div>
<div class="column">
<div class="box">
<div class="media">
<div class="media-content">
<div class="subtitle is-5">Список конкурентов</div>
</div>
<div class="media-right">
<button
v-if="!props.row.b_deleted && isOperator"
class="button is-info is-small is-fullwidth"
@click="
selectedOrganization = null;
organizationModal_visible = true;
"
>
Добавить конкурента
</button>
</div>
</div>
<b-table v-if="props.row.competitors" :data="props.row.competitors" :bordered="false" striped narrowed hoverable class="is-size-7">
<template>
<b-table-column v-slot="{ row }" field="p_id" label="№" width="10"> {{ providers[row].p_id }} </b-table-column>
<b-table-column v-slot="{ row }" field="p_name" label="Наименование" width="50">
{{ providers[row].p_name }}
</b-table-column>
<b-table-column v-slot="{ row }" field="edit" label="" width="10">
<button
class="button is-primary is-small is-outlined"
style="height: 24px"
disabled
title="Редактировать конкурента"
@click="
selectedOrganization = row;
organizationModal_visible = true;
"
>
<b-icon icon="pen" />
</button>
</b-table-column>
</template>
<template slot="empty">
<section class="section">
<div class="content has-text-grey has-text-centered">
<p>Данные не получены</p>
</div>
</section>
</template>
</b-table>
<div v-else class="subtitle is-6 has-text-weight-bold mt-2">Адреса не добавлены</div>
</div>
</div>
</div>
</template>
<template #empty>
<div class="content has-text-grey has-text-centered">
<b-icon class="my-icon height-100 is-size-1" icon="blind" size="is-large" />
<p class="mb-3">Ничего не найдено</p>
</div>
</template>
</b-table>
</div>
</div>
<b-modal v-model="buildingModal_visible" :active.sync="buildingModal_visible" has-modal-card width="1000px">
<Building-modal
:edit_building="recordSelected"
:providers="providers"
@close-modal="buildingModal_visible = false"
@update-info="
getList();
if ($refs.shortloglist) $refs.shortloglist.getShortLog();
"
/>
</b-modal>
</div>
</template>
<script>
import debounce from 'lodash/debounce';
// leaflet
import L from 'leaflet';
import { Icon } from 'leaflet';
import { LMap, LTileLayer, LMarker, LIcon, LPopup, LPolyline, LPolygon, LControl } from 'vue2-leaflet';
import Vue2LeafletMarkerCluster from 'vue2-leaflet-markercluster';
import 'leaflet/dist/leaflet.css';
// Для правильного отображения иконок Leaflet (если нужно)
delete Icon.Default.prototype._getIconUrl;
Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
});
// wrappers
import { wrapWithLoading } from '../../../mixins/wrappers.mixin';
//toast
import { showDangerToast } from '../../../mixins/toast.mixin';
import BuildingModal from '../modals/buildingModal.vue';
export default {
components: {
LMap,
LTileLayer,
LMarker,
LIcon,
LPopup,
LPolyline,
LPolygon,
LControl,
'v-marker-cluster': Vue2LeafletMarkerCluster,
BuildingModal,
},
mixins: [wrapWithLoading, showDangerToast],
props: ['list', 'providers', 'isOperator'],
MapSettings: {
// url: 'storage/Tiles/{z}/{x}/{y}.png',
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
center: [53.9, 27.56],
zoom: 12,
minZoom: 5,
maxZoom: 18,
maxBounds: L.latLngBounds([
[51.190785980509354, 23.076354200895736],
[56.245591731745186, 32.878022287814566],
]),
},
Types: {
stop: 'red',
walk: 'green',
trip: 'blue',
},
LineTypes: {
stop: '#ff0000',
walk: '#23e635',
trip: '#0000ff',
out: '#e40045',
},
data() {
return {
clusterOptions: {
spiderfyDistanceMultiplier: 1, // Опции кластера
chunkedLoading: false, // Отключаем асинхронную загрузку чанками
maxClusterRadius: 70,
disableClusteringAtZoom: 16,
},
visibleAddresses: [],
mapObject: null,
clusterObject: null,
openedDetails: [],
buildingModal_visible: false,
perPage: 50,
currentPage: 1,
bounds: L.latLngBounds([
[53.88, 27.4],
[53.93, 27.7],
]),
};
},
computed: {
recordSelected() {
return this.openedDetails.length && this.list.length ? this.list.find((e) => e.b_id === this.openedDetails[0]) : null;
},
},
mounted: {},
beforeDestroy() {
// Важно: удаляем обработчик при уничтожении компонента
if (this.mapObject) {
this.mapObject.off('moveend', this.updateVisibleAddresses);
}
},
watch: {
// filter: { deep: true, handler(newValue, oldValue) {this.getCurrentPosition();}},
// searchAddress2: 'getAddressList',
},
methods: {
mapClick(event) {
// this.getSelectedAddress(event);
},
onMapReady(map) {
// Сохраняем объект карты
this.mapObject = map;
// Добавляем обработчик moveend
map.on('moveend', this.updateVisibleAddresses);
// Запускаем сразу для начального состояния
this.updateVisibleAddresses();
},
onMarkerReady(marker, point) {
// Сохраняем адрес прямо на маркере для легкого доступа
if (marker && marker.mapObject) {
marker.mapObject = point;
}
},
updateVisibleAddresses: debounce(function () {
// console.time('FirstWay');
if (!this.mapObject) return;
const bounds = this.mapObject.getBounds();
const addresses = [];
// Получаем кластер-группу через ref
// if (this.$refs.cluster && this.$refs.cluster.mapObject) {
// this.$refs.cluster.mapObject.eachLayer((layer) => {
// // Проверяем, что это маркер, а не кластер
// if (layer instanceof L.Marker && !(layer instanceof L.MarkerCluster)) {
// if (bounds.contains(layer.getLatLng())) {
// // Берем адрес из свойства, которое мы сохранили
// const address = layer.address || (layer.getPopup() && layer.getPopup().getContent());
// if (address) {
// addresses.push(address);
// }
// }
// }
// });
// }
//
// this.visibleAddresses = addresses;
// console.log('Адреса в видимой области:', addresses);
this.visibleAddresses = this.list.filter((item) => {
const point = L.latLng(item.a_lat, item.a_lon);
return bounds.contains(point);
});
// console.timeEnd('FirstWay');
}, 1000),
setFitBounds(data) {
return;
// if (!data.length) return;
console.log(data);
this.bounds = L.latLngBounds([
[data.a_lat - 0.0001, data.a_lon - 0.0001],
[data.a_lat + 0.0001, data.a_lon + 0.0001],
]).pad(0.1);
},
getMinAndMaxСoordinates(data) {
const minСoordinates = [data[0].latitude, data[0].longitude];
const maxСoordinates = [data[0].latitude, data[0].longitude];
for (const { latitude, longitude } of data) {
if (latitude < minСoordinates[0]) minСoordinates[0] = latitude;
if (latitude > maxСoordinates[0]) maxСoordinates[0] = latitude;
if (longitude < minСoordinates[1]) minСoordinates[1] = longitude;
if (longitude > maxСoordinates[1]) maxСoordinates[1] = longitude;
}
return [minСoordinates, maxСoordinates];
},
},
};
</script>
<style scoped lang="scss">
@import '~leaflet/dist/leaflet.css';
@import '~leaflet.markercluster/dist/MarkerCluster.css';
@import '~leaflet.markercluster/dist/MarkerCluster.Default.css';
.app-main-page .main-map::v-deep {
.vue2leaflet-map {
border-radius: 10px;
box-shadow: 3px 3px 5px #0000005c, 0 0 3px #0000008c;
height: calc(100vh - 180px) !important;
z-index: 1 !important;
.leaflet-marker-icon.object-icon {
background-color: white;
border-style: solid;
display: flex;
align-items: center;
justify-content: center;
height: 28px !important;
width: 28px !important;
border-radius: 24px;
border-width: 2px;
padding: 3px;
&.border-red {
border-color: red;
&:after {
background: red;
}
}
&.border-green {
border-color: #23e635;
&:after {
background: #23e635;
}
}
&.border-blue {
border-color: #2160e6;
&:after {
background: #2160e6;
}
}
&.border-yellow {
border-color: yellow;
&:after {
background: yellow;
}
}
&.border-violet {
border-color: violet;
&:after {
background: violet;
}
}
&.border-orange {
border-color: orange;
&:after {
background: orange;
}
}
&.border-olive {
border-color: olive;
&:after {
background: olive;
}
}
&.border-turquoise {
border-color: turquoise;
&:after {
background: turquoise;
}
}
&.border-orangeRed {
border-color: OrangeRed;
&:after {
background: OrangeRed;
}
}
}
.leaflet-marker-icon.addressPopup {
background-color: white;
border: 2px solid black;
height: 10px;
border-radius: 50%;
}
.leaflet-control-attribution.leaflet-control a[href="https://leafletjs.com"]
{
display: none;
}
}
}
.app-map-page::v-deep {
.smallTable {
.table-wrapper {
// font-size: 13px;
// height: calc(100vh - 210px);
overflow-x: hidden;
}
::-webkit-scrollbar {
width: 7px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
border-radius: 3px;
background: #888;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
}
.smallScrollCntainer {
.smallScroll {
overflow-x: hidden;
max-height: calc(100vh - 180px - 220px) !important;
overflow-y: scroll;
}
::-webkit-scrollbar {
width: 7px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
border-radius: 3px;
background: #888;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
}
.halfHeight > div.table-wrapper {
height: calc(50vh - 140px) !important;
}
.fullHeight > div.table-wrapper {
height: calc(100vh - 230px) !important;
}
@media screen and (max-width: 1000px) {
.columns {
display: block !important;
.column {
width: 100% !important;
.vue2leaflet-map {
height: 600px !important;
}
.smallTable {
.table-wrapper {
height: 100%;
}
}
}
}
}
.vs__dropdown-menu {
width: auto !important;
}
.v-select input {
height: 20px;
}
}
</style>