219 lines
4.5 KiB
Vue
219 lines
4.5 KiB
Vue
<template>
|
|
<div class="map-container">
|
|
<div ref="mapElement" class="map" :style="{ height: mapHeight }"></div>
|
|
<div v-if="!latitude || !longitude" class="map-overlay">
|
|
<div class="no-coordinates">
|
|
<i
|
|
class="pi pi-map-marker"
|
|
style="font-size: 2rem; color: #64748b; margin-bottom: 0.5rem"
|
|
></i>
|
|
<p>No coordinates available</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted, watch, nextTick } from "vue";
|
|
import L from "leaflet";
|
|
import "leaflet/dist/leaflet.css";
|
|
|
|
// Fix Leaflet default marker icons
|
|
delete L.Icon.Default.prototype._getIconUrl;
|
|
L.Icon.Default.mergeOptions({
|
|
iconRetinaUrl:
|
|
"https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png",
|
|
iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png",
|
|
shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png",
|
|
});
|
|
|
|
const props = defineProps({
|
|
latitude: {
|
|
type: [Number, String],
|
|
required: false,
|
|
default: null,
|
|
},
|
|
longitude: {
|
|
type: [Number, String],
|
|
required: false,
|
|
default: null,
|
|
},
|
|
addressTitle: {
|
|
type: String,
|
|
default: "Location",
|
|
},
|
|
mapHeight: {
|
|
type: String,
|
|
default: "400px",
|
|
},
|
|
zoomLevel: {
|
|
type: Number,
|
|
default: 15,
|
|
},
|
|
interactive: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
});
|
|
|
|
const mapElement = ref(null);
|
|
let map = null;
|
|
let marker = null;
|
|
|
|
const initializeMap = async () => {
|
|
if (!mapElement.value) return;
|
|
|
|
// Clean up existing map
|
|
if (map) {
|
|
map.remove();
|
|
map = null;
|
|
marker = null;
|
|
}
|
|
|
|
const lat = parseFloat(props.latitude);
|
|
const lng = parseFloat(props.longitude);
|
|
|
|
// Only create map if we have valid coordinates
|
|
if (!isNaN(lat) && !isNaN(lng)) {
|
|
await nextTick();
|
|
|
|
// Initialize map
|
|
map = L.map(mapElement.value, {
|
|
zoomControl: props.interactive,
|
|
dragging: props.interactive,
|
|
touchZoom: props.interactive,
|
|
scrollWheelZoom: props.interactive,
|
|
doubleClickZoom: props.interactive,
|
|
boxZoom: props.interactive,
|
|
keyboard: props.interactive,
|
|
}).setView([lat, lng], props.zoomLevel);
|
|
|
|
// Add tile layer
|
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
maxZoom: 19,
|
|
attribution: "© OpenStreetMap contributors",
|
|
}).addTo(map);
|
|
|
|
// Add marker
|
|
marker = L.marker([lat, lng])
|
|
.addTo(map)
|
|
.bindPopup(
|
|
`
|
|
<div style="text-align: center;">
|
|
<strong>${props.addressTitle}</strong><br>
|
|
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
|
|
</div>
|
|
`,
|
|
)
|
|
.openPopup();
|
|
}
|
|
};
|
|
|
|
const updateMap = () => {
|
|
const lat = parseFloat(props.latitude);
|
|
const lng = parseFloat(props.longitude);
|
|
|
|
if (map && !isNaN(lat) && !isNaN(lng)) {
|
|
// Update map view
|
|
map.setView([lat, lng], props.zoomLevel);
|
|
|
|
// Update marker
|
|
if (marker) {
|
|
marker.setLatLng([lat, lng]);
|
|
marker.setPopupContent(`
|
|
<div style="text-align: center;">
|
|
<strong>${props.addressTitle}</strong><br>
|
|
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
|
|
</div>
|
|
`);
|
|
} else {
|
|
marker = L.marker([lat, lng])
|
|
.addTo(map)
|
|
.bindPopup(
|
|
`
|
|
<div style="text-align: center;">
|
|
<strong>${props.addressTitle}</strong><br>
|
|
<small>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</small>
|
|
</div>
|
|
`,
|
|
)
|
|
.openPopup();
|
|
}
|
|
} else if (!isNaN(lat) && !isNaN(lng)) {
|
|
// Coordinates available but no map yet - initialize
|
|
initializeMap();
|
|
}
|
|
};
|
|
|
|
// Watch for coordinate changes
|
|
watch(
|
|
() => [props.latitude, props.longitude, props.addressTitle],
|
|
() => {
|
|
updateMap();
|
|
},
|
|
{ immediate: false },
|
|
);
|
|
|
|
onMounted(() => {
|
|
initializeMap();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (map) {
|
|
map.remove();
|
|
map = null;
|
|
marker = null;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.map-container {
|
|
position: relative;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--surface-border);
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.map {
|
|
width: 100%;
|
|
z-index: 1;
|
|
}
|
|
|
|
.map-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: var(--surface-ground);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 2;
|
|
}
|
|
|
|
.no-coordinates {
|
|
text-align: center;
|
|
color: var(--text-color-secondary);
|
|
padding: 2rem;
|
|
}
|
|
|
|
.no-coordinates p {
|
|
margin: 0;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Leaflet popup customization */
|
|
:deep(.leaflet-popup-content-wrapper) {
|
|
border-radius: 6px;
|
|
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
:deep(.leaflet-popup-tip) {
|
|
background: white;
|
|
border: none;
|
|
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
|
|
}
|
|
</style>
|