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