feat: integrate map for report creation
This commit is contained in:
+115
-28
@@ -1,5 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { Category, CreateReportRequest } from '~/types/api'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import L from 'leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
|
||||
// Fix per le icone di Leaflet in Nuxt/Vite
|
||||
const DefaultIcon = L.icon({
|
||||
iconUrl: 'https://unpkg.com/[email protected]/dist/images/marker-icon.png',
|
||||
iconRetinaUrl: 'https://unpkg.com/[email protected]/dist/images/marker-icon-2x.png',
|
||||
shadowUrl: 'https://unpkg.com/[email protected]/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41]
|
||||
});
|
||||
L.Marker.prototype.options.icon = DefaultIcon;
|
||||
|
||||
interface Props {
|
||||
isLoading?: boolean
|
||||
@@ -30,6 +45,66 @@ const errors = ref<Record<string, string>>({})
|
||||
const successMessage = ref(false)
|
||||
const isGettingLocation = ref(false)
|
||||
|
||||
// Map variables
|
||||
const mapContainer = ref<HTMLElement | null>(null)
|
||||
let map: L.Map | null = null
|
||||
let marker: L.Marker | null = null
|
||||
|
||||
const initMap = () => {
|
||||
if (!mapContainer.value || map) return
|
||||
|
||||
// Centro Italia default
|
||||
const defaultLat = 41.8719
|
||||
const defaultLng = 12.5674
|
||||
const zoom = 5
|
||||
|
||||
map = L.map(mapContainer.value).setView([defaultLat, defaultLng], zoom)
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map)
|
||||
|
||||
map.on('click', (e: L.LeafletMouseEvent) => {
|
||||
updateLocation(e.latlng.lat, e.latlng.lng)
|
||||
})
|
||||
|
||||
// Se c'è già una posizione, imposta il marker
|
||||
if (form.value.latitude && form.value.longitude) {
|
||||
updateLocation(form.value.latitude, form.value.longitude, true)
|
||||
}
|
||||
}
|
||||
|
||||
const updateLocation = (lat: number, lng: number, zoomTo = false) => {
|
||||
form.value.latitude = Number(lat.toFixed(6))
|
||||
form.value.longitude = Number(lng.toFixed(6))
|
||||
|
||||
if (map) {
|
||||
if (!marker) {
|
||||
marker = L.marker([lat, lng]).addTo(map)
|
||||
} else {
|
||||
marker.setLatLng([lat, lng])
|
||||
}
|
||||
if (zoomTo) {
|
||||
map.setView([lat, lng], 16)
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.value.location) {
|
||||
errors.value.location = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Map logic is handled by ClientOnly placeholder if needed, but we initialize here
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (map) {
|
||||
map.remove()
|
||||
map = null
|
||||
}
|
||||
})
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
errors.value = {}
|
||||
|
||||
@@ -37,7 +112,7 @@ const validateForm = (): boolean => {
|
||||
errors.value.category_id = 'Seleziona una categoria'
|
||||
}
|
||||
if (!form.value.latitude || !form.value.longitude) {
|
||||
errors.value.location = 'Imposta la posizione'
|
||||
errors.value.location = 'Imposta la posizione sulla mappa'
|
||||
}
|
||||
if (!form.value.address?.trim()) {
|
||||
errors.value.address = 'Inserisci l\'indirizzo'
|
||||
@@ -63,13 +138,9 @@ const getGeolocation = async () => {
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject)
|
||||
})
|
||||
|
||||
form.value.latitude = Number(position.coords.latitude.toFixed(6))
|
||||
form.value.longitude = Number(position.coords.longitude.toFixed(6))
|
||||
|
||||
// Richiedi indirizzo da coordinate (reverse geocoding)
|
||||
// Se il backend non lo fa, userà le coord così come sono
|
||||
updateLocation(position.coords.latitude, position.coords.longitude, true)
|
||||
} catch (error: any) {
|
||||
errors.value.location = 'Impossibile ottenere la posizione. Inserisci manualmente.'
|
||||
errors.value.location = 'Impossibile ottenere la posizione. Clicca sulla mappa per inserirla manualmente.'
|
||||
} finally {
|
||||
isGettingLocation.value = false
|
||||
}
|
||||
@@ -99,26 +170,43 @@ const resetForm = () => {
|
||||
}
|
||||
media.value = []
|
||||
errors.value = {}
|
||||
|
||||
if (marker && map) {
|
||||
map.removeLayer(marker)
|
||||
marker = null
|
||||
map.setView([41.8719, 12.5674], 5)
|
||||
}
|
||||
}
|
||||
|
||||
// Osserva cambio categoria
|
||||
watch(() => form.value.category_id, () => {
|
||||
if (errors.value.category_id) {
|
||||
errors.value.category_id = ''
|
||||
}
|
||||
})
|
||||
|
||||
// Sincronizza input manuali con la mappa
|
||||
watch([() => form.value.latitude, () => form.value.longitude], ([newLat, newLng]) => {
|
||||
if (newLat && newLng && map) {
|
||||
if (!marker || marker.getLatLng().lat !== newLat || marker.getLatLng().lng !== newLng) {
|
||||
if (!marker) {
|
||||
marker = L.marker([newLat, newLng]).addTo(map)
|
||||
} else {
|
||||
marker.setLatLng([newLat, newLng])
|
||||
}
|
||||
map.setView([newLat, newLng], 16)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="report-form">
|
||||
<!-- Successo -->
|
||||
<div v-if="successMessage" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
Segnalazione creata con successo! Reindirizzamento...
|
||||
<button type="button" class="btn-close" @click="successMessage = false"></button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<!-- CATEGORIA -->
|
||||
<div class="mb-4">
|
||||
<label for="category" class="form-label fw-bold">
|
||||
<BIIcon name="it-list" size="sm" color="primary" class="me-2" />
|
||||
@@ -141,20 +229,12 @@ watch(() => form.value.category_id, () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GEOLOCALIZZAZIONE -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">
|
||||
<BIIcon name="it-map-marker-circle" size="sm" color="primary" class="me-2" />
|
||||
Posizione *
|
||||
</label>
|
||||
|
||||
<div v-if="form.latitude && form.longitude" class="alert alert-info mb-3">
|
||||
<strong>Coordinate:</strong> {{ form.latitude }}, {{ form.longitude }}
|
||||
<button type="button" class="btn btn-sm btn-link float-end" @click="form.latitude = null; form.longitude = null">
|
||||
Cancella
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary w-100 mb-3"
|
||||
@@ -166,9 +246,14 @@ watch(() => form.value.category_id, () => {
|
||||
{{ isGettingLocation ? 'Localizzazione in corso...' : 'Usa posizione attuale' }}
|
||||
</button>
|
||||
|
||||
<div class="alert alert-warning mb-3 small" role="alert">
|
||||
Se la geolocalizzazione non funziona, inserisci manualmente coordinate o indirizzo approssimativo.
|
||||
</div>
|
||||
<ClientOnly>
|
||||
<div
|
||||
ref="mapContainer"
|
||||
class="map-container mb-3 border rounded"
|
||||
:class="{ 'border-danger': errors.location }"
|
||||
@vue:mounted="initMap"
|
||||
></div>
|
||||
</ClientOnly>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
@@ -176,7 +261,7 @@ watch(() => form.value.category_id, () => {
|
||||
v-model.number="form.latitude"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
class="form-control"
|
||||
class="form-control form-control-sm"
|
||||
:class="{ 'is-invalid': errors.location }"
|
||||
placeholder="Latitudine (es. 45.5)"
|
||||
required
|
||||
@@ -187,19 +272,18 @@ watch(() => form.value.category_id, () => {
|
||||
v-model.number="form.longitude"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
class="form-control"
|
||||
class="form-control form-control-sm"
|
||||
:class="{ 'is-invalid': errors.location }"
|
||||
placeholder="Longitudine (es. 10.2)"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errors.location" class="invalid-feedback d-block">
|
||||
<div v-if="errors.location" class="invalid-feedback d-block mt-0 mb-3">
|
||||
{{ errors.location }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- INDIRIZZO -->
|
||||
<div class="mb-4">
|
||||
<label for="address" class="form-label fw-bold">
|
||||
<BIIcon name="it-home" size="sm" color="primary" class="me-2" />
|
||||
@@ -219,7 +303,6 @@ watch(() => form.value.category_id, () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DESCRIZIONE -->
|
||||
<div class="mb-4">
|
||||
<label for="description" class="form-label fw-bold">
|
||||
<BIIcon name="it-write" size="sm" color="primary" class="me-2" />
|
||||
@@ -242,7 +325,6 @@ watch(() => form.value.category_id, () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MEDIA -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">
|
||||
<BIIcon name="it-camera" size="sm" color="primary" class="me-2" />
|
||||
@@ -251,7 +333,6 @@ watch(() => form.value.category_id, () => {
|
||||
<MediaUploader v-model="media" :max-files="5" :max-file-size="50" />
|
||||
</div>
|
||||
|
||||
<!-- BOTTONI -->
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-between">
|
||||
<button
|
||||
type="button"
|
||||
@@ -281,6 +362,12 @@ watch(() => form.value.category_id, () => {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
z-index: 1; /* Fix for Bootstrap elements overlapping map */
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
|
||||
Reference in New Issue
Block a user