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