index.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <template>
  2. <view class="relative h-screen">
  3. <!-- 顶部工具栏 -->
  4. <view class="absolute top-2 left-2 right-2 flex gap-1.5 flex-wrap z-20">
  5. <button class="px-2 py-1.5 bg-white rounded-md" @click="startNewPolygon">新建多边形</button>
  6. <button class="px-2 py-1.5 bg-white rounded-md" @click="finishPolygon">完成当前多边形</button>
  7. <button class="px-2 py-1.5 bg-white rounded-md" @click="toggleSatellite">{{ isSatellite ? '矢量图' : '卫星图' }}</button>
  8. <button class="px-2 py-1.5 bg-white rounded-md" @click="undo">撤销</button>
  9. <button class="px-2 py-1.5 bg-white rounded-md" @click="redo">重做</button>
  10. <button class="px-2 py-1.5 bg-white rounded-md" @click="savePolygons">保存(打印)</button>
  11. <button class="px-2 py-1.5 bg-white rounded-md" @click="loadPolygons">加载(从控制台最后保存)</button>
  12. <button class="px-2 py-1.5 bg-red-500 text-white rounded-md" @click="deleteSelectedPolygon">删除选中多边形</button>
  13. <button class="px-2 py-1.5 bg-white rounded-md" @click="toggleSelectMode">{{ mode === 'select' ? '退出选择模式' : '选择多边形' }}</button>
  14. </view>
  15. <!-- 地图 -->
  16. <map
  17. class="w-screen h-screen"
  18. :latitude="center.lat"
  19. :longitude="center.lng"
  20. :enable-satellite="isSatellite"
  21. :polygons="mapPolygons"
  22. :markers="allMarkers"
  23. @tap="onMapTap"
  24. @markertap="onMarkerTap"
  25. @markerdragend="onMarkerDragEnd"
  26. @polygontap="onPolygonTap"
  27. ></map>
  28. <!-- 提示 -->
  29. <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>
  30. </view>
  31. </template>
  32. <script setup lang="ts">
  33. import {computed, ref} from 'vue'
  34. interface LatLng {
  35. latitude: number;
  36. longitude: number
  37. }
  38. interface PolygonItem {
  39. id: number
  40. title?: string
  41. points: LatLng[]
  42. fillColor: string
  43. strokeColor: string
  44. strokeWidth: number
  45. }
  46. const center = ref({lat: 28.21, lng: 112.89})
  47. const isSatellite = ref(false)
  48. const polygons = ref<PolygonItem[]>([])
  49. const editingId = ref<number | null>(null)
  50. const mode = ref<'idle' | 'editing' | 'select'>('idle')
  51. const undoStack = ref<string[]>([])
  52. const redoStack = ref<string[]>([])
  53. const lastSaved = ref<string | null>(null)
  54. // ---------- 选择模式支持 ----------
  55. const toggleSelectMode = () => {
  56. mode.value = mode.value === 'select' ? 'idle' : 'select'
  57. editingId.value = null
  58. }
  59. const mapPolygons = computed(() => {
  60. return polygons.value.map(p => {
  61. const pts = p.points.map(pt => ({latitude: pt.latitude, longitude: pt.longitude}))
  62. if (pts.length > 2) pts.push({...pts[0]})
  63. return {
  64. points: pts,
  65. fillColor: editingId.value === p.id ? '#00FF0033' : p.fillColor, // 选中高亮
  66. strokeColor: editingId.value === p.id ? '#00FF00' : p.strokeColor,
  67. strokeWidth: p.strokeWidth
  68. }
  69. })
  70. })
  71. const allMarkers = computed(() => {
  72. const markers: any[] = []
  73. polygons.value.forEach(p => {
  74. p.points.forEach((pt, idx) => {
  75. markers.push({
  76. id: Number(`${p.id}${idx}`),
  77. latitude: pt.latitude,
  78. longitude: pt.longitude,
  79. width: 22,
  80. height: 22,
  81. iconPath: '/static/point.png',
  82. draggable: true,
  83. callout: {content: String(idx + 1), color: '#ffffff', bgColor: '#007aff', padding: 4}
  84. })
  85. })
  86. })
  87. return markers
  88. })
  89. const modeText = computed(() => {
  90. if (mode.value === 'idle') return '空闲(点击 新建 多边形 开始绘制)'
  91. if (mode.value === 'editing') return `编辑中:ID=${editingId.value}`
  92. if (mode.value === 'select') return '选择模式:点击多边形选中'
  93. return ''
  94. })
  95. const pushHistory = () => {
  96. undoStack.value.push(JSON.stringify(polygons.value));
  97. redoStack.value = []
  98. }
  99. const undo = () => {
  100. if (undoStack.value.length) {
  101. redoStack.value.push(JSON.stringify(polygons.value));
  102. polygons.value = JSON.parse(undoStack.value.pop()!)
  103. } else uni.showToast({title: '无法撤销', icon: 'none'})
  104. }
  105. const redo = () => {
  106. if (redoStack.value.length) {
  107. // 当前状态入撤销栈,但不要清空 redoStack
  108. undoStack.value.push(JSON.stringify(polygons.value));
  109. // 从 redoStack 弹出一个快照恢复
  110. polygons.value = JSON.parse(redoStack.value.pop()!);
  111. } else {
  112. uni.showToast({title: '无法重做', icon: 'none'});
  113. }
  114. }
  115. const startNewPolygon = () => {
  116. const id = Date.now();
  117. const newPoly: PolygonItem = {
  118. id,
  119. title: `多边形-${polygons.value.length + 1}`,
  120. points: [],
  121. fillColor: '#FF000033',
  122. strokeColor: '#FF0000',
  123. strokeWidth: 2
  124. };
  125. pushHistory();
  126. polygons.value.push(newPoly);
  127. editingId.value = id;
  128. mode.value = 'editing'
  129. }
  130. const finishPolygon = () => {
  131. if (!editingId.value) return uni.showToast({title: '当前没有正在编辑的多边形', icon: 'none'});
  132. const p = polygons.value.find(x => x.id === editingId.value);
  133. if (!p) return;
  134. if (p.points.length < 3) return uni.showToast({title: '至少需要 3 个点才能完成', icon: 'none'});
  135. editingId.value = null;
  136. mode.value = 'idle'
  137. }
  138. const deleteSelectedPolygon = () => {
  139. if (!editingId.value) return uni.showToast({title: '请先选中一个多边形(在编辑状态)', icon: 'none'});
  140. pushHistory();
  141. polygons.value = polygons.value.filter(p => p.id !== editingId.value);
  142. editingId.value = null;
  143. mode.value = 'idle'
  144. }
  145. const onMapTap = (e: any) => {
  146. const {latitude, longitude} = e.detail;
  147. if (mode.value === 'editing' && editingId.value != null) {
  148. pushHistory();
  149. const p = polygons.value.find(x => x.id === editingId.value);
  150. if (!p) return;
  151. p.points.push({latitude, longitude})
  152. }
  153. }
  154. const onMarkerDragEnd = (e: any) => {
  155. const {markerId, latitude, longitude} = e.detail;
  156. let found = false;
  157. for (const p of polygons.value) {
  158. for (let idx = 0; idx < p.points.length; idx++) {
  159. if (Number(`${p.id}${idx}`) === markerId) {
  160. pushHistory();
  161. p.points[idx].latitude = latitude;
  162. p.points[idx].longitude = longitude;
  163. found = true;
  164. break
  165. }
  166. }
  167. if (found) break
  168. }
  169. }
  170. const onMarkerTap = (e: any) => {
  171. const markerId = e.detail.markerId;
  172. for (const p of polygons.value) {
  173. for (let idx = 0; idx < p.points.length; idx++) {
  174. if (Number(`${p.id}${idx}`) === markerId) {
  175. if (mode.value === 'select') {
  176. editingId.value = p.id;
  177. return
  178. }
  179. editingId.value = p.id;
  180. mode.value = 'editing';
  181. uni.showModal({
  182. title: '顶点操作',
  183. content: `你点击了多边形 "${p.title}" 的第 ${idx + 1} 个顶点。`,
  184. showCancel: true,
  185. cancelText: '删除点',
  186. confirmText: '取消',
  187. success(res) {
  188. if (res.cancel) {
  189. if (p.points.length <= 3) {
  190. uni.showModal({
  191. title: '提示', content: '删除会导致顶点不足(<3),是否删除整 polygon?', success(r) {
  192. if (r.confirm) {
  193. pushHistory();
  194. polygons.value = polygons.value.filter(pp => pp.id !== p.id);
  195. editingId.value = null;
  196. mode.value = 'idle'
  197. }
  198. }
  199. });
  200. return
  201. }
  202. pushHistory();
  203. p.points.splice(idx, 1)
  204. }
  205. }
  206. });
  207. return
  208. }
  209. }
  210. }
  211. }
  212. const onPolygonTap = (e: any) => {
  213. if (mode.value === 'select') {
  214. const idx = e.detail.index;
  215. const p = polygons.value[idx];
  216. if (p) {
  217. editingId.value = p.id
  218. }
  219. }
  220. }
  221. const toggleSatellite = () => {
  222. isSatellite.value = !isSatellite.value
  223. }
  224. const savePolygons = () => {
  225. const payload = polygons.value.map(p => ({id: p.id, title: p.title, points: p.points}));
  226. const json = JSON.stringify(payload, null, 2);
  227. console.log('保存多边形:', json);
  228. lastSaved.value = json;
  229. uni.showToast({title: '已打印到控制台(并保存到内存)', icon: 'none'})
  230. }
  231. const loadPolygons = () => {
  232. if (!lastSaved.value) {
  233. return uni.showToast({title: '没有保存记录,请先点击 保存(打印)', icon: 'none'})
  234. }
  235. pushHistory();
  236. const arr = JSON.parse(lastSaved.value);
  237. polygons.value = arr.map((x: any, idx: number) => ({
  238. id: x.id ?? Date.now() + idx,
  239. title: x.title ?? `多边形-${idx + 1}`,
  240. points: x.points,
  241. fillColor: '#FF000033',
  242. strokeColor: '#FF0000',
  243. strokeWidth: 2
  244. }));
  245. editingId.value = null;
  246. mode.value = 'idle'
  247. }
  248. </script>