feat: add LeafletMap component (foldable), remove legacy station-map.js
This commit is contained in:
97
resources/js/components/LeafletMap.vue
Normal file
97
resources/js/components/LeafletMap.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<button
|
||||||
|
@click="toggleMap"
|
||||||
|
class="flex items-center gap-2 text-sm font-bold text-[#bb5b3e] hover:text-[#a34a31] transition-colors"
|
||||||
|
>
|
||||||
|
<iconify-icon :icon="isOpen ? 'lucide:chevron-up' : 'lucide:chevron-down'"></iconify-icon>
|
||||||
|
{{ isOpen ? 'Hide map' : 'Show map' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-show="isOpen"
|
||||||
|
ref="mapContainer"
|
||||||
|
class="w-full h-72 rounded-2xl overflow-hidden border border-[#e5ded7] shadow-sm"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, onUnmounted, nextTick } from 'vue'
|
||||||
|
import L from 'leaflet'
|
||||||
|
import 'leaflet/dist/leaflet.css'
|
||||||
|
|
||||||
|
// Fix Leaflet default marker icon path broken by Vite
|
||||||
|
delete L.Icon.Default.prototype._getIconUrl
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
|
||||||
|
iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
|
||||||
|
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
stations: { type: Array, required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapContainer = ref(null)
|
||||||
|
const isOpen = ref(false)
|
||||||
|
let mapInstance = null
|
||||||
|
let markersLayer = null
|
||||||
|
|
||||||
|
function initMap() {
|
||||||
|
if (mapInstance || !mapContainer.value) return
|
||||||
|
|
||||||
|
mapInstance = L.map(mapContainer.value)
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
}).addTo(mapInstance)
|
||||||
|
|
||||||
|
markersLayer = L.layerGroup().addTo(mapInstance)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkers() {
|
||||||
|
if (!mapInstance || !markersLayer) return
|
||||||
|
|
||||||
|
markersLayer.clearLayers()
|
||||||
|
|
||||||
|
if (!props.stations.length) return
|
||||||
|
|
||||||
|
const bounds = []
|
||||||
|
|
||||||
|
props.stations.forEach(station => {
|
||||||
|
const marker = L.marker([station.lat, station.lng])
|
||||||
|
.bindPopup(`<strong>${station.name}</strong><br>${station.price}p`)
|
||||||
|
markersLayer.addLayer(marker)
|
||||||
|
bounds.push([station.lat, station.lng])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (bounds.length) {
|
||||||
|
mapInstance.fitBounds(bounds, { padding: [30, 30] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleMap() {
|
||||||
|
isOpen.value = !isOpen.value
|
||||||
|
|
||||||
|
if (isOpen.value) {
|
||||||
|
await nextTick()
|
||||||
|
initMap()
|
||||||
|
mapInstance.invalidateSize()
|
||||||
|
renderMarkers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.stations, () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
renderMarkers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (mapInstance) {
|
||||||
|
mapInstance.remove()
|
||||||
|
mapInstance = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user