Загрузка данных


<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>