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 | 175 +++++++++++++++++++++++++++++++++++++++++++++++-----------
1 files changed, 141 insertions(+), 34 deletions(-)
diff --git a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
index 3c51a03..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 />
@@ -169,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) }}
@@ -180,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>
@@ -201,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="类型">
@@ -260,6 +260,7 @@
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: '', // 区域名称
@@ -314,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)
@@ -321,6 +323,7 @@
let deviceRangeRenderToken = 0
let deviceListTimer = null
let deviceListRequestToken = 0
+let deviceMapRequestToken = 0
const rules = {
areaName: fieldRules(true, 50),
@@ -437,6 +440,7 @@
hideTypePanel()
clearShapeDisplay()
clearDeviceRangePrimitives()
+ clearDeviceIconEntities()
}
}
)
@@ -504,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 = []
@@ -521,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) {
@@ -596,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
@@ -620,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)
@@ -628,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
@@ -665,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) {
@@ -887,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()
@@ -1279,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)
}
// 关联设备变更
@@ -1289,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
@@ -1304,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)
+ }
}
// 同步选择状态
@@ -1324,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 => {
@@ -1357,6 +1463,7 @@
initMap()
clearShapeDisplay()
clearDeviceRangePrimitives()
+ clearDeviceIconEntities()
viewEntity && viewer?.entities?.remove(viewEntity)
viewEntity = null
clearActiveTool()
@@ -1393,11 +1500,11 @@
lastSelectedDeviceIds = selectedDeviceRows.value.map(row => row.id)
await nextTick()
suppressDeviceFly.value = false
- if (!readonly.value) {
- renderDeviceRanges(selectedDeviceRows.value)
- }
isDialogInitializing.value = false
queueDeviceListRefresh()
+ if (dialogMode.value === 'add' || dialogMode.value === 'edit') {
+ void getDeviceListForMapDisplay()
+ }
void getPoliceStationList()
}
--
Gitblit v1.9.3