|
|
@@ -0,0 +1,188 @@
|
|
|
+<template>
|
|
|
+ <!-- 仅微信小程序端 -->
|
|
|
+ <!-- #ifdef MP-WEIXIN -->
|
|
|
+ <view class="wx-map-draw-area">
|
|
|
+ <up-navbar title="地图绘制" @leftClick="navigateBackOrHome()" :fixed="false"></up-navbar>
|
|
|
+ <view class="flex1 ov-hd">
|
|
|
+ <map class="wx-map" :latitude="center[0]" :longitude="center[1]" enable-rotate :scale="zoomToScale(zoom)"
|
|
|
+ :polygons="wxPolygons" :polyline="wxPolylines" :show-location="false" @tap="onWxMapTap"
|
|
|
+ enable-satellite />
|
|
|
+
|
|
|
+ </view>
|
|
|
+ <view class="wx-toolbar">
|
|
|
+ <u-button size="small" type="primary" @click="startOrFinish">
|
|
|
+ {{ isDrawing ? '完成' : '开始绘制' }}
|
|
|
+ </u-button>
|
|
|
+ <u-button size="small" class="mt-8" @click="undoPoint" :disabled="wxPoints.length === 0">撤销</u-button>
|
|
|
+ <u-button size="small" class="mt-8" type="warning" @click="clearAll"
|
|
|
+ :disabled="wxPoints.length === 0">清除</u-button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="wx-info" v-if="showInfo">
|
|
|
+ <text>点数:{{ wxPoints.length }}</text>
|
|
|
+ <text class="ml-12">面积:{{ formattedWxArea }}</text>
|
|
|
+ <text class="ml-12" v-if="isClosed">(已闭合)</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <!-- #endif -->
|
|
|
+
|
|
|
+ <!-- 非微信端兜底提示(可选) -->
|
|
|
+ <!-- #ifndef MP-WEIXIN -->
|
|
|
+ <view class="map-draw-area__unsupported">
|
|
|
+ <text>该组件仅支持微信小程序端绘制。</text>
|
|
|
+ </view>
|
|
|
+ <!-- #endif -->
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed } from 'vue'
|
|
|
+
|
|
|
+type LatLng = [number, number]
|
|
|
+type WxPoint = { latitude: number; longitude: number }
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ center: { type: Array as () => LatLng, default: () => [23.1291, 113.2644] },
|
|
|
+ zoom: { type: Number, default: 13 },
|
|
|
+ showInfo: { type: Boolean, default: true },
|
|
|
+})
|
|
|
+
|
|
|
+const emits = defineEmits<{
|
|
|
+ (e: 'change', payload: { area: number; latlngs: LatLng[]; closed: boolean }): void
|
|
|
+}>()
|
|
|
+
|
|
|
+// 绘制状态
|
|
|
+const wxPoints = ref<WxPoint[]>([])
|
|
|
+const isDrawing = ref(false)
|
|
|
+const isClosed = ref(false)
|
|
|
+const wxArea = ref(0)
|
|
|
+
|
|
|
+// 可视化:多边形(填充)、折线(路径)
|
|
|
+const wxPolygons = computed<any[]>(() => {
|
|
|
+ if (wxPoints.value.length < 3) return []
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ points: wxPoints.value,
|
|
|
+ strokeColor: '#18A058',
|
|
|
+ strokeWidth: 2,
|
|
|
+ fillColor: '#18A05833',
|
|
|
+ zIndex: 2,
|
|
|
+ },
|
|
|
+ ]
|
|
|
+})
|
|
|
+
|
|
|
+const wxPolylines = computed<any[]>(() => {
|
|
|
+ if (wxPoints.value.length < 2) return []
|
|
|
+ const lines: any[] = [
|
|
|
+ { points: wxPoints.value, color: '#18A058', width: 2, dottedLine: false, zIndex: 3 },
|
|
|
+ ]
|
|
|
+ if (wxPoints.value.length >= 2) {
|
|
|
+ const first = wxPoints.value[0]
|
|
|
+ const last = wxPoints.value[wxPoints.value.length - 1]
|
|
|
+ lines.push({ points: [last, first], color: '#18A058', width: 2, dottedLine: true, zIndex: 3 })
|
|
|
+ }
|
|
|
+ return lines
|
|
|
+})
|
|
|
+
|
|
|
+const formattedWxArea = computed(() => formatArea(wxArea.value))
|
|
|
+
|
|
|
+function onWxMapTap(e: any) {
|
|
|
+ if (!isDrawing.value) return
|
|
|
+ const { latitude, longitude } = e?.detail || {}
|
|
|
+ if (typeof latitude === 'number' && typeof longitude === 'number') {
|
|
|
+ wxPoints.value = [...wxPoints.value, { latitude, longitude }]
|
|
|
+ computeWxArea()
|
|
|
+ emitChange()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function startOrFinish() {
|
|
|
+ if (!isDrawing.value) {
|
|
|
+ isDrawing.value = true
|
|
|
+ isClosed.value = false
|
|
|
+ wxPoints.value = []
|
|
|
+ wxArea.value = 0
|
|
|
+ } else {
|
|
|
+ isDrawing.value = false
|
|
|
+ isClosed.value = wxPoints.value.length >= 3
|
|
|
+ computeWxArea()
|
|
|
+ emitChange()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function undoPoint() {
|
|
|
+ if (wxPoints.value.length === 0) return
|
|
|
+ wxPoints.value = wxPoints.value.slice(0, -1)
|
|
|
+ computeWxArea()
|
|
|
+ emitChange()
|
|
|
+}
|
|
|
+
|
|
|
+function clearAll() {
|
|
|
+ wxPoints.value = []
|
|
|
+ wxArea.value = 0
|
|
|
+ isDrawing.value = false
|
|
|
+ isClosed.value = false
|
|
|
+ emitChange()
|
|
|
+}
|
|
|
+
|
|
|
+function emitChange() {
|
|
|
+ emits('change', {
|
|
|
+ area: wxArea.value,
|
|
|
+ latlngs: wxPoints.value.map((p) => [p.latitude, p.longitude]) as LatLng[],
|
|
|
+ closed: isClosed.value,
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+function computeWxArea() {
|
|
|
+ if (wxPoints.value.length < 3) {
|
|
|
+ wxArea.value = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ wxArea.value = calcSphericalPolygonAreaLngLat(
|
|
|
+ wxPoints.value.map((p) => [p.longitude, p.latitude])
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+// 球面多边形面积(近似,WGS84)输入 [lng, lat]
|
|
|
+function calcSphericalPolygonAreaLngLat(coords: [number, number][]): number {
|
|
|
+ const n = coords.length
|
|
|
+ if (n < 3) return 0
|
|
|
+ const rad = (d: number) => (d * Math.PI) / 180
|
|
|
+ const R = 6378137
|
|
|
+ let area = 0
|
|
|
+ for (let i = 0; i < n; i++) {
|
|
|
+ const lower = coords[(i + n - 1) % n]
|
|
|
+ const middle = coords[i]
|
|
|
+ const upper = coords[(i + 1) % n]
|
|
|
+ area += (rad(upper[0]) - rad(lower[0])) * Math.sin(rad(middle[1]))
|
|
|
+ }
|
|
|
+ area = (area * R * R) / 2
|
|
|
+ return Math.abs(area)
|
|
|
+}
|
|
|
+
|
|
|
+function zoomToScale(zoom: number) {
|
|
|
+ const z = Math.max(3, Math.min(20, Math.round(zoom)))
|
|
|
+ return z
|
|
|
+}
|
|
|
+
|
|
|
+function formatArea(m2: number): string {
|
|
|
+ if (!m2) return '0 m²'
|
|
|
+ if (m2 < 100000) return `${m2.toFixed(2)} m²`
|
|
|
+ const km2 = m2 / 1_000_000
|
|
|
+ return `${km2.toFixed(4)} km²`
|
|
|
+}
|
|
|
+
|
|
|
+defineExpose({ startOrFinish, undoPoint, clearAll })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.wx-map-draw-area {
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.wx-map {
|
|
|
+ width: 750rpx;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+</style>
|