|
@@ -0,0 +1,262 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="relative h-screen">
|
|
|
|
|
+ <!-- 顶部工具栏 -->
|
|
|
|
|
+ <view class="absolute top-2 left-2 right-2 flex gap-1.5 flex-wrap z-20">
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-white rounded-md" @click="startNewPolygon">新建多边形</button>
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-white rounded-md" @click="finishPolygon">完成当前多边形</button>
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-white rounded-md" @click="toggleSatellite">{{ isSatellite ? '矢量图' : '卫星图' }}</button>
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-white rounded-md" @click="undo">撤销</button>
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-white rounded-md" @click="redo">重做</button>
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-white rounded-md" @click="savePolygons">保存(打印)</button>
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-white rounded-md" @click="loadPolygons">加载(从控制台最后保存)</button>
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-red-500 text-white rounded-md" @click="deleteSelectedPolygon">删除选中多边形</button>
|
|
|
|
|
+ <button class="px-2 py-1.5 bg-white rounded-md" @click="toggleSelectMode">{{ mode === 'select' ? '退出选择模式' : '选择多边形' }}</button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 地图 -->
|
|
|
|
|
+ <map
|
|
|
|
|
+ class="w-screen h-screen"
|
|
|
|
|
+ :latitude="center.lat"
|
|
|
|
|
+ :longitude="center.lng"
|
|
|
|
|
+ :enable-satellite="isSatellite"
|
|
|
|
|
+ :polygons="mapPolygons"
|
|
|
|
|
+ :markers="allMarkers"
|
|
|
|
|
+ @tap="onMapTap"
|
|
|
|
|
+ @markertap="onMarkerTap"
|
|
|
|
|
+ @markerdragend="onMarkerDragEnd"
|
|
|
|
|
+ @polygontap="onPolygonTap"
|
|
|
|
|
+ ></map>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 提示 -->
|
|
|
|
|
+ <view class="absolute left-3 bottom-4.5 bg-black bg-op-50 text-white px-2.5 py-1.5 rounded-md">当前模式:{{ modeText }}</view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import {computed, ref} from 'vue'
|
|
|
|
|
+
|
|
|
|
|
+interface LatLng {
|
|
|
|
|
+ latitude: number;
|
|
|
|
|
+ longitude: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface PolygonItem {
|
|
|
|
|
+ id: number
|
|
|
|
|
+ title?: string
|
|
|
|
|
+ points: LatLng[]
|
|
|
|
|
+ fillColor: string
|
|
|
|
|
+ strokeColor: string
|
|
|
|
|
+ strokeWidth: number
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const center = ref({lat: 28.21, lng: 112.89})
|
|
|
|
|
+const isSatellite = ref(false)
|
|
|
|
|
+const polygons = ref<PolygonItem[]>([])
|
|
|
|
|
+const editingId = ref<number | null>(null)
|
|
|
|
|
+const mode = ref<'idle' | 'editing' | 'select'>('idle')
|
|
|
|
|
+const undoStack = ref<string[]>([])
|
|
|
|
|
+const redoStack = ref<string[]>([])
|
|
|
|
|
+const lastSaved = ref<string | null>(null)
|
|
|
|
|
+
|
|
|
|
|
+// ---------- 选择模式支持 ----------
|
|
|
|
|
+const toggleSelectMode = () => {
|
|
|
|
|
+ mode.value = mode.value === 'select' ? 'idle' : 'select'
|
|
|
|
|
+ editingId.value = null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const mapPolygons = computed(() => {
|
|
|
|
|
+ return polygons.value.map(p => {
|
|
|
|
|
+ const pts = p.points.map(pt => ({latitude: pt.latitude, longitude: pt.longitude}))
|
|
|
|
|
+ if (pts.length > 2) pts.push({...pts[0]})
|
|
|
|
|
+ return {
|
|
|
|
|
+ points: pts,
|
|
|
|
|
+ fillColor: editingId.value === p.id ? '#00FF0033' : p.fillColor, // 选中高亮
|
|
|
|
|
+ strokeColor: editingId.value === p.id ? '#00FF00' : p.strokeColor,
|
|
|
|
|
+ strokeWidth: p.strokeWidth
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const allMarkers = computed(() => {
|
|
|
|
|
+ const markers: any[] = []
|
|
|
|
|
+ polygons.value.forEach(p => {
|
|
|
|
|
+ p.points.forEach((pt, idx) => {
|
|
|
|
|
+ markers.push({
|
|
|
|
|
+ id: Number(`${p.id}${idx}`),
|
|
|
|
|
+ latitude: pt.latitude,
|
|
|
|
|
+ longitude: pt.longitude,
|
|
|
|
|
+ width: 22,
|
|
|
|
|
+ height: 22,
|
|
|
|
|
+ iconPath: '/static/point.png',
|
|
|
|
|
+ draggable: true,
|
|
|
|
|
+ callout: {content: String(idx + 1), color: '#ffffff', bgColor: '#007aff', padding: 4}
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ })
|
|
|
|
|
+ return markers
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const modeText = computed(() => {
|
|
|
|
|
+ if (mode.value === 'idle') return '空闲(点击 新建 多边形 开始绘制)'
|
|
|
|
|
+ if (mode.value === 'editing') return `编辑中:ID=${editingId.value}`
|
|
|
|
|
+ if (mode.value === 'select') return '选择模式:点击多边形选中'
|
|
|
|
|
+ return ''
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const pushHistory = () => {
|
|
|
|
|
+ undoStack.value.push(JSON.stringify(polygons.value));
|
|
|
|
|
+ redoStack.value = []
|
|
|
|
|
+}
|
|
|
|
|
+const undo = () => {
|
|
|
|
|
+ if (undoStack.value.length) {
|
|
|
|
|
+ redoStack.value.push(JSON.stringify(polygons.value));
|
|
|
|
|
+ polygons.value = JSON.parse(undoStack.value.pop()!)
|
|
|
|
|
+ } else uni.showToast({title: '无法撤销', icon: 'none'})
|
|
|
|
|
+}
|
|
|
|
|
+const redo = () => {
|
|
|
|
|
+ if (redoStack.value.length) {
|
|
|
|
|
+ // 当前状态入撤销栈,但不要清空 redoStack
|
|
|
|
|
+ undoStack.value.push(JSON.stringify(polygons.value));
|
|
|
|
|
+ // 从 redoStack 弹出一个快照恢复
|
|
|
|
|
+ polygons.value = JSON.parse(redoStack.value.pop()!);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({title: '无法重做', icon: 'none'});
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const startNewPolygon = () => {
|
|
|
|
|
+ const id = Date.now();
|
|
|
|
|
+ const newPoly: PolygonItem = {
|
|
|
|
|
+ id,
|
|
|
|
|
+ title: `多边形-${polygons.value.length + 1}`,
|
|
|
|
|
+ points: [],
|
|
|
|
|
+ fillColor: '#FF000033',
|
|
|
|
|
+ strokeColor: '#FF0000',
|
|
|
|
|
+ strokeWidth: 2
|
|
|
|
|
+ };
|
|
|
|
|
+ pushHistory();
|
|
|
|
|
+ polygons.value.push(newPoly);
|
|
|
|
|
+ editingId.value = id;
|
|
|
|
|
+ mode.value = 'editing'
|
|
|
|
|
+}
|
|
|
|
|
+const finishPolygon = () => {
|
|
|
|
|
+ if (!editingId.value) return uni.showToast({title: '当前没有正在编辑的多边形', icon: 'none'});
|
|
|
|
|
+ const p = polygons.value.find(x => x.id === editingId.value);
|
|
|
|
|
+ if (!p) return;
|
|
|
|
|
+ if (p.points.length < 3) return uni.showToast({title: '至少需要 3 个点才能完成', icon: 'none'});
|
|
|
|
|
+ editingId.value = null;
|
|
|
|
|
+ mode.value = 'idle'
|
|
|
|
|
+}
|
|
|
|
|
+const deleteSelectedPolygon = () => {
|
|
|
|
|
+ if (!editingId.value) return uni.showToast({title: '请先选中一个多边形(在编辑状态)', icon: 'none'});
|
|
|
|
|
+ pushHistory();
|
|
|
|
|
+ polygons.value = polygons.value.filter(p => p.id !== editingId.value);
|
|
|
|
|
+ editingId.value = null;
|
|
|
|
|
+ mode.value = 'idle'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onMapTap = (e: any) => {
|
|
|
|
|
+ const {latitude, longitude} = e.detail;
|
|
|
|
|
+ if (mode.value === 'editing' && editingId.value != null) {
|
|
|
|
|
+ pushHistory();
|
|
|
|
|
+ const p = polygons.value.find(x => x.id === editingId.value);
|
|
|
|
|
+ if (!p) return;
|
|
|
|
|
+ p.points.push({latitude, longitude})
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+const onMarkerDragEnd = (e: any) => {
|
|
|
|
|
+ const {markerId, latitude, longitude} = e.detail;
|
|
|
|
|
+ let found = false;
|
|
|
|
|
+ for (const p of polygons.value) {
|
|
|
|
|
+ for (let idx = 0; idx < p.points.length; idx++) {
|
|
|
|
|
+ if (Number(`${p.id}${idx}`) === markerId) {
|
|
|
|
|
+ pushHistory();
|
|
|
|
|
+ p.points[idx].latitude = latitude;
|
|
|
|
|
+ p.points[idx].longitude = longitude;
|
|
|
|
|
+ found = true;
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (found) break
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+const onMarkerTap = (e: any) => {
|
|
|
|
|
+ const markerId = e.detail.markerId;
|
|
|
|
|
+ for (const p of polygons.value) {
|
|
|
|
|
+ for (let idx = 0; idx < p.points.length; idx++) {
|
|
|
|
|
+ if (Number(`${p.id}${idx}`) === markerId) {
|
|
|
|
|
+ if (mode.value === 'select') {
|
|
|
|
|
+ editingId.value = p.id;
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ editingId.value = p.id;
|
|
|
|
|
+ mode.value = 'editing';
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '顶点操作',
|
|
|
|
|
+ content: `你点击了多边形 "${p.title}" 的第 ${idx + 1} 个顶点。`,
|
|
|
|
|
+ showCancel: true,
|
|
|
|
|
+ cancelText: '删除点',
|
|
|
|
|
+ confirmText: '取消',
|
|
|
|
|
+ success(res) {
|
|
|
|
|
+ if (res.cancel) {
|
|
|
|
|
+ if (p.points.length <= 3) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '提示', content: '删除会导致顶点不足(<3),是否删除整 polygon?', success(r) {
|
|
|
|
|
+ if (r.confirm) {
|
|
|
|
|
+ pushHistory();
|
|
|
|
|
+ polygons.value = polygons.value.filter(pp => pp.id !== p.id);
|
|
|
|
|
+ editingId.value = null;
|
|
|
|
|
+ mode.value = 'idle'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ pushHistory();
|
|
|
|
|
+ p.points.splice(idx, 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+const onPolygonTap = (e: any) => {
|
|
|
|
|
+ if (mode.value === 'select') {
|
|
|
|
|
+ const idx = e.detail.index;
|
|
|
|
|
+ const p = polygons.value[idx];
|
|
|
|
|
+ if (p) {
|
|
|
|
|
+ editingId.value = p.id
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const toggleSatellite = () => {
|
|
|
|
|
+ isSatellite.value = !isSatellite.value
|
|
|
|
|
+}
|
|
|
|
|
+const savePolygons = () => {
|
|
|
|
|
+ const payload = polygons.value.map(p => ({id: p.id, title: p.title, points: p.points}));
|
|
|
|
|
+ const json = JSON.stringify(payload, null, 2);
|
|
|
|
|
+ console.log('保存多边形:', json);
|
|
|
|
|
+ lastSaved.value = json;
|
|
|
|
|
+ uni.showToast({title: '已打印到控制台(并保存到内存)', icon: 'none'})
|
|
|
|
|
+}
|
|
|
|
|
+const loadPolygons = () => {
|
|
|
|
|
+ if (!lastSaved.value) {
|
|
|
|
|
+ return uni.showToast({title: '没有保存记录,请先点击 保存(打印)', icon: 'none'})
|
|
|
|
|
+ }
|
|
|
|
|
+ pushHistory();
|
|
|
|
|
+ const arr = JSON.parse(lastSaved.value);
|
|
|
|
|
+ polygons.value = arr.map((x: any, idx: number) => ({
|
|
|
|
|
+ id: x.id ?? Date.now() + idx,
|
|
|
|
|
+ title: x.title ?? `多边形-${idx + 1}`,
|
|
|
|
|
+ points: x.points,
|
|
|
|
|
+ fillColor: '#FF000033',
|
|
|
|
|
+ strokeColor: '#FF0000',
|
|
|
|
|
+ strokeWidth: 2
|
|
|
|
|
+ }));
|
|
|
|
|
+ editingId.value = null;
|
|
|
|
|
+ mode.value = 'idle'
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|