Procházet zdrojové kódy

feat(sb): 添加地图选点功能用于初始化打点

- 新增 InitPointMapSelect 组件实现地图选点界面
- 在 SbInfo 页面添加从地图选点按钮
- 集成地图选点组件到设备位置表单中
- 实现点击地图获取左上角和右下角坐标的逻辑
- 添加底图切换和坐标重置功能
- 实现选点数据传递和坐标转换功能
- 为设备位置选择添加搜索过滤功能
dazhaxie před 3 hodinami
rodič
revize
dfe35f80fe

+ 31 - 1
src/views/sb/info/SbInfo.vue

@@ -262,11 +262,19 @@
             </a-form-item>
           </a-col>
         </a-row>
+        <a-row :gutter="16">
+          <a-col :span="24" style="text-align: center;">
+            <a-button type="primary" icon="environment" @click="openMapSelector">从地图选点</a-button>
+          </a-col>
+        </a-row>
         <a-divider orientation="left">选择设备位置</a-divider>
         <a-form-item label="设备位置" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
           <a-select
             v-model="initPointsData.positionIds"
             mode="multiple"
+            show-search
+            :filter-option="filterPositionOption"
+            option-filter-prop="children"
             placeholder="请选择设备位置"
             style="width: 100%"
           >
@@ -303,6 +311,9 @@
         </a-form-item>
       </a-form>
     </a-modal>
+
+    <!-- 地图选点组件 -->
+    <init-point-map-select ref="initPointMapSelect" @ok="handleMapPointSelected" />
   </a-card>
 
 </template>
@@ -322,6 +333,7 @@ import ImportFormAdd from './modules/ImportFormAdd'
 import ImportFormUpdate from './modules/ImportFormUpdate'
 import PrintSbCode from '@/views/sb/info/modules/PrintSbCode'
 import PrintInSbInfoBatch from '@/views/sb/info/modules/PrintInSbInfoBatch'
+import InitPointMapSelect from '@/views/sb/info/modules/InitPointMapSelect'
 import { querySbPosition, initPoints } from '@/api/sb/position'
 import { queryFile } from '@/api/upms/file'
 
@@ -338,7 +350,8 @@ export default {
     ImportFormAdd,
     ImportFormUpdate,
     PrintInSbInfoBatch,
-    BaseFormStatusLog
+    BaseFormStatusLog,
+    InitPointMapSelect
   },
   props: {
     filter: {
@@ -858,6 +871,9 @@ export default {
         this.BaseTool.UPLOAD.downLoadExportExcel(file)
       })
     },
+    filterPositionOption (input, option) {
+      return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
+    },
     handleInitPoints () {
       this.initPointsData = {
         points: [
@@ -897,6 +913,20 @@ export default {
       }).finally(() => {
         this.initPointsLoading = false
       })
+    },
+    openMapSelector () {
+      // 打开地图选点组件,传入当前选择的底图
+      this.$refs.initPointMapSelect.base(this.initPointsData.mapName)
+    },
+    handleMapPointSelected (data) {
+      // 地图选点回调,data 包含 x1, y1, x2, y2 (小数形式 0-1)
+      // 转换为实际坐标值(这里假设地图坐标范围是 0-1,直接存储)
+      // 如果需要转换为实际经纬度,需要根据底图的坐标范围进行转换
+      this.initPointsData.points[0].longitude = data.x1
+      this.initPointsData.points[0].latitude = data.y1
+      this.initPointsData.points[1].longitude = data.x2
+      this.initPointsData.points[1].latitude = data.y2
+      this.$message.success('地图选点成功')
     }
   }
 }

+ 215 - 0
src/views/sb/info/modules/InitPointMapSelect.vue

@@ -0,0 +1,215 @@
+<template>
+  <a-modal title="初始化打点 - 地图选点" :width="1300" :visible="visible" :footer="null" @cancel="handleCancel">
+    <div class="point-toolbar">
+      <span>底图:</span>
+      <a-select v-model="mapName" style="width: 140px" @change="handleMapChange">
+        <a-select-option v-for="item in mapList" :key="item.targetId" :value="item.targetId">
+          {{ item.targetId }}
+        </a-select-option>
+      </a-select>
+      <span class="ml">左上角 - 经度(%):</span>
+      <a-input-number v-model="x1Pct" :min="0" :max="100" :step="0.0001" style="width: 110px" placeholder="经度" />
+      <span class="ml">纬度(%):</span>
+      <a-input-number v-model="y1Pct" :min="0" :max="100" :step="0.0001" style="width: 110px" placeholder="纬度" />
+      <span class="ml">右下角 - 经度(%):</span>
+      <a-input-number v-model="x2Pct" :min="0" :max="100" :step="0.0001" style="width: 110px" placeholder="经度" />
+      <span class="ml">纬度(%):</span>
+      <a-input-number v-model="y2Pct" :min="0" :max="100" :step="0.0001" style="width: 110px" placeholder="纬度" />
+      <a-button class="ml" type="primary" @click="resetPoints">重置</a-button>
+      <a-button class="ml" type="primary" :loading="saving" @click="save">确认选点</a-button>
+      <span class="tip ml">第1次点击=左上角,第2次点击=右下角</span>
+    </div>
+    <div class="point-map" ref="mapBox" @click="handleMapClick">
+      <img v-if="mapUrl" class="map-bg" :src="mapUrl" alt="">
+      <div v-if="hasPoint1" class="point-marker marker-1" :style="{ left: x1Pct + '%', top: y1Pct + '%' }">
+        <span class="marker-label">左上</span>
+      </div>
+      <div v-if="hasPoint2" class="point-marker marker-2" :style="{ left: x2Pct + '%', top: y2Pct + '%' }">
+        <span class="marker-label">右下</span>
+      </div>
+    </div>
+  </a-modal>
+</template>
+
+<script>
+import { axios } from '@/utils/request'
+
+export default {
+  name: 'InitPointMapSelect',
+  data() {
+    return {
+      visible: false,
+      saving: false,
+      mapList: [],
+      mapName: null,
+      clickCount: 0,
+      x1Pct: null,
+      y1Pct: null,
+      x2Pct: null,
+      y2Pct: null
+    }
+  },
+  computed: {
+    mapUrl() {
+      const current = (this.mapList || []).find(item => item.targetId === this.mapName)
+      return current ? current.url : ''
+    },
+    hasPoint1() {
+      return this.x1Pct != null && this.y1Pct != null
+    },
+    hasPoint2() {
+      return this.x2Pct != null && this.y2Pct != null
+    }
+  },
+  methods: {
+    base(mapName) {
+      this.visible = true
+      this.mapName = mapName || null
+      this.clickCount = 0
+      this.x1Pct = null
+      this.y1Pct = null
+      this.x2Pct = null
+      this.y2Pct = null
+      this.getMapList()
+    },
+    getMapList() {
+      axios.get('/upms/files', { params: { type: 56 } }).then(res => {
+        this.mapList = res.data || []
+        if (!this.mapName && this.mapList.length > 0) {
+          this.mapName = this.mapList[0].targetId
+        }
+      })
+    },
+    handleMapChange() {
+      // 切换底图时重置选点
+      this.resetPoints()
+    },
+    handleMapClick(e) {
+      const rect = this.$refs.mapBox.getBoundingClientRect()
+      if (!rect.width || !rect.height) {
+        return
+      }
+      const x = ((e.clientX - rect.left) / rect.width) * 100
+      const y = ((e.clientY - rect.top) / rect.height) * 100
+      const xPct = Math.round(x * 10000) / 10000
+      const yPct = Math.round(y * 10000) / 10000
+
+      this.clickCount++
+      if (this.clickCount === 1) {
+        // 第1次点击 = 左上角
+        this.x1Pct = xPct
+        this.y1Pct = yPct
+        this.x2Pct = null
+        this.y2Pct = null
+      } else if (this.clickCount === 2) {
+        // 第2次点击 = 右下角
+        this.x2Pct = xPct
+        this.y2Pct = yPct
+      } else {
+        // 第3次点击重新开始
+        this.clickCount = 1
+        this.x1Pct = xPct
+        this.y1Pct = yPct
+        this.x2Pct = null
+        this.y2Pct = null
+      }
+    },
+    resetPoints() {
+      this.clickCount = 0
+      this.x1Pct = null
+      this.y1Pct = null
+      this.x2Pct = null
+      this.y2Pct = null
+    },
+    save() {
+      if (!this.hasPoint1 || !this.hasPoint2) {
+        this.$message.error('请点击地图选取左上角和右下角两个点')
+        return
+      }
+      this.saving = true
+      // 百分比转小数(0-1),保留6位小数
+      const data = {
+        x1: Math.round((this.x1Pct / 100) * 1000000) / 1000000,
+        y1: Math.round((this.y1Pct / 100) * 1000000) / 1000000,
+        x2: Math.round((this.x2Pct / 100) * 1000000) / 1000000,
+        y2: Math.round((this.y2Pct / 100) * 1000000) / 1000000
+      }
+      this.saving = false
+      this.$emit('ok', data)
+      this.visible = false
+    },
+    handleCancel() {
+      this.visible = false
+    }
+  }
+}
+</script>
+
+<style lang="less" scoped>
+.point-toolbar {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+  flex-wrap: wrap;
+  gap: 8px;
+
+  .ml {
+    margin-left: 8px;
+  }
+
+  .tip {
+    color: #999;
+    font-size: 12px;
+  }
+}
+
+.point-map {
+  position: relative;
+  height: 620px;
+  border: 1px solid #e8e8e8;
+  overflow: hidden;
+  cursor: crosshair;
+
+  .map-bg {
+    display: block;
+    width: 100%;
+    height: 100%;
+  }
+
+  .point-marker {
+    position: absolute;
+    width: 16px;
+    height: 16px;
+    margin: -8px 0 0 -8px;
+    border-radius: 50%;
+    z-index: 10;
+    pointer-events: none;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    .marker-label {
+      position: absolute;
+      top: -20px;
+      left: 50%;
+      transform: translateX(-50%);
+      font-size: 12px;
+      white-space: nowrap;
+      background: rgba(0, 0, 0, 0.7);
+      color: white;
+      padding: 2px 6px;
+      border-radius: 3px;
+    }
+
+    &.marker-1 {
+      background: #ff4d4f;
+      box-shadow: 0 0 8px #ff4d4f;
+    }
+
+    &.marker-2 {
+      background: #52c41a;
+      box-shadow: 0 0 8px #52c41a;
+    }
+  }
+}
+</style>