|
@@ -5,6 +5,37 @@
|
|
|
>
|
|
>
|
|
|
<div id="tdt-map-container" ref="mapContainer"></div>
|
|
<div id="tdt-map-container" ref="mapContainer"></div>
|
|
|
|
|
|
|
|
|
|
+ <transition name="map-cluster-fade">
|
|
|
|
|
+ <div v-if="clusterListVisible" class="map-cluster-popup">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="map-cluster-popup__mask"
|
|
|
|
|
+ @click="clusterListVisible = false"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="map-cluster-popup__panel" @click.stop>
|
|
|
|
|
+ <div class="map-cluster-popup__title">
|
|
|
|
|
+ 该位置共 {{ clusterListItems.length }} 处
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <ul class="map-cluster-popup__list">
|
|
|
|
|
+ <li
|
|
|
|
|
+ v-for="rec in clusterListItems"
|
|
|
|
|
+ :key="rec.name"
|
|
|
|
|
+ class="map-cluster-popup__item"
|
|
|
|
|
+ @click="onClusterItemClick(rec)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ rec.name }}
|
|
|
|
|
+ </li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="map-cluster-popup__close"
|
|
|
|
|
+ @click="clusterListVisible = false"
|
|
|
|
|
+ >
|
|
|
|
|
+ 关闭
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </transition>
|
|
|
|
|
+
|
|
|
<img
|
|
<img
|
|
|
class="map-page-back"
|
|
class="map-page-back"
|
|
|
src="@/assets/images/icon-fanhui.png"
|
|
src="@/assets/images/icon-fanhui.png"
|
|
@@ -14,7 +45,7 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
|
|
|
|
|
|
+import { ref, onMounted, onBeforeUnmount, watch, nextTick } from "vue";
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
import { storeToRefs } from "pinia";
|
|
import { storeToRefs } from "pinia";
|
|
|
import { useSidebarStore } from "@/stores/sidebar";
|
|
import { useSidebarStore } from "@/stores/sidebar";
|
|
@@ -24,9 +55,19 @@ const { visible: sidebarVisible } = storeToRefs(useSidebarStore());
|
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
|
const mapContainer = ref(null);
|
|
const mapContainer = ref(null);
|
|
|
let map = null;
|
|
let map = null;
|
|
|
-/** @type {Array<{ marker: T.Marker, item: object, name: string, boxW: number, totalH: number }>} */
|
|
|
|
|
|
|
+/** @type {Array<{ marker: T.Marker, item: object, name: string, boxW: number, totalH: number, lng: number, lat: number }>} */
|
|
|
const markerRecords = [];
|
|
const markerRecords = [];
|
|
|
|
|
+/** @type {Array<{ marker: T.Marker, members: typeof markerRecords }>} */
|
|
|
|
|
+const clusterMarkerRecords = [];
|
|
|
const mapData = ref([]); // 有经纬度的数据项
|
|
const mapData = ref([]); // 有经纬度的数据项
|
|
|
|
|
+const clusterListVisible = ref(false);
|
|
|
|
|
+const clusterListItems = ref([]);
|
|
|
|
|
+
|
|
|
|
|
+/** zoom 小于此值时(更小的级别、视野更远)对屏幕距离过近的标注做聚合;≥ 此值时全部单点展示 */
|
|
|
|
|
+const CLUSTER_ZOOM_THRESHOLD = 16;
|
|
|
|
|
+/** 图层像素距离小于该值视为堆叠,参与同一聚合 */
|
|
|
|
|
+const CLUSTER_PIXEL_RADIUS = 50;
|
|
|
|
|
+const CLUSTER_ICON_SIZE = 52;
|
|
|
|
|
|
|
|
// 珠海市中心坐标
|
|
// 珠海市中心坐标
|
|
|
const ZHUHAI_CENTER = { lng: 113.57668, lat: 22.271 };
|
|
const ZHUHAI_CENTER = { lng: 113.57668, lat: 22.271 };
|
|
@@ -89,6 +130,22 @@ function escapeXml(s) {
|
|
|
.replace(/'/g, "'");
|
|
.replace(/'/g, "'");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 聚合点圆形图标(与单点标注配色一致) */
|
|
|
|
|
+function createClusterSvg(count, highlight = false) {
|
|
|
|
|
+ const s = CLUSTER_ICON_SIZE;
|
|
|
|
|
+ const r = 18;
|
|
|
|
|
+ const cx = s / 2;
|
|
|
|
|
+ const cy = s / 2 - 1;
|
|
|
|
|
+ const fill = highlight ? "#b84033" : "#98382e";
|
|
|
|
|
+ const stroke = highlight ? "#e5b735" : "#fffccd";
|
|
|
|
|
+ const text = count > 99 ? "99+" : String(count);
|
|
|
|
|
+ const fontSize = count > 9 ? 14 : 16;
|
|
|
|
|
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${s}" height="${s}" viewBox="0 0 ${s} ${s}">
|
|
|
|
|
+ <circle cx="${cx}" cy="${cy}" r="${r}" fill="${fill}" stroke="${stroke}" stroke-width="2"/>
|
|
|
|
|
+ <text x="${cx}" y="${cy + fontSize / 3}" text-anchor="middle" font-size="${fontSize}" font-weight="700" fill="#fffef0" font-family="sans-serif">${escapeXml(text)}</text>
|
|
|
|
|
+</svg>`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 将 SVG 转为 data URL,供 T.Icon 使用 */
|
|
/** 将 SVG 转为 data URL,供 T.Icon 使用 */
|
|
|
function svgToDataUrl(svg) {
|
|
function svgToDataUrl(svg) {
|
|
|
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
|
|
@@ -116,20 +173,157 @@ function applyMarkerVisual(rec, useHighlight) {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** 聚合点图标随路由选中项更新 */
|
|
|
|
|
+function syncClusterMarkersHighlight() {
|
|
|
|
|
+ for (const cm of clusterMarkerRecords) {
|
|
|
|
|
+ const hl = cm.members.some((r) => isNameSelected(r.name));
|
|
|
|
|
+ cm.marker.setIcon(
|
|
|
|
|
+ new T.Icon({
|
|
|
|
|
+ iconUrl: svgToDataUrl(createClusterSvg(cm.members.length, hl)),
|
|
|
|
|
+ iconSize: new T.Point(CLUSTER_ICON_SIZE, CLUSTER_ICON_SIZE),
|
|
|
|
|
+ iconAnchor: new T.Point(CLUSTER_ICON_SIZE / 2, CLUSTER_ICON_SIZE / 2),
|
|
|
|
|
+ }),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 根据路由同步所有 marker 高亮,并将选中项置于覆盖物最上层 */
|
|
/** 根据路由同步所有 marker 高亮,并将选中项置于覆盖物最上层 */
|
|
|
function syncMarkerHighlight() {
|
|
function syncMarkerHighlight() {
|
|
|
if (!map || !markerRecords.length) return;
|
|
if (!map || !markerRecords.length) return;
|
|
|
for (const rec of markerRecords) {
|
|
for (const rec of markerRecords) {
|
|
|
applyMarkerVisual(rec, isNameSelected(rec.name));
|
|
applyMarkerVisual(rec, isNameSelected(rec.name));
|
|
|
}
|
|
}
|
|
|
|
|
+ syncClusterMarkersHighlight();
|
|
|
const sel = getSelectedNameFromRoute();
|
|
const sel = getSelectedNameFromRoute();
|
|
|
if (!sel) return;
|
|
if (!sel) return;
|
|
|
const rec = markerRecords.find((r) => String(r.name).trim() === sel);
|
|
const rec = markerRecords.find((r) => String(r.name).trim() === sel);
|
|
|
if (!rec) return;
|
|
if (!rec) return;
|
|
|
|
|
+ const overlays = map.getOverlays?.() ?? [];
|
|
|
|
|
+ if (!overlays.includes(rec.marker)) return;
|
|
|
map.removeOverLay(rec.marker);
|
|
map.removeOverLay(rec.marker);
|
|
|
map.addOverLay(rec.marker);
|
|
map.addOverLay(rec.marker);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 在图层像素坐标下按距离做并查集聚合(传递闭包:A 近 B、B 近 C 则三者合一)
|
|
|
|
|
+ * @param {typeof markerRecords} records
|
|
|
|
|
+ */
|
|
|
|
|
+/** 使用容器像素坐标(与标注实际屏幕位置一致);图层坐标 lngLatToLayerPoint 在 EPSG:4326 下与堆叠判断易不一致 */
|
|
|
|
|
+function clusterRecordsByPixel(records, mapInstance, thresholdPx) {
|
|
|
|
|
+ const n = records.length;
|
|
|
|
|
+ if (n === 0) return [];
|
|
|
|
|
+ const pts = records.map((rec) => ({
|
|
|
|
|
+ rec,
|
|
|
|
|
+ p: mapInstance.lngLatToContainerPoint(new T.LngLat(rec.lng, rec.lat)),
|
|
|
|
|
+ }));
|
|
|
|
|
+ const parent = Array.from({ length: n }, (_, i) => i);
|
|
|
|
|
+ function find(x) {
|
|
|
|
|
+ return parent[x] === x ? x : (parent[x] = find(parent[x]));
|
|
|
|
|
+ }
|
|
|
|
|
+ function union(a, b) {
|
|
|
|
|
+ const ra = find(a);
|
|
|
|
|
+ const rb = find(b);
|
|
|
|
|
+ if (ra !== rb) parent[rb] = ra;
|
|
|
|
|
+ }
|
|
|
|
|
+ const t2 = thresholdPx * thresholdPx;
|
|
|
|
|
+ for (let i = 0; i < n; i++) {
|
|
|
|
|
+ for (let j = i + 1; j < n; j++) {
|
|
|
|
|
+ const dx = pts[i].p.x - pts[j].p.x;
|
|
|
|
|
+ const dy = pts[i].p.y - pts[j].p.y;
|
|
|
|
|
+ if (dx * dx + dy * dy <= t2) union(i, j);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const groups = new Map();
|
|
|
|
|
+ for (let i = 0; i < n; i++) {
|
|
|
|
|
+ const r = find(i);
|
|
|
|
|
+ if (!groups.has(r)) groups.set(r, []);
|
|
|
|
|
+ groups.get(r).push(pts[i].rec);
|
|
|
|
|
+ }
|
|
|
|
|
+ return [...groups.values()];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function clearClusterMarkersFromMap() {
|
|
|
|
|
+ for (const cm of clusterMarkerRecords) {
|
|
|
|
|
+ map?.removeOverLay(cm.marker);
|
|
|
|
|
+ }
|
|
|
|
|
+ clusterMarkerRecords.length = 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 按当前缩放与视野刷新:zoom<16 时堆叠点聚合,≥16 时全部单点展示 */
|
|
|
|
|
+function refreshMarkersDisplay() {
|
|
|
|
|
+ if (!map || !markerRecords.length) return;
|
|
|
|
|
+ clearClusterMarkersFromMap();
|
|
|
|
|
+ for (const r of markerRecords) {
|
|
|
|
|
+ map.removeOverLay(r.marker);
|
|
|
|
|
+ }
|
|
|
|
|
+ const z = Number(map.getZoom());
|
|
|
|
|
+ if (isNaN(z) || z >= CLUSTER_ZOOM_THRESHOLD) {
|
|
|
|
|
+ for (const r of markerRecords) {
|
|
|
|
|
+ map.addOverLay(r.marker);
|
|
|
|
|
+ }
|
|
|
|
|
+ clusterListVisible.value = false;
|
|
|
|
|
+ syncMarkerHighlight();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const groups = clusterRecordsByPixel(
|
|
|
|
|
+ markerRecords,
|
|
|
|
|
+ map,
|
|
|
|
|
+ CLUSTER_PIXEL_RADIUS,
|
|
|
|
|
+ );
|
|
|
|
|
+ for (const group of groups) {
|
|
|
|
|
+ if (group.length === 1) {
|
|
|
|
|
+ map.addOverLay(group[0].marker);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ let sumLng = 0;
|
|
|
|
|
+ let sumLat = 0;
|
|
|
|
|
+ for (const g of group) {
|
|
|
|
|
+ sumLng += g.lng;
|
|
|
|
|
+ sumLat += g.lat;
|
|
|
|
|
+ }
|
|
|
|
|
+ const lng = sumLng / group.length;
|
|
|
|
|
+ const lat = sumLat / group.length;
|
|
|
|
|
+ const highlight = group.some((r) => isNameSelected(r.name));
|
|
|
|
|
+ const clusterMarker = new T.Marker(new T.LngLat(lng, lat), {
|
|
|
|
|
+ icon: new T.Icon({
|
|
|
|
|
+ iconUrl: svgToDataUrl(createClusterSvg(group.length, highlight)),
|
|
|
|
|
+ iconSize: new T.Point(CLUSTER_ICON_SIZE, CLUSTER_ICON_SIZE),
|
|
|
|
|
+ iconAnchor: new T.Point(CLUSTER_ICON_SIZE / 2, CLUSTER_ICON_SIZE / 2),
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+ clusterMarker.on("click", () => {
|
|
|
|
|
+ clusterListItems.value = group;
|
|
|
|
|
+ clusterListVisible.value = true;
|
|
|
|
|
+ });
|
|
|
|
|
+ map.addOverLay(clusterMarker);
|
|
|
|
|
+ clusterMarkerRecords.push({ marker: clusterMarker, members: group });
|
|
|
|
|
+ }
|
|
|
|
|
+ syncMarkerHighlight();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleMapViewChanged() {
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ refreshMarkersDisplay();
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function sceneRouteQuery(item) {
|
|
|
|
|
+ const url = String(item.scene).trim();
|
|
|
|
|
+ const q = { url };
|
|
|
|
|
+ if (item.batch != null && item.batch !== "") {
|
|
|
|
|
+ q.batch = String(item.batch);
|
|
|
|
|
+ }
|
|
|
|
|
+ return q;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function onClusterItemClick(rec) {
|
|
|
|
|
+ clusterListVisible.value = false;
|
|
|
|
|
+ const item = rec.item;
|
|
|
|
|
+ if (item.scene) {
|
|
|
|
|
+ router.push({ name: "Scene", query: sceneRouteQuery(item) });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** 地图 min/maxZoom 与实例一致,用于钳制 query.zoom */
|
|
/** 地图 min/maxZoom 与实例一致,用于钳制 query.zoom */
|
|
|
function clampZoom(z) {
|
|
function clampZoom(z) {
|
|
|
return Math.min(18, Math.max(12, z));
|
|
return Math.min(18, Math.max(12, z));
|
|
@@ -174,11 +368,11 @@ function addMarkers() {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const marker = new T.Marker(new T.LngLat(lng, lat), { icon });
|
|
const marker = new T.Marker(new T.LngLat(lng, lat), { icon });
|
|
|
- const rec = { marker, item, name, boxW, totalH };
|
|
|
|
|
|
|
+ const rec = { marker, item, name, boxW, totalH, lng, lat };
|
|
|
|
|
|
|
|
marker.on("click", () => {
|
|
marker.on("click", () => {
|
|
|
if (item.scene) {
|
|
if (item.scene) {
|
|
|
- router.push({ name: "Scene", query: { url: item.scene.trim() } });
|
|
|
|
|
|
|
+ router.push({ name: "Scene", query: sceneRouteQuery(item) });
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
marker.on("mouseover", () => {
|
|
marker.on("mouseover", () => {
|
|
@@ -188,10 +382,9 @@ function addMarkers() {
|
|
|
applyMarkerVisual(rec, isNameSelected(name));
|
|
applyMarkerVisual(rec, isNameSelected(name));
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- map.addOverLay(marker);
|
|
|
|
|
markerRecords.push(rec);
|
|
markerRecords.push(rec);
|
|
|
});
|
|
});
|
|
|
- syncMarkerHighlight();
|
|
|
|
|
|
|
+ refreshMarkersDisplay();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
watch(
|
|
watch(
|
|
@@ -201,9 +394,10 @@ watch(
|
|
|
name: route.query.name,
|
|
name: route.query.name,
|
|
|
zoom: route.query.zoom,
|
|
zoom: route.query.zoom,
|
|
|
}),
|
|
}),
|
|
|
- () => {
|
|
|
|
|
|
|
+ async () => {
|
|
|
centerMapToQuery();
|
|
centerMapToQuery();
|
|
|
- syncMarkerHighlight();
|
|
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ refreshMarkersDisplay();
|
|
|
},
|
|
},
|
|
|
{ deep: true },
|
|
{ deep: true },
|
|
|
);
|
|
);
|
|
@@ -245,9 +439,33 @@ onMounted(async () => {
|
|
|
|
|
|
|
|
centerMapToQuery();
|
|
centerMapToQuery();
|
|
|
addMarkers();
|
|
addMarkers();
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof map.on === "function") {
|
|
|
|
|
+ map.on("zoomend", handleMapViewChanged);
|
|
|
|
|
+ map.on("moveend", handleMapViewChanged);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ map.addEventListener("zoomend", handleMapViewChanged);
|
|
|
|
|
+ map.addEventListener("moveend", handleMapViewChanged);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ requestAnimationFrame(() => {
|
|
|
|
|
+ refreshMarkersDisplay();
|
|
|
|
|
+ });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
onBeforeUnmount(() => {
|
|
|
|
|
+ clusterListVisible.value = false;
|
|
|
|
|
+ if (map) {
|
|
|
|
|
+ if (typeof map.off === "function") {
|
|
|
|
|
+ map.off("zoomend", handleMapViewChanged);
|
|
|
|
|
+ map.off("moveend", handleMapViewChanged);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ map.removeEventListener("zoomend", handleMapViewChanged);
|
|
|
|
|
+ map.removeEventListener("moveend", handleMapViewChanged);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ clearClusterMarkersFromMap();
|
|
|
markerRecords.forEach((r) => map?.removeOverLay(r.marker));
|
|
markerRecords.forEach((r) => map?.removeOverLay(r.marker));
|
|
|
markerRecords.length = 0;
|
|
markerRecords.length = 0;
|
|
|
map = null;
|
|
map = null;
|
|
@@ -289,4 +507,96 @@ onBeforeUnmount(() => {
|
|
|
:deep(.tdt-marker) {
|
|
:deep(.tdt-marker) {
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+.map-cluster-popup {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ z-index: 1000;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+
|
|
|
|
|
+ &__mask {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.35);
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__panel {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: min(92vw, 700px);
|
|
|
|
|
+ max-height: min(55vh, 500);
|
|
|
|
|
+ padding: utils.vh-calc(16) utils.vw-calc(20);
|
|
|
|
|
+ background: rgb(217, 191, 157);
|
|
|
|
|
+ border: 2px solid #98382e;
|
|
|
|
|
+ border-radius: utils.vh-calc(8);
|
|
|
|
|
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: utils.vh-calc(10);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__title {
|
|
|
|
|
+ font-size: utils.vh-calc(16);
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #98382e;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__list {
|
|
|
|
|
+ list-style: none;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 0 utils.vw-calc(20);
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__item {
|
|
|
|
|
+ padding: utils.vh-calc(10) utils.vh-calc(12);
|
|
|
|
|
+ margin-bottom: utils.vh-calc(6);
|
|
|
|
|
+ font-size: utils.vh-calc(15);
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.7);
|
|
|
|
|
+ border-radius: utils.vh-calc(6);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: background 0.15s ease;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgba(152, 56, 46, 0.12);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &__close {
|
|
|
|
|
+ align-self: center;
|
|
|
|
|
+ padding: utils.vh-calc(8) utils.vh-calc(24);
|
|
|
|
|
+ font-size: utils.vh-calc(14);
|
|
|
|
|
+ color: #98382e;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: 1px solid #98382e;
|
|
|
|
|
+ border-radius: utils.vh-calc(6);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background: rgba(152, 56, 46, 0.08);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-cluster-fade-enter-active,
|
|
|
|
|
+.map-cluster-fade-leave-active {
|
|
|
|
|
+ transition: opacity 0.2s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.map-cluster-fade-enter-from,
|
|
|
|
|
+.map-cluster-fade-leave-to {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|