feat(map): improve co-located properties with smart grouping, layered pin badges, popup navigation, and spread mode
deploy-chabokan / deploy (push) Canceled after 0s
deploy-chabokan / deploy (push) Canceled after 0s
This commit is contained in:
@@ -13,12 +13,14 @@
|
|||||||
<path d="M3 17v4h4"/><path d="M21 17v4h-4"/>
|
<path d="M3 17v4h4"/><path d="M21 17v4h-4"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Fit all markers -->
|
<!-- Fit all markers -->
|
||||||
<button class="map-btn map-btn-fit" @click="fitBounds" title="نمایش همه املاک روی نقشه" :disabled="propertiesWithCoords.length === 0">
|
<button class="map-btn map-btn-fit" @click="fitBounds" title="نمایش همه املاک روی نقشه" :disabled="propertiesWithCoords.length === 0">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
|
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Locate me -->
|
<!-- Locate me -->
|
||||||
<button class="map-btn map-btn-locate" @click="locateMe" title="موقعیت من" :class="{ locating: isLocating }">
|
<button class="map-btn map-btn-locate" @click="locateMe" title="موقعیت من" :class="{ locating: isLocating }">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -27,12 +29,40 @@
|
|||||||
<circle cx="12" cy="12" r="7" opacity="0.3"/>
|
<circle cx="12" cy="12" r="7" opacity="0.3"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Spread / Group Mode Toggle (when co-located properties exist) -->
|
||||||
|
<button
|
||||||
|
v-if="hasCoLocatedProperties"
|
||||||
|
class="map-btn map-btn-spread"
|
||||||
|
:class="{ 'is-active': isSpreadMode }"
|
||||||
|
@click="toggleSpreadMode"
|
||||||
|
:title="isSpreadMode ? 'نمایش تجمیعی املاک هممکان' : 'تفکیک و نمایش جداگانه املاک هممکان'"
|
||||||
|
>
|
||||||
|
<svg v-if="isSpreadMode" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Collapse / Group icon -->
|
||||||
|
<rect x="4" y="4" width="7" height="7" rx="1.5"/>
|
||||||
|
<rect x="13" y="13" width="7" height="7" rx="1.5"/>
|
||||||
|
<path d="M11 4v4h-4M13 20v-4h4"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Spread / Fan-out icon -->
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<circle cx="6" cy="6" r="2.5"/>
|
||||||
|
<circle cx="18" cy="6" r="2.5"/>
|
||||||
|
<circle cx="6" cy="18" r="2.5"/>
|
||||||
|
<circle cx="18" cy="18" r="2.5"/>
|
||||||
|
<path d="M8 8l2.5 2.5M16 8l-2.5 2.5M8 16l2.5-2.5M16 16l-2.5-2.5" opacity="0.5"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Top-Left: Properties count badge ── -->
|
<!-- ── Top-Left: Properties count badge ── -->
|
||||||
<div class="map-overlay count-badge">
|
<div class="map-overlay count-badge">
|
||||||
<i class="pi pi-map-marker text-indigo-500"></i>
|
<i class="pi pi-map-marker text-indigo-500"></i>
|
||||||
<span>{{ propertiesWithCoords.length }} ملک روی نقشه</span>
|
<span>{{ propertiesWithCoords.length }} ملک</span>
|
||||||
|
<span v-if="hasCoLocatedProperties && !isSpreadMode" class="badge-subcount">
|
||||||
|
({{ locationGroups.length }} موقعیت)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Bottom-Left: Coordinate readout ── -->
|
<!-- ── Bottom-Left: Coordinate readout ── -->
|
||||||
@@ -77,59 +107,60 @@
|
|||||||
<!-- Scale control -->
|
<!-- Scale control -->
|
||||||
<l-control-scale position="bottomleft" :imperial="false" :maxWidth="150" />
|
<l-control-scale position="bottomleft" :imperial="false" :maxWidth="150" />
|
||||||
|
|
||||||
<!-- Property Markers with Airbnb Popups -->
|
<!-- ── VIEW A: SPREAD MODE MARKERS (Separated individual pins) ── -->
|
||||||
|
<template v-if="isSpreadMode">
|
||||||
<l-marker
|
<l-marker
|
||||||
v-for="prop in propertiesWithCoords"
|
v-for="item in spreadMarkersList"
|
||||||
:key="prop._id || prop.id"
|
:key="item.prop._id || item.prop.id"
|
||||||
:lat-lng="prop.resolvedCoords"
|
:lat-lng="item.coords"
|
||||||
:icon="getMarkerIcon(prop)"
|
:icon="getIndividualMarkerIcon(item.prop, item.isOffset)"
|
||||||
@click="onMarkerClick(prop)"
|
@click="onMarkerClick(item.prop)"
|
||||||
>
|
>
|
||||||
<l-popup :options="{ maxWidth: 320, minWidth: 270, className: 'airbnb-custom-popup' }">
|
<l-popup :options="{ maxWidth: 330, minWidth: 280, className: 'airbnb-custom-popup' }">
|
||||||
<div class="airbnb-popup-card">
|
<div class="airbnb-popup-card">
|
||||||
<!-- Image / Header -->
|
<!-- Image / Header -->
|
||||||
<div class="popup-image-container">
|
<div class="popup-image-container">
|
||||||
<img
|
<img
|
||||||
:src="getPropertyImage(prop)"
|
:src="getPropertyImage(item.prop)"
|
||||||
:alt="prop.name"
|
:alt="item.prop.name"
|
||||||
class="popup-image"
|
class="popup-image"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<span v-if="getCategoryName(prop)" class="popup-category-badge">
|
<span v-if="getCategoryName(item.prop)" class="popup-category-badge">
|
||||||
{{ getCategoryName(prop) }}
|
{{ getCategoryName(item.prop) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="prop.propertyCode" class="popup-code-badge">
|
<span v-if="item.prop.propertyCode" class="popup-code-badge">
|
||||||
کد: {{ prop.propertyCode }}
|
کد: {{ item.prop.propertyCode }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Body -->
|
<!-- Card Body -->
|
||||||
<div class="popup-content-body">
|
<div class="popup-content-body">
|
||||||
<h4 class="popup-title">{{ prop.name || 'ملک بدون نام' }}</h4>
|
<h4 class="popup-title">{{ item.prop.name || 'ملک بدون نام' }}</h4>
|
||||||
|
|
||||||
<div class="popup-info-grid">
|
<div class="popup-info-grid">
|
||||||
<div v-if="prop.nationalGeoCode" class="popup-info-item">
|
<div v-if="item.prop.nationalGeoCode" class="popup-info-item">
|
||||||
<span class="info-label">شناسه ملی جغرافیایی:</span>
|
<span class="info-label">شناسه ملی جغرافیایی:</span>
|
||||||
<span class="info-value text-mono">{{ prop.nationalGeoCode }}</span>
|
<span class="info-value text-mono">{{ item.prop.nationalGeoCode }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="prop.dong" class="popup-info-item">
|
<div v-if="item.prop.dong" class="popup-info-item">
|
||||||
<span class="info-label">سهم دانگ:</span>
|
<span class="info-label">سهم دانگ:</span>
|
||||||
<span class="info-value">{{ prop.dong }} دانگ</span>
|
<span class="info-value text-indigo-600 font-bold">{{ item.prop.dong }} دانگ</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="prop.zoningType" class="popup-info-item">
|
<div v-if="item.prop.zoningType" class="popup-info-item">
|
||||||
<span class="info-label">کاربری:</span>
|
<span class="info-label">کاربری:</span>
|
||||||
<span class="info-value">{{ prop.zoningType }}</span>
|
<span class="info-value">{{ item.prop.zoningType }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="prop.address" class="popup-info-item full-width">
|
<div v-if="item.prop.address" class="popup-info-item full-width">
|
||||||
<span class="info-label">آدرس:</span>
|
<span class="info-label">آدرس:</span>
|
||||||
<span class="info-value">{{ prop.address }}</span>
|
<span class="info-value">{{ item.prop.address }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="popup-actions">
|
<div class="popup-actions">
|
||||||
<a
|
<a
|
||||||
:href="'/property/edit-property/' + (prop._id || prop.id)"
|
:href="'/property/edit-property/' + (item.prop._id || item.prop.id)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="popup-btn popup-btn-external"
|
class="popup-btn popup-btn-external"
|
||||||
@@ -141,7 +172,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="popup-btn popup-btn-primary"
|
class="popup-btn popup-btn-primary"
|
||||||
@click="onEditClick(prop)"
|
@click="onEditClick(item.prop)"
|
||||||
title="ویرایش ملک"
|
title="ویرایش ملک"
|
||||||
>
|
>
|
||||||
<i class="pi pi-pencil"></i>
|
<i class="pi pi-pencil"></i>
|
||||||
@@ -152,6 +183,145 @@
|
|||||||
</div>
|
</div>
|
||||||
</l-popup>
|
</l-popup>
|
||||||
</l-marker>
|
</l-marker>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ── VIEW B: GROUPED MODE MARKERS (Default Smart Grouping & Multi-Property Card) ── -->
|
||||||
|
<template v-else>
|
||||||
|
<l-marker
|
||||||
|
v-for="group in locationGroups"
|
||||||
|
:key="group.id"
|
||||||
|
:lat-lng="group.coords"
|
||||||
|
:icon="getGroupMarkerIcon(group)"
|
||||||
|
@click="onGroupMarkerClick(group)"
|
||||||
|
>
|
||||||
|
<l-popup :options="{ maxWidth: 340, minWidth: 290, className: 'airbnb-custom-popup' }">
|
||||||
|
<div class="airbnb-popup-card">
|
||||||
|
<!-- Multi-Property Group Header & Switcher (If multiple properties share this location) -->
|
||||||
|
<div v-if="group.isGroup" class="popup-group-header">
|
||||||
|
<div class="group-header-top">
|
||||||
|
<div class="group-header-title">
|
||||||
|
<span class="group-header-icon">🏢</span>
|
||||||
|
<span>موقعیت مشترک: {{ group.count }} سهم / ملک</span>
|
||||||
|
</div>
|
||||||
|
<div class="group-nav-controls">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="group-nav-btn"
|
||||||
|
@click.stop="prevGroupProperty(group)"
|
||||||
|
title="سهم قبلی"
|
||||||
|
>
|
||||||
|
<i class="pi pi-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
<span class="group-step-indicator">
|
||||||
|
{{ getActiveGroupIndex(group) + 1 }} از {{ group.count }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="group-nav-btn"
|
||||||
|
@click.stop="nextGroupProperty(group)"
|
||||||
|
title="سهم بعدی"
|
||||||
|
>
|
||||||
|
<i class="pi pi-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Property Tabs Chips -->
|
||||||
|
<div class="group-tabs-bar">
|
||||||
|
<button
|
||||||
|
v-for="(p, idx) in group.properties"
|
||||||
|
:key="p._id || p.id"
|
||||||
|
type="button"
|
||||||
|
class="group-tab-chip"
|
||||||
|
:class="{ 'is-active': getActiveGroupIndex(group) === idx }"
|
||||||
|
@click.stop="setActiveGroupIndex(group, idx)"
|
||||||
|
>
|
||||||
|
<span class="tab-chip-code">
|
||||||
|
{{ p.propertyCode ? `#${p.propertyCode}` : `سهم ${idx + 1}` }}
|
||||||
|
</span>
|
||||||
|
<span v-if="p.dong" class="tab-chip-dong">
|
||||||
|
{{ p.dong }} دانگ
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Currently Active Property in this Group -->
|
||||||
|
<template v-if="getActiveProperty(group)">
|
||||||
|
<!-- Image / Header -->
|
||||||
|
<div class="popup-image-container">
|
||||||
|
<img
|
||||||
|
:src="getPropertyImage(getActiveProperty(group))"
|
||||||
|
:alt="getActiveProperty(group).name"
|
||||||
|
class="popup-image"
|
||||||
|
@error="handleImageError"
|
||||||
|
/>
|
||||||
|
<span v-if="getCategoryName(getActiveProperty(group))" class="popup-category-badge">
|
||||||
|
{{ getCategoryName(getActiveProperty(group)) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="getActiveProperty(group).propertyCode" class="popup-code-badge">
|
||||||
|
کد: {{ getActiveProperty(group).propertyCode }}
|
||||||
|
</span>
|
||||||
|
<span v-if="group.isGroup && getActiveProperty(group).dong" class="popup-dong-badge">
|
||||||
|
سهم: {{ getActiveProperty(group).dong }} دانگ
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card Body -->
|
||||||
|
<div class="popup-content-body">
|
||||||
|
<h4 class="popup-title">
|
||||||
|
{{ getActiveProperty(group).name || 'ملک بدون نام' }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="popup-info-grid">
|
||||||
|
<div v-if="getActiveProperty(group).nationalGeoCode" class="popup-info-item">
|
||||||
|
<span class="info-label">شناسه ملی جغرافیایی:</span>
|
||||||
|
<span class="info-value text-mono">{{ getActiveProperty(group).nationalGeoCode }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="getActiveProperty(group).dong" class="popup-info-item">
|
||||||
|
<span class="info-label">میزان مالکیت:</span>
|
||||||
|
<span class="info-value text-indigo-600 font-bold">
|
||||||
|
{{ getActiveProperty(group).dong }} دانگ ({{ (parseFloat(getActiveProperty(group).dong) / 6 * 100).toFixed(0) }}٪)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="getActiveProperty(group).zoningType" class="popup-info-item">
|
||||||
|
<span class="info-label">کاربری:</span>
|
||||||
|
<span class="info-value">{{ getActiveProperty(group).zoningType }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="getActiveProperty(group).address" class="popup-info-item full-width">
|
||||||
|
<span class="info-label">آدرس:</span>
|
||||||
|
<span class="info-value">{{ getActiveProperty(group).address }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="popup-actions">
|
||||||
|
<a
|
||||||
|
:href="'/property/edit-property/' + (getActiveProperty(group)._id || getActiveProperty(group).id)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="popup-btn popup-btn-external"
|
||||||
|
title="باز کردن در برگه جدید"
|
||||||
|
>
|
||||||
|
<i class="pi pi-external-link"></i>
|
||||||
|
<span>باز کردن در تب جدید</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="popup-btn popup-btn-primary"
|
||||||
|
@click="onEditClick(getActiveProperty(group))"
|
||||||
|
title="ویرایش ملک"
|
||||||
|
>
|
||||||
|
<i class="pi pi-pencil"></i>
|
||||||
|
<span>ویرایش</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</l-popup>
|
||||||
|
</l-marker>
|
||||||
|
</template>
|
||||||
</l-map>
|
</l-map>
|
||||||
|
|
||||||
<!-- Locating overlay -->
|
<!-- Locating overlay -->
|
||||||
@@ -209,6 +379,10 @@ const hoverCoords = ref(null);
|
|||||||
const isLocating = ref(false);
|
const isLocating = ref(false);
|
||||||
const isFullscreen = ref(false);
|
const isFullscreen = ref(false);
|
||||||
const mapReady = ref(false);
|
const mapReady = ref(false);
|
||||||
|
const isSpreadMode = ref(false);
|
||||||
|
|
||||||
|
// Active property index tracking for multi-property location groups: { [groupId]: activePropertyIndex }
|
||||||
|
const activeGroupIndexMap = ref({});
|
||||||
|
|
||||||
// Default fallback image
|
// Default fallback image
|
||||||
const DEFAULT_IMAGE = 'https://cloud.mbmanager.ir/images/item-no-image.png';
|
const DEFAULT_IMAGE = 'https://cloud.mbmanager.ir/images/item-no-image.png';
|
||||||
@@ -246,7 +420,7 @@ const resolveCoordinates = (prop) => {
|
|||||||
const c1 = parseFloat(rawCoords[1]);
|
const c1 = parseFloat(rawCoords[1]);
|
||||||
|
|
||||||
if (!isNaN(c0) && !isNaN(c1) && !(c0 === 0 && c1 === 0)) {
|
if (!isNaN(c0) && !isNaN(c1) && !(c0 === 0 && c1 === 0)) {
|
||||||
// Check orientation: Iran latitude is ~25-40, longitude is ~44-68
|
// Check orientation: Iran latitude is ~20-45, longitude is ~40-68
|
||||||
if (c0 >= 20 && c0 <= 45 && c1 >= 40 && c1 <= 68) {
|
if (c0 >= 20 && c0 <= 45 && c1 >= 40 && c1 <= 68) {
|
||||||
return [c0, c1]; // [lat, lng]
|
return [c0, c1]; // [lat, lng]
|
||||||
}
|
}
|
||||||
@@ -297,8 +471,91 @@ const propertiesWithCoords = computed(() => {
|
|||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group properties by identical / near-identical coordinates (~1m precision)
|
||||||
|
*/
|
||||||
|
const locationGroups = computed(() => {
|
||||||
|
const groupsMap = new Map();
|
||||||
|
|
||||||
|
propertiesWithCoords.value.forEach((prop) => {
|
||||||
|
const [lat, lng] = prop.resolvedCoords;
|
||||||
|
// Group key with 5 decimal places (~1.1 meter precision)
|
||||||
|
const key = `${lat.toFixed(5)},${lng.toFixed(5)}`;
|
||||||
|
|
||||||
|
if (!groupsMap.has(key)) {
|
||||||
|
groupsMap.set(key, {
|
||||||
|
id: key,
|
||||||
|
coords: [lat, lng],
|
||||||
|
properties: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groupsMap.get(key).properties.push(prop);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groupsMap.values()).map((group) => {
|
||||||
|
const count = group.properties.length;
|
||||||
|
const codes = group.properties.map((p) => (p.propertyCode ? `#${p.propertyCode}` : null)).filter(Boolean);
|
||||||
|
const totalDong = group.properties.reduce((sum, p) => sum + (parseFloat(p.dong) || 0), 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
count,
|
||||||
|
codes,
|
||||||
|
totalDong,
|
||||||
|
isGroup: count > 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there is at least one location with 2 or more properties
|
||||||
|
*/
|
||||||
|
const hasCoLocatedProperties = computed(() => {
|
||||||
|
return locationGroups.value.some((g) => g.count > 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spread / Spiderfy offset calculator for Spread Mode
|
||||||
|
*/
|
||||||
|
const spreadMarkersList = computed(() => {
|
||||||
|
const list = [];
|
||||||
|
const radius = 0.00018; // ~18 meters offset
|
||||||
|
|
||||||
|
locationGroups.value.forEach((group) => {
|
||||||
|
if (group.count === 1) {
|
||||||
|
list.push({
|
||||||
|
prop: group.properties[0],
|
||||||
|
coords: group.coords,
|
||||||
|
isOffset: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const [baseLat, baseLng] = group.coords;
|
||||||
|
const N = group.count;
|
||||||
|
|
||||||
|
group.properties.forEach((prop, i) => {
|
||||||
|
const angle = (2 * Math.PI * i) / N - Math.PI / 2;
|
||||||
|
const latOffset = radius * Math.cos(angle);
|
||||||
|
const lngOffset = (radius * Math.sin(angle)) / Math.cos((baseLat * Math.PI) / 180);
|
||||||
|
|
||||||
|
list.push({
|
||||||
|
prop,
|
||||||
|
coords: [baseLat + latOffset, baseLng + lngOffset],
|
||||||
|
isOffset: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleSpreadMode = () => {
|
||||||
|
isSpreadMode.value = !isSpreadMode.value;
|
||||||
|
};
|
||||||
|
|
||||||
// Helper functions for property fields
|
// Helper functions for property fields
|
||||||
const getPropertyImage = (prop) => {
|
const getPropertyImage = (prop) => {
|
||||||
|
if (!prop) return DEFAULT_IMAGE;
|
||||||
return prop.imageUrl || prop.mainImageUrls?.[0] || prop.signedMainImageUrls?.[0] || DEFAULT_IMAGE;
|
return prop.imageUrl || prop.mainImageUrls?.[0] || prop.signedMainImageUrls?.[0] || DEFAULT_IMAGE;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -307,6 +564,7 @@ const handleImageError = (e) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCategoryName = (prop) => {
|
const getCategoryName = (prop) => {
|
||||||
|
if (!prop) return '';
|
||||||
if (prop.nativeCategory) return prop.nativeCategory;
|
if (prop.nativeCategory) return prop.nativeCategory;
|
||||||
if (typeof prop.category === 'object' && prop.category?.nativeName) return prop.category.nativeName;
|
if (typeof prop.category === 'object' && prop.category?.nativeName) return prop.category.nativeName;
|
||||||
if (typeof prop.category === 'string') return prop.category;
|
if (typeof prop.category === 'string') return prop.category;
|
||||||
@@ -314,12 +572,57 @@ const getCategoryName = (prop) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates custom Airbnb-style Leaflet pin icon
|
* Group active property index helper
|
||||||
*/
|
*/
|
||||||
const getMarkerIcon = (prop) => {
|
const getActiveGroupIndex = (group) => {
|
||||||
const propId = prop._id || prop.id;
|
if (!group || !group.id) return 0;
|
||||||
const isSelected = props.selectedPropertyId === propId;
|
const idx = activeGroupIndexMap.value[group.id];
|
||||||
const isHovered = props.hoveredPropertyId === propId;
|
if (typeof idx === 'number' && idx >= 0 && idx < group.properties.length) {
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveProperty = (group) => {
|
||||||
|
if (!group || !group.properties || group.properties.length === 0) return null;
|
||||||
|
const idx = getActiveGroupIndex(group);
|
||||||
|
return group.properties[idx] || group.properties[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActiveGroupIndex = (group, index) => {
|
||||||
|
if (!group || !group.properties) return;
|
||||||
|
const boundedIndex = Math.max(0, Math.min(index, group.properties.length - 1));
|
||||||
|
activeGroupIndexMap.value[group.id] = boundedIndex;
|
||||||
|
|
||||||
|
const prop = group.properties[boundedIndex];
|
||||||
|
if (prop) {
|
||||||
|
emit('select-property', prop._id || prop.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextGroupProperty = (group) => {
|
||||||
|
const currentIdx = getActiveGroupIndex(group);
|
||||||
|
const nextIdx = (currentIdx + 1) % group.properties.length;
|
||||||
|
setActiveGroupIndex(group, nextIdx);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevGroupProperty = (group) => {
|
||||||
|
const currentIdx = getActiveGroupIndex(group);
|
||||||
|
const prevIdx = (currentIdx - 1 + group.properties.length) % group.properties.length;
|
||||||
|
setActiveGroupIndex(group, prevIdx);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates custom Leaflet pin icon for Location Groups (Smart Multi-Property Pill)
|
||||||
|
*/
|
||||||
|
const getGroupMarkerIcon = (group) => {
|
||||||
|
const propIds = group.properties.map((p) => p._id || p.id);
|
||||||
|
const isSelected = propIds.includes(props.selectedPropertyId);
|
||||||
|
const isHovered = propIds.includes(props.hoveredPropertyId);
|
||||||
|
|
||||||
|
if (!group.isGroup) {
|
||||||
|
// Single property pin
|
||||||
|
const prop = group.properties[0];
|
||||||
const code = prop.propertyCode ? `#${prop.propertyCode}` : '';
|
const code = prop.propertyCode ? `#${prop.propertyCode}` : '';
|
||||||
const cat = getCategoryName(prop) || 'ملک';
|
const cat = getCategoryName(prop) || 'ملک';
|
||||||
|
|
||||||
@@ -336,8 +639,66 @@ const getMarkerIcon = (prop) => {
|
|||||||
return L.divIcon({
|
return L.divIcon({
|
||||||
className: 'airbnb-marker-wrapper',
|
className: 'airbnb-marker-wrapper',
|
||||||
html: html,
|
html: html,
|
||||||
iconSize: [64, 38],
|
iconSize: [68, 38],
|
||||||
iconAnchor: [32, 38],
|
iconAnchor: [34, 38],
|
||||||
|
popupAnchor: [0, -38]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-property group pin
|
||||||
|
let labelText = '';
|
||||||
|
if (group.codes.length > 0 && group.codes.length <= 2) {
|
||||||
|
labelText = group.codes.join(' • ');
|
||||||
|
} else {
|
||||||
|
labelText = `${group.count} سهم`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="airbnb-pin is-multi-pin ${isSelected ? 'is-selected' : ''} ${isHovered ? 'is-hovered' : ''}">
|
||||||
|
<div class="pin-content">
|
||||||
|
<span class="pin-icon">🏢</span>
|
||||||
|
<span class="pin-label">${labelText}</span>
|
||||||
|
<span class="group-count-badge" title="${group.count} ملک در این موقعیت">${group.count}</span>
|
||||||
|
</div>
|
||||||
|
<div class="pin-pointer"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return L.divIcon({
|
||||||
|
className: 'airbnb-marker-wrapper',
|
||||||
|
html: html,
|
||||||
|
iconSize: [84, 40],
|
||||||
|
iconAnchor: [42, 40],
|
||||||
|
popupAnchor: [0, -40]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates custom Leaflet pin icon for Individual (Spread Mode) Markers
|
||||||
|
*/
|
||||||
|
const getIndividualMarkerIcon = (prop, isOffset = false) => {
|
||||||
|
const propId = prop._id || prop.id;
|
||||||
|
const isSelected = props.selectedPropertyId === propId;
|
||||||
|
const isHovered = props.hoveredPropertyId === propId;
|
||||||
|
const code = prop.propertyCode ? `#${prop.propertyCode}` : '';
|
||||||
|
const cat = getCategoryName(prop) || 'ملک';
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="airbnb-pin ${isOffset ? 'is-spread-pin' : ''} ${isSelected ? 'is-selected' : ''} ${isHovered ? 'is-hovered' : ''}">
|
||||||
|
<div class="pin-content">
|
||||||
|
<span class="pin-icon">🏠</span>
|
||||||
|
<span class="pin-label">${code || cat}</span>
|
||||||
|
${prop.dong ? `<span class="pin-dong-tag">${prop.dong}د</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="pin-pointer"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return L.divIcon({
|
||||||
|
className: 'airbnb-marker-wrapper',
|
||||||
|
html: html,
|
||||||
|
iconSize: [68, 38],
|
||||||
|
iconAnchor: [34, 38],
|
||||||
popupAnchor: [0, -38]
|
popupAnchor: [0, -38]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -371,7 +732,15 @@ const onMarkerClick = (prop) => {
|
|||||||
emit('select-property', prop._id || prop.id);
|
emit('select-property', prop._id || prop.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onGroupMarkerClick = (group) => {
|
||||||
|
const activeProp = getActiveProperty(group);
|
||||||
|
if (activeProp) {
|
||||||
|
emit('select-property', activeProp._id || activeProp.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onEditClick = (prop) => {
|
const onEditClick = (prop) => {
|
||||||
|
if (!prop) return;
|
||||||
const id = prop._id || prop.id;
|
const id = prop._id || prop.id;
|
||||||
emit('edit-property', id);
|
emit('edit-property', id);
|
||||||
router.push(`/property/edit-property/${id}`);
|
router.push(`/property/edit-property/${id}`);
|
||||||
@@ -442,11 +811,19 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for selected property changes to zoom to it
|
// Watch for selected property changes to zoom and sync group active index
|
||||||
watch(
|
watch(
|
||||||
() => props.selectedPropertyId,
|
() => props.selectedPropertyId,
|
||||||
(newId) => {
|
(newId) => {
|
||||||
if (newId) {
|
if (newId) {
|
||||||
|
// Find which group contains this property
|
||||||
|
locationGroups.value.forEach((group) => {
|
||||||
|
const idx = group.properties.findIndex((p) => (p._id || p.id) === newId);
|
||||||
|
if (idx !== -1) {
|
||||||
|
activeGroupIndexMap.value[group.id] = idx;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
flyToProperty(newId);
|
flyToProperty(newId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -515,6 +892,12 @@ defineExpose({
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-subcount {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
top: 14px;
|
top: 14px;
|
||||||
right: 14px;
|
right: 14px;
|
||||||
@@ -571,6 +954,19 @@ defineExpose({
|
|||||||
box-shadow: 0 6px 20px rgba(5, 150, 105, 0.45);
|
box-shadow: 0 6px 20px rgba(5, 150, 105, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.map-btn-spread {
|
||||||
|
background: #8b5cf6;
|
||||||
|
box-shadow: 0 4px 14px rgba(139, 92, 246, 0.35);
|
||||||
|
}
|
||||||
|
.map-btn-spread:hover:not(:disabled) {
|
||||||
|
background: #7c3aed;
|
||||||
|
box-shadow: 0 6px 20px rgba(124, 58, 237, 0.45);
|
||||||
|
}
|
||||||
|
.map-btn-spread.is-active {
|
||||||
|
background: #ec4899;
|
||||||
|
box-shadow: 0 4px 14px rgba(236, 72, 153, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.map-btn.locating {
|
.map-btn.locating {
|
||||||
animation: pulse-btn 1s infinite;
|
animation: pulse-btn 1s infinite;
|
||||||
}
|
}
|
||||||
@@ -637,11 +1033,12 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.airbnb-pin {
|
.airbnb-pin {
|
||||||
display: flex;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
|
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.airbnb-pin:hover,
|
.airbnb-pin:hover,
|
||||||
@@ -651,17 +1048,18 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.airbnb-pin.is-selected {
|
.airbnb-pin.is-selected {
|
||||||
transform: scale(1.2) translateY(-6px);
|
transform: scale(1.22) translateY(-6px);
|
||||||
z-index: 1001 !important;
|
z-index: 1001 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin-content {
|
.pin-content {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #1e293b;
|
color: #1e293b;
|
||||||
padding: 5px 10px;
|
padding: 5px 11px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -671,6 +1069,64 @@ defineExpose({
|
|||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Multi-Property Pin Layered Styling ── */
|
||||||
|
.airbnb-pin.is-multi-pin .pin-content {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||||
|
border: 2px solid #6366f1;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 16px rgba(99, 102, 241, 0.3),
|
||||||
|
3px -3px 0 0 #4338ca,
|
||||||
|
6px -6px 0 0 #312e81;
|
||||||
|
padding-right: 8px;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-count-badge {
|
||||||
|
position: absolute;
|
||||||
|
left: -8px;
|
||||||
|
top: -8px;
|
||||||
|
background: linear-gradient(135deg, #ec4899, #8b5cf6);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
min-width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(236, 72, 153, 0.5);
|
||||||
|
border: 1.5px solid #ffffff;
|
||||||
|
animation: badge-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes badge-pop {
|
||||||
|
0% { transform: scale(0); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.airbnb-pin.is-multi-pin:hover .pin-content,
|
||||||
|
.airbnb-pin.is-multi-pin.is-hovered .pin-content {
|
||||||
|
background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow:
|
||||||
|
0 8px 24px rgba(79, 70, 229, 0.45),
|
||||||
|
4px -4px 0 0 #3730a3,
|
||||||
|
8px -8px 0 0 #1e1b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.airbnb-pin.is-multi-pin.is-selected .pin-content {
|
||||||
|
background: linear-gradient(135deg, #4338ca 0%, #4f46e5 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #ffffff;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 3px rgba(99, 102, 241, 0.45),
|
||||||
|
0 10px 28px rgba(79, 70, 229, 0.55),
|
||||||
|
4px -4px 0 0 #3730a3,
|
||||||
|
8px -8px 0 0 #1e1b4b;
|
||||||
|
}
|
||||||
|
|
||||||
.airbnb-pin:hover .pin-content,
|
.airbnb-pin:hover .pin-content,
|
||||||
.airbnb-pin.is-hovered .pin-content {
|
.airbnb-pin.is-hovered .pin-content {
|
||||||
background: #4f46e5;
|
background: #4f46e5;
|
||||||
@@ -689,6 +1145,21 @@ defineExpose({
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pin-dong-tag {
|
||||||
|
background: rgba(79, 70, 229, 0.12);
|
||||||
|
color: #4f46e5;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.airbnb-pin:hover .pin-dong-tag,
|
||||||
|
.airbnb-pin.is-selected .pin-dong-tag {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.pin-pointer {
|
.pin-pointer {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
@@ -698,12 +1169,16 @@ defineExpose({
|
|||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.airbnb-pin.is-multi-pin .pin-pointer {
|
||||||
|
border-top-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Airbnb Popup Card ── */
|
/* ── Airbnb Popup Card ── */
|
||||||
.airbnb-custom-popup .leaflet-popup-content-wrapper {
|
.airbnb-custom-popup .leaflet-popup-content-wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22);
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.24);
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-family: 'Estedad', sans-serif !important;
|
font-family: 'Estedad', sans-serif !important;
|
||||||
@@ -728,6 +1203,113 @@ defineExpose({
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Multi-Property Group Popup Header & Switcher ── */
|
||||||
|
.popup-group-header {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding: 10px 14px 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-header-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-nav-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-nav-btn {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #475569;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-nav-btn:hover {
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-step-indicator {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
min-width: 36px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-tabs-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-tab-chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-tab-chip:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-tab-chip.is-active {
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #4f46e5;
|
||||||
|
box-shadow: 0 2px 8px rgba(79, 70, 229, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-chip-dong {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Popup Image & Badges ── */
|
||||||
.popup-image-container {
|
.popup-image-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -774,6 +1356,19 @@ defineExpose({
|
|||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-dong-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
background: rgba(16, 185, 129, 0.9);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-content-body {
|
.popup-content-body {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -884,6 +1479,10 @@ defineExpose({
|
|||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-dark .badge-subcount {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
.app-dark .airbnb-custom-popup .leaflet-popup-content-wrapper {
|
.app-dark .airbnb-custom-popup .leaflet-popup-content-wrapper {
|
||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
@@ -894,6 +1493,43 @@ defineExpose({
|
|||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-dark .popup-group-header {
|
||||||
|
background: #0f172a;
|
||||||
|
border-bottom-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dark .group-header-title {
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dark .group-nav-btn {
|
||||||
|
background: #1e293b;
|
||||||
|
border-color: #475569;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dark .group-nav-btn:hover {
|
||||||
|
background: #6366f1;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dark .group-tab-chip {
|
||||||
|
background: #1e293b;
|
||||||
|
border-color: #334155;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dark .group-tab-chip:hover {
|
||||||
|
background: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-dark .group-tab-chip.is-active {
|
||||||
|
background: #6366f1;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
.app-dark .popup-title {
|
.app-dark .popup-title {
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
}
|
}
|
||||||
@@ -923,4 +1559,13 @@ defineExpose({
|
|||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
border-color: #6366f1;
|
border-color: #6366f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-dark .airbnb-pin.is-multi-pin .pin-content {
|
||||||
|
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
border-color: #818cf8;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 16px rgba(0, 0, 0, 0.4),
|
||||||
|
3px -3px 0 0 #4f46e5,
|
||||||
|
6px -6px 0 0 #3730a3;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user