From 1d552a3bad95caae4af15322df28e6240b797693 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Thu, 13 Aug 2026 15:32:06 +0800
Subject: [PATCH] feat: app视频封面图
---
applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue | 267 ++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 193 insertions(+), 74 deletions(-)
diff --git a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
index 472eddb..ec42046 100644
--- a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
+++ b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -97,7 +97,7 @@
<div class="detail-title">绘制区域列表</div>
<div class="command-table-container">
<div class="command-table-content">
- <el-table class="command-table" :data="shapeList" row-key="id">
+ <el-table class="command-table" :data="shapeList" row-key="id" height="240">
<el-table-column prop="areaType" label="区域类型">
<template v-slot="{ row }">
{{ resolveShapeAreaTypeLabel(row) }}
@@ -116,7 +116,7 @@
<div class="command-table-container">
<div class="command-table-content">
<el-table class="command-table" ref="deviceTableRef" :data="deviceOptions" row-key="id"
- @row-click="handleDeviceRowClick">
+ @row-click="handleDeviceRowClick" height="240">
<el-table-column prop="deviceName" label="设备名称" />
<el-table-column prop="deviceType" label="类型">
<template v-slot="{ row }">
@@ -129,7 +129,7 @@
</div>
<el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
- label-width="96px">
+ label-width="116px">
<el-form-item label="区域名称" prop="areaName">
<el-input class="command-input" v-model="formData.areaName" maxlength="50" placeholder="请输入"
clearable />
@@ -160,7 +160,8 @@
</el-form-item>
<el-form-item label="关联派出所" prop="policeStationId">
<el-select class="command-select" popper-class="command-select-popper"
- v-model="formData.policeStationId" placeholder="请选择" clearable>
+ v-model="formData.policeStationId" placeholder="请选择" clearable filterable remote
+ :remote-method="handlePoliceStationSearch" :loading="policeStationLoading">
<el-option v-for="item in policeStationOptions" :key="item.id" :label="item.stationName"
:value="item.id" />
</el-select>
@@ -168,7 +169,7 @@
<div class="shape-table-container">
<div class="shape-table-title">绘制区域列表</div>
- <el-table class="command-table" :data="shapeList" row-key="id" height="auto">
+ <el-table class="command-table" :data="shapeList" row-key="id" height="240">
<el-table-column prop="areaType" label="区域类型">
<template v-slot="{ row }">
{{ resolveShapeAreaTypeLabel(row) }}
@@ -179,7 +180,7 @@
{{ drawTypeLabelMap[row.drawType] || '-' }}
</template>
</el-table-column>
- <el-table-column label="操作" class-name="operation-btns" width="120">
+ <el-table-column label="操作" class-name="operation-btns" fixed="right" width="120">
<template v-slot="{ row }">
<el-link @click="handleShapeEdit(row)">编辑</el-link>
<el-link @click="handleShapeDelete(row)">删除</el-link>
@@ -200,7 +201,7 @@
<el-form-item prop="deviceIds" label-width="0">
<el-table class="command-table" ref="deviceTableRef"
:data="deviceOptions.filter(item => item.deviceName.includes(searchName))" row-key="id"
- @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick">
+ @selection-change="handleDeviceSelectionChange" @row-click="handleDeviceRowClick" height="240">
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column prop="deviceName" label="设备名称" />
<el-table-column prop="deviceType" label="类型">
@@ -252,13 +253,14 @@
} from '@/utils/cesium/deviceRange'
import * as Cesium from 'cesium'
import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
-import { fwPoliceStationQueryByPolygonsApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
+import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
import { fwDeviceListByPolygonsApi } from '@/views/basicManage/deviceStock/fwDevice'
import polygonIcon from '@/assets/images/areaMap/polygon.png'
import rectIcon from '@/assets/images/areaMap/rect.png'
import ellipseIcon from '@/assets/images/areaMap/ellipse.png'
import bufferIcon from '@/assets/images/areaMap/buffer.png'
import trashIcon from '@/assets/images/areaMap/trash.png'
+import equipmentIcon from '@/assets/images/dataCockpit/map/equipment.png'
const initForm = () => ({
areaName: '', // 区域名称
@@ -285,6 +287,10 @@
const searchName = ref('')
const selectedDeviceRows = ref([])
const policeStationOptions = ref([]) // 关联派出所
+const policeStationAllOptions = ref([])
+const policeStationLoading = ref(false)
+const policeStationSearchTimer = ref(null)
+const policeStationSearchKeyword = ref('')
const deviceOptions = ref([]) // 关联设备
const dictObj = inject('dictObj')
const mapRef = ref(null)
@@ -309,6 +315,7 @@
let viewEntity
let shapeDisplaySource
const deviceRangePrimitiveMap = new Map()
+const deviceIconEntityMap = new Map()
const activeToolMode = ref('')
const suppressDeviceFly = ref(false)
const isDialogInitializing = ref(false)
@@ -316,8 +323,7 @@
let deviceRangeRenderToken = 0
let deviceListTimer = null
let deviceListRequestToken = 0
-let policeStationListTimer = null
-let policeStationRequestToken = 0
+let deviceMapRequestToken = 0
const rules = {
areaName: fieldRules(true, 50),
@@ -424,9 +430,9 @@
clearTimeout(deviceListTimer)
deviceListTimer = null
}
- if (policeStationListTimer) {
- clearTimeout(policeStationListTimer)
- policeStationListTimer = null
+ if (policeStationSearchTimer.value) {
+ clearTimeout(policeStationSearchTimer.value)
+ policeStationSearchTimer.value = null
}
viewEntity && viewer?.entities?.remove(viewEntity)
viewEntity = null
@@ -434,6 +440,7 @@
hideTypePanel()
clearShapeDisplay()
clearDeviceRangePrimitives()
+ clearDeviceIconEntities()
}
}
)
@@ -448,7 +455,6 @@
() => {
if (isDialogInitializing.value) return
queueDeviceListRefresh()
- queuePoliceStationRefresh()
},
{ deep: true }
)
@@ -502,6 +508,14 @@
deviceRangePrimitiveMap.clear()
}
+function clearDeviceIconEntities () {
+ if (!viewer || !deviceIconEntityMap.size) return
+ for (const entity of deviceIconEntityMap.values()) {
+ viewer.entities.remove(entity)
+ }
+ deviceIconEntityMap.clear()
+}
+
function clearDeviceListState () {
deviceOptions.value = []
selectedDeviceRows.value = []
@@ -509,11 +523,6 @@
if (deviceTableRef.value) {
deviceTableRef.value.clearSelection()
}
-}
-
-function clearPoliceStationState () {
- policeStationOptions.value = []
- formData.value.policeStationId = ''
}
function formatWktNumber (value) {
@@ -524,6 +533,60 @@
function isSameWktPoint (left, right) {
if (!left || !right) return false
return Math.abs(left.lat - right.lat) < 0.000001 && Math.abs(left.lng - right.lng) < 0.000001
+}
+
+function normalizePolygonPoints (points) {
+ if (!Array.isArray(points)) return []
+ const ring = points.filter(point => point && Number.isFinite(point.lng) && Number.isFinite(point.lat))
+ if (ring.length < 2) return ring
+ const first = ring[0]
+ const last = ring[ring.length - 1]
+ if (isSameWktPoint(first, last)) {
+ return ring.slice(0, -1)
+ }
+ return ring
+}
+
+function isSelfIntersectingPolygon (points) {
+ const ring = normalizePolygonPoints(points)
+ if (ring.length < 4) return false
+ const epsilon = 1e-12
+ const orientation = (a, b, c) => {
+ const value = (b.lat - a.lat) * (c.lng - b.lng) - (b.lng - a.lng) * (c.lat - b.lat)
+ if (Math.abs(value) < epsilon) return 0
+ return value > 0 ? 1 : 2
+ }
+ const onSegment = (a, b, c) =>
+ b.lng <= Math.max(a.lng, c.lng) + epsilon &&
+ b.lng + epsilon >= Math.min(a.lng, c.lng) &&
+ b.lat <= Math.max(a.lat, c.lat) + epsilon &&
+ b.lat + epsilon >= Math.min(a.lat, c.lat)
+ const segmentsIntersect = (p1, q1, p2, q2) => {
+ const o1 = orientation(p1, q1, p2)
+ const o2 = orientation(p1, q1, q2)
+ const o3 = orientation(p2, q2, p1)
+ const o4 = orientation(p2, q2, q1)
+ if (o1 !== o2 && o3 !== o4) return true
+ if (o1 === 0 && onSegment(p1, p2, q1)) return true
+ if (o2 === 0 && onSegment(p1, q2, q1)) return true
+ if (o3 === 0 && onSegment(p2, p1, q2)) return true
+ if (o4 === 0 && onSegment(p2, q1, q2)) return true
+ return false
+ }
+
+ const count = ring.length
+ for (let i = 0; i < count; i += 1) {
+ const p1 = ring[i]
+ const q1 = ring[(i + 1) % count]
+ for (let j = i + 1; j < count; j += 1) {
+ const p2 = ring[j]
+ const q2 = ring[(j + 1) % count]
+ const isAdjacent = j === i || j === i + 1 || (i === 0 && j === count - 1)
+ if (isAdjacent) continue
+ if (segmentsIntersect(p1, q1, p2, q2)) return true
+ }
+ }
+ return false
}
function buildPolygonWktFromPoints (points) {
@@ -599,18 +662,6 @@
return polygons
}
-async function renderDeviceRangeSingle (device) {
- if (!viewer) return
- clearDeviceRangePrimitives()
- const position = getDeviceLngLat(device)
- if (!position) return
- const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm)
- const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
- if (!primitive) return
- deviceRangePrimitiveMap.set(String(device?.id ?? Date.now()), primitive)
- viewer.scene.primitives.add(primitive)
-}
-
async function renderDeviceRanges (rows) {
if (!viewer) return
deviceRangeRenderToken += 1
@@ -623,6 +674,12 @@
deviceRangePrimitiveMap.delete(id)
}
}
+ for (const [id, entity] of deviceIconEntityMap.entries()) {
+ if (!nextIds.has(id)) {
+ viewer.entities.remove(entity)
+ deviceIconEntityMap.delete(id)
+ }
+ }
const pendingRows = selectedRows.filter(row => {
const id = String(row?.id)
return id && !deviceRangePrimitiveMap.has(id)
@@ -631,14 +688,17 @@
const primitives = await Promise.all(
pendingRows.map(async row => {
const position = getDeviceLngLat(row)
- if (!position) return { row, primitive: null }
+ if (!position) return { row, primitive: null, position: null }
const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm)
const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
- return { row, primitive }
+ return { row, primitive, position }
})
)
if (renderToken !== deviceRangeRenderToken) return
- primitives.forEach(({ row, primitive }) => {
+ primitives.forEach(({ row, primitive, position }) => {
+ if (position) {
+ addDeviceIconEntity(row, position)
+ }
if (!primitive) return
const id = String(row?.id)
if (!nextIds.has(id)) return
@@ -668,6 +728,23 @@
function clearShapeDisplay () {
if (!shapeDisplaySource) return
shapeDisplaySource.entities.removeAll()
+}
+
+function addDeviceIconEntity (device, position) {
+ if (!viewer || !position) return
+ const id = String(device?.id ?? '')
+ if (!id || deviceIconEntityMap.has(id)) return
+ const entity = viewer.entities.add({
+ position: Cesium.Cartesian3.fromDegrees(position.lng, position.lat, position.height || 0),
+ billboard: {
+ image: equipmentIcon,
+ width: 28,
+ height: 38,
+ verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ })
+ deviceIconEntityMap.set(id, entity)
}
function normalizeShapePoint (point) {
@@ -890,6 +967,16 @@
const val = cartesian3Convert(item, viewer)
return { ...val, lng: val.longitude, lat: val.latitude }
})
+ if (currentShapeType.value === 'polygon' && isSelfIntersectingPolygon(pointList)) {
+ ElMessage.warning('多边形存在自相交,请调整后再保存')
+ if (activeToolMode.value === 'draw') {
+ pointList = []
+ activeShapeId.value = null
+ clearActiveTool()
+ startDraw('polygon')
+ }
+ return
+ }
const controlPoints = resolveControlPoints(meta, currentShapeType.value)
if (!activeAreaType.value) {
const detectAreaType = getDetectAreaTypeKey()
@@ -1203,36 +1290,45 @@
}
// 获取派出所列表
-function queuePoliceStationRefresh () {
+async function getPoliceStationList (keyword = '') {
if (!visible.value) return
- if (policeStationListTimer) {
- clearTimeout(policeStationListTimer)
+ policeStationLoading.value = true
+ try {
+ const res = await fwPoliceStationListApi({
+ stationName: keyword || undefined,
+ })
+ const list = res?.data?.data ?? []
+ policeStationOptions.value = list
+ if (!keyword) {
+ policeStationAllOptions.value = list
+ }
+ await nextTick()
+ if (!keyword) {
+ const matched = policeStationOptions.value.find(
+ item => String(item.id) === String(formData.value.policeStationId)
+ )
+ if (!matched) {
+ formData.value.policeStationId = ''
+ }
+ }
+ } finally {
+ policeStationLoading.value = false
}
- policeStationListTimer = setTimeout(() => {
- policeStationListTimer = null
- void getPoliceStationListByPolygons()
- }, 200)
}
-async function getPoliceStationListByPolygons () {
- if (!visible.value) return
- const polygons = resolveShapePolygonsWkt()
- if (!polygons.length) {
- clearPoliceStationState()
+function handlePoliceStationSearch (query) {
+ policeStationSearchKeyword.value = (query || '').trim()
+ if (!policeStationSearchKeyword.value) {
+ policeStationOptions.value = [...policeStationAllOptions.value]
return
}
- policeStationRequestToken += 1
- const requestToken = policeStationRequestToken
- const res = await fwPoliceStationQueryByPolygonsApi({ polygons })
- if (requestToken !== policeStationRequestToken) return
- policeStationOptions.value = res?.data?.data ?? []
- await nextTick()
- const matched = policeStationOptions.value.find(
- item => String(item.id) === String(formData.value.policeStationId)
- )
- if (!matched) {
- formData.value.policeStationId = ''
+ if (policeStationSearchTimer.value) {
+ clearTimeout(policeStationSearchTimer.value)
}
+ policeStationSearchTimer.value = setTimeout(() => {
+ policeStationSearchTimer.value = null
+ void getPoliceStationList(policeStationSearchKeyword.value)
+ }, 300)
}
// 获取设备列表
@@ -1273,9 +1369,29 @@
suppressDeviceFly.value = true
syncDeviceSelection()
suppressDeviceFly.value = prevSuppressDeviceFly
- if (!readonly.value) {
- renderDeviceRanges(selectedDeviceRows.value)
+ if (readonly.value) {
+ await renderDeviceRanges(deviceOptions.value)
}
+}
+
+// 设备上图(新增/编辑用)
+async function getDeviceListForMapDisplay () {
+ if (!visible.value || !viewer) return
+ deviceMapRequestToken += 1
+ const requestToken = deviceMapRequestToken
+ const res = await fwDeviceListByPolygonsApi({
+ areaId: dialogMode.value === 'add' ? '' : formData.value.id,
+ isAreaSelect: 1,
+ isTrack: 1,
+ })
+ if (requestToken !== deviceMapRequestToken) return
+ const list = res?.data?.data ?? []
+ if (!list.length) {
+ clearDeviceRangePrimitives()
+ clearDeviceIconEntities()
+ return
+ }
+ await renderDeviceRanges(list)
}
// 关联设备变更
@@ -1283,7 +1399,6 @@
selectedDeviceRows.value = rows
const ids = rows.map(item => item.id)
formData.value.deviceIds = ids.join(',')
- renderDeviceRanges(rows)
if (suppressDeviceFly.value) {
lastSelectedDeviceIds = [...ids]
return
@@ -1298,12 +1413,13 @@
function handleDeviceRowClick (row) {
if (readonly.value) {
- renderDeviceRangeSingle(row)
void flyToDevice(row)
return
}
- renderDeviceRanges(selectedDeviceRows.value)
- void flyToDevice(row)
+ const isSelected = selectedDeviceRows.value.some(item => String(item?.id) === String(row?.id))
+ if (isSelected) {
+ void flyToDevice(row)
+ }
}
// 同步选择状态
@@ -1318,10 +1434,6 @@
})
selectedDeviceRows.value = rows
formData.value.deviceIds = rows.map(item => item.id).join(',')
- if (readonly.value) {
- renderDeviceRanges(selectedDeviceRows.value)
- return
- }
if (!deviceTableRef.value) return
deviceTableRef.value.clearSelection()
rows.forEach(row => {
@@ -1337,6 +1449,12 @@
selectedDeviceRows.value = []
deviceOptions.value = []
policeStationOptions.value = []
+ policeStationAllOptions.value = []
+ policeStationSearchKeyword.value = ''
+ if (policeStationSearchTimer.value) {
+ clearTimeout(policeStationSearchTimer.value)
+ policeStationSearchTimer.value = null
+ }
shapeList.value = []
activeShapeId.value = null
activeAreaType.value = ''
@@ -1345,6 +1463,7 @@
initMap()
clearShapeDisplay()
clearDeviceRangePrimitives()
+ clearDeviceIconEntities()
viewEntity && viewer?.entities?.remove(viewEntity)
viewEntity = null
clearActiveTool()
@@ -1381,12 +1500,12 @@
lastSelectedDeviceIds = selectedDeviceRows.value.map(row => row.id)
await nextTick()
suppressDeviceFly.value = false
- if (!readonly.value) {
- renderDeviceRanges(selectedDeviceRows.value)
- }
isDialogInitializing.value = false
queueDeviceListRefresh()
- queuePoliceStationRefresh()
+ if (dialogMode.value === 'add' || dialogMode.value === 'edit') {
+ void getDeviceListForMapDisplay()
+ }
+ void getPoliceStationList()
}
onBeforeUnmount(() => {
@@ -1394,9 +1513,9 @@
clearTimeout(deviceListTimer)
deviceListTimer = null
}
- if (policeStationListTimer) {
- clearTimeout(policeStationListTimer)
- policeStationListTimer = null
+ if (policeStationSearchTimer.value) {
+ clearTimeout(policeStationSearchTimer.value)
+ policeStationSearchTimer.value = null
}
hideTypePanel()
})
--
Gitblit v1.9.3