- Port all React components to Vue 3 (GwTabs, GwMN, GwCRA, GwCRS, GwMap, GwRelations, GwPerimeterList) - Port hooks to Vue composables (useCesiumProfiles, useSavedPerimeters) - Copy pure TS services and utils (duniter/, ss58, gratewizard utils) - Add Leaflet + Geoman + MarkerCluster dependencies - Serve grateWizard as popup via /gratewizard?popup (layout: false) and info page on /gratewizard (with Librodrome layout) - Remove public/gratewizard-app/ static Next.js export - Refine UI: compact tabs, buttons, inputs, cards, perimeter list - Use Ğ1 breve everywhere, French locale for all dates and amounts - Rename roles: vendeur→offre / acheteur→reçoit le produit ou service - Rename prix→évaluation in all visible text - Add calculated result column in CRA and CRS relation tables - DU/Ğ1 selector uses toggle switch (same as role toggle) - Auto-scroll to monetary data card on polygon selection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
147 lines
3.6 KiB
Vue
147 lines
3.6 KiB
Vue
<template>
|
|
<div ref="containerRef" class="gw-map" />
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import L from 'leaflet';
|
|
import 'leaflet/dist/leaflet.css';
|
|
import 'leaflet.markercluster';
|
|
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
|
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
|
import '@geoman-io/leaflet-geoman-free';
|
|
import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css';
|
|
import type { GeoMember } from '~/composables/useCesiumProfiles';
|
|
|
|
type LoadPolygon = { coords: [number, number][]; name: string } | null;
|
|
|
|
const props = defineProps<{
|
|
members: GeoMember[];
|
|
clearTrigger?: number;
|
|
loadPolygon?: LoadPolygon;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
polygonChange: [polygon: [number, number][] | null, name?: string];
|
|
}>();
|
|
|
|
const containerRef = ref<HTMLDivElement | null>(null);
|
|
let map: L.Map | null = null;
|
|
let cluster: any = null;
|
|
let polygonLayer: L.Layer | null = null;
|
|
|
|
onMounted(() => {
|
|
if (!containerRef.value) return;
|
|
|
|
map = L.map(containerRef.value).setView([46.5, 2.5], 6);
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
|
}).addTo(map);
|
|
|
|
cluster = (L as any).markerClusterGroup();
|
|
map.addLayer(cluster);
|
|
|
|
// Geoman controls
|
|
if (map.pm) {
|
|
map.pm.addControls({
|
|
position: 'topleft',
|
|
drawCircle: false,
|
|
drawCircleMarker: false,
|
|
drawMarker: false,
|
|
drawPolyline: false,
|
|
drawText: false,
|
|
editMode: false,
|
|
dragMode: false,
|
|
cutPolygon: false,
|
|
rotateMode: false,
|
|
removalMode: false,
|
|
});
|
|
map.pm.setLang('fr');
|
|
}
|
|
|
|
map.on('pm:create', (e: any) => {
|
|
if (polygonLayer) {
|
|
map!.removeLayer(polygonLayer);
|
|
}
|
|
polygonLayer = e.layer;
|
|
const latlngs = (e.layer as L.Polygon).getLatLngs()[0] as L.LatLng[];
|
|
emit('polygonChange', latlngs.map((ll) => [ll.lat, ll.lng]));
|
|
});
|
|
|
|
map.on('pm:remove', () => {
|
|
polygonLayer = null;
|
|
emit('polygonChange', null);
|
|
});
|
|
|
|
// Populate initial markers
|
|
updateMarkers(props.members);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (map) {
|
|
map.remove();
|
|
map = null;
|
|
cluster = null;
|
|
}
|
|
});
|
|
|
|
// Clear polygon when clearTrigger changes
|
|
watch(() => props.clearTrigger, () => {
|
|
if (!props.clearTrigger || !map) return;
|
|
if (polygonLayer) {
|
|
const center = map.getCenter();
|
|
const zoom = map.getZoom();
|
|
map.removeLayer(polygonLayer);
|
|
polygonLayer = null;
|
|
map.setView(center, zoom, { animate: false });
|
|
}
|
|
if (map.pm) {
|
|
map.pm.disableDraw();
|
|
}
|
|
});
|
|
|
|
// Load a saved polygon
|
|
watch(() => props.loadPolygon, (loadPolygon) => {
|
|
if (!map || !loadPolygon) return;
|
|
|
|
if (polygonLayer) {
|
|
map.removeLayer(polygonLayer);
|
|
polygonLayer = null;
|
|
}
|
|
|
|
const latlngs = loadPolygon.coords.map(([lat, lng]) => L.latLng(lat, lng));
|
|
const poly = L.polygon(latlngs, { color: '#3388ff' }).addTo(map);
|
|
polygonLayer = poly;
|
|
map.fitBounds(poly.getBounds(), { padding: [20, 20] });
|
|
emit('polygonChange', loadPolygon.coords, loadPolygon.name);
|
|
});
|
|
|
|
// Update markers when members change
|
|
function updateMarkers(members: GeoMember[]) {
|
|
if (!cluster) return;
|
|
cluster.clearLayers();
|
|
const markers = members.map((m) =>
|
|
L.circleMarker([m.lat, m.lon], {
|
|
radius: 6,
|
|
color: '#f59e0b',
|
|
fillColor: '#f59e0b',
|
|
fillOpacity: 0.7,
|
|
}).bindPopup(
|
|
`<strong>${m.title || m.pubkey.slice(0, 8)}</strong>${m.city ? '<br>' + m.city : ''}`
|
|
)
|
|
);
|
|
cluster.addLayers(markers);
|
|
}
|
|
|
|
watch(() => props.members, (members) => {
|
|
updateMarkers(members);
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.gw-map {
|
|
height: 500px;
|
|
width: 100%;
|
|
border-radius: 0.75rem;
|
|
}
|
|
</style>
|