From 89380e6260a75d1d3b94de687ebcc2f50d50659d Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Tue, 03 Feb 2026 15:44:33 +0800
Subject: [PATCH] feat:环境变量配置调整

---
 applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue | 1076 ++++++++++++++++++++++++++++++++++++++---------------------
 1 files changed, 698 insertions(+), 378 deletions(-)

diff --git a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
index f1a0cd1..342b43c 100644
--- a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
+++ b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -1,72 +1,66 @@
 <template>
-	<el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false"
-		:close-on-click-modal="false">
+	<el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false" :close-on-click-modal="false">
 		<div class="dialog-container">
 			<div class="left-container">
-				<CommonCesiumMap
-					ref="mapRef"
-					class="leftMap command-cesium"
-					:active="visible"
-					:flat-mode="false"
-					:terrain="true"
-					:layer-mode="4"
-					:boundary="false"
-				/>
-				<div
-					v-if="showTypePanel && !readonly"
-					class="shape-type-panel"
-				>
+				<CommonCesiumMap ref="mapRef" class="leftMap command-cesium" :active="visible" :flat-mode="false"
+					:terrain="true" :layer-mode="4" :boundary="false" :zoom-to-boundary="dialogMode === 'add'" />
+				<div v-if="showTypePanel && !readonly" class="shape-type-panel">
 					<div class="panel-header">
 						<span>{{ drawTypeLabelMap[currentShapeType] || '区域' }}</span>
-						<el-icon class="panel-close" @click.stop="handleTypePanelClose"><Close /></el-icon>
+						<el-icon class="panel-close" @click.stop="handleTypePanelClose">
+							<Close />
+						</el-icon>
 					</div>
 					<div class="panel-body">
 						<span class="panel-label">区域类型</span>
-						<el-select
-							class="command-select"
-							popper-class="command-select-popper"
-							v-model="formData.areaType"
-							placeholder="请选择"
-							@change="handleAreaTypeChange"
-						>
-							<el-option
-								v-for="item in dictObj.areaType"
-								:key="item.dictKey"
-								:label="item.dictValue"
-								:value="item.dictKey"
-							/>
+						<el-select class="command-select" popper-class="command-select-popper" v-model="activeAreaType"
+							placeholder="请选择" :disabled="currentShapeType === 'buffer'" collapse-tags
+							:multiple="currentShapeType === 'buffer'" @change="handleAreaTypeChange">
+							<el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
+								:value="item.dictKey" />
 						</el-select>
 					</div>
 				</div>
 				<div class="map-toolbar">
-					<button class="tool-btn" type="button" @click="handleDraw('polygon')">
-						<span class="tool-icon polygon"></span>
-						<span class="tool-text">多边形</span>
-					</button>
-					<button class="tool-btn" type="button" @click="handleDraw('rectangle')">
-						<span class="tool-icon rect"></span>
-						<span class="tool-text">矩形</span>
-					</button>
-					<button class="tool-btn" type="button" @click="handleDraw('ellipse')">
-						<span class="tool-icon ellipse"></span>
-						<span class="tool-text">椭圆</span>
-					</button>
-					<button class="tool-btn" type="button" @click="handleDraw('buffer')">
-						<span class="tool-icon buffer"></span>
-						<span class="tool-text">缓冲圆</span>
-					</button>
-					<button class="tool-btn danger" type="button" @click="handleClearShape">
-						<span class="tool-icon trash"></span>
-						<span class="tool-text">删除</span>
-					</button>
+					<el-tooltip content="多边形" placement="left">
+						<button class="tool-btn" :class="{ active: currentShapeType === 'polygon' }" type="button"
+							@click="handleDraw('polygon')">
+							<img class="tool-icon-image" :src="polygonIcon" alt="多边形" />
+						</button>
+					</el-tooltip>
+					<el-tooltip content="矩形" placement="left">
+						<button class="tool-btn" :class="{ active: currentShapeType === 'rectangle' }" type="button"
+							@click="handleDraw('rectangle')">
+							<img class="tool-icon-image" :src="rectIcon" alt="矩形" />
+						</button>
+					</el-tooltip>
+					<el-tooltip content="椭圆" placement="left">
+						<button class="tool-btn" :class="{ active: currentShapeType === 'ellipse' }" type="button"
+							@click="handleDraw('ellipse')">
+							<img class="tool-icon-image" :src="ellipseIcon" alt="椭圆" />
+						</button>
+					</el-tooltip>
+					<el-tooltip content="缓冲圆" placement="left">
+						<button class="tool-btn" :class="{ active: currentShapeType === 'buffer' }" type="button"
+							@click="handleDraw('buffer')">
+							<img class="tool-icon-image" :src="bufferIcon" alt="缓冲圆" />
+						</button>
+					</el-tooltip>
+					<el-tooltip content="删除" placement="left">
+						<button class="tool-btn danger" type="button" @click="handleClearShape">
+							<img class="tool-icon-image" :src="trashIcon" alt="删除" />
+						</button>
+					</el-tooltip>
 				</div>
 			</div>
 
 			<div class="right-container">
 				<div class="header">
 					<span>{{ titleEnum[dialogMode] }}</span>
-				
-					<el-icon class="close-btn" @click.stop="visible = false"><Close /></el-icon>
+
+					<el-icon class="close-btn" @click.stop="visible = false">
+						<Close />
+					</el-icon>
 				</div>
 
 				<div class="content" v-if="readonly">
@@ -75,18 +69,6 @@
 						<el-col :span="24">
 							<div class="label">区域名称</div>
 							<div class="val">{{ formData.areaName }}</div>
-						</el-col>
-						<el-col :span="24">
-							<div class="label">区域位置</div>
-							<div class="val">{{ formatLocation(formData) }}</div>
-						</el-col>
-						<el-col :span="24">
-							<div class="label">区域面积</div>
-							<div class="val">{{ formData.areaSize || '-' }}k㎡</div>
-						</el-col>
-						<el-col :span="24">
-							<div class="label">区域类型</div>
-							<div class="val">{{ getDictLabel(formData.areaType, dictObj.areaType) }}</div>
 						</el-col>
 						<el-col :span="24">
 							<div class="label">触发条件</div>
@@ -112,11 +94,29 @@
 						</el-col>
 					</el-row>
 
+					<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-column prop="areaType" label="区域类型">
+									<template v-slot="{ row }">
+										{{ resolveShapeAreaTypeLabel(row) }}
+									</template>
+								</el-table-column>
+								<el-table-column prop="drawType" label="绘制类型">
+									<template v-slot="{ row }">
+										{{ drawTypeLabelMap[row.drawType] || '-' }}
+									</template>
+								</el-table-column>
+							</el-table>
+						</div>
+					</div>
+
 					<div class="detail-title">关联设备</div>
 					<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">
+							<el-table class="command-table" ref="deviceTableRef" :data="deviceOptions" row-key="id"
+								@row-click="handleDeviceRowClick">
 								<el-table-column prop="deviceName" label="设备名称" />
 								<el-table-column prop="deviceType" label="类型">
 									<template v-slot="{ row }">
@@ -131,25 +131,12 @@
 				<el-form class="dialog-form" v-else ref="formRef" :model="formData" :rules="rules" :disabled="readonly"
 					label-width="96px">
 					<el-form-item label="区域名称" prop="areaName">
-						<el-input class="command-input" v-model="formData.areaName" maxlength="50"
-							placeholder="请输入" clearable />
-					</el-form-item>
-					<el-form-item label="区域位置" prop="longitude">
-						<div class="val">{{ formatLocation(formData) }}</div>
-					</el-form-item>
-					<el-form-item label="区域面积" prop="areaSize">
-						<div class="val">{{ formData.areaSize || '-' }}k㎡</div>
-					</el-form-item>
-					<el-form-item label="区域类型" prop="areaType">
-						<el-select class="command-select" popper-class="command-select-popper"
-							v-model="formData.areaType" placeholder="请选择" clearable>
-							<el-option v-for="item in dictObj.areaType" :key="item.dictKey" :label="item.dictValue"
-								:value="item.dictKey" />
-						</el-select>
+						<el-input class="command-input" v-model="formData.areaName" maxlength="50" placeholder="请输入"
+							clearable />
 					</el-form-item>
 					<el-form-item label="响应机制" prop="responseMechanism">
-						<el-input class="command-input" v-model="formData.responseMechanism"
-							maxlength="200" placeholder="请输入" clearable />
+						<el-input class="command-input" v-model="formData.responseMechanism" maxlength="200"
+							placeholder="请输入" clearable />
 					</el-form-item>
 					<el-form-item label="触发条件" prop="triggerCondition">
 						<el-select class="command-select" popper-class="command-select-popper"
@@ -166,15 +153,15 @@
 						</el-select>
 					</el-form-item>
 					<el-form-item label="可飞行时段" prop="flyDateStart">
-						<el-date-picker class="command-date-picker"
-							popper-class="command-date-picker-popper" v-model="flyDateRange"
-							type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"
-							:disabled-date="disabledFlyDate" :disabled-time="disabledFlyTime"
+						<el-date-picker class="command-date-picker" popper-class="command-date-picker-popper"
+							v-model="flyDateRange" type="datetimerange" range-separator="至" start-placeholder="开始时间"
+							end-placeholder="结束时间" :disabled-date="disabledFlyDate" :disabled-time="disabledFlyTime"
 							value-format="YYYY-MM-DD HH:mm:ss" clearable />
 					</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>
@@ -182,10 +169,10 @@
 
 					<div class="shape-table-container">
 						<div class="shape-table-title">绘制区域列表</div>
-						<el-table class="command-table" :data="shapeList" row-key="id" height="360">
-							<el-table-column prop="areaType" label="类型">
+						<el-table class="command-table" :data="shapeList" row-key="id" height="auto">
+							<el-table-column prop="areaType" label="区域类型">
 								<template v-slot="{ row }">
-									{{ getDictLabel(row.areaType, dictObj.areaType) }}
+									{{ resolveShapeAreaTypeLabel(row) }}
 								</template>
 							</el-table-column>
 							<el-table-column prop="drawType" label="绘制类型">
@@ -208,15 +195,13 @@
 								关联设备
 							</div>
 
-							<el-input class="command-input" v-model="searchName" placeholder="请输入"
-								clearable></el-input>
+							<el-input class="command-input" v-model="searchName" placeholder="请输入" clearable></el-input>
 						</div>
 
 						<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">
 								<el-table-column type="selection" width="55" :reserve-selection="true" />
 								<el-table-column prop="deviceName" label="设备名称" />
 								<el-table-column prop="deviceType" label="类型">
@@ -230,7 +215,9 @@
 				</el-form>
 
 				<div class="footer">
-					<el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' : '取消' }}</el-button>
+					<el-button v-if="dialogMode != 'view'" color="#2B2B4C" @click="handleCancel">{{ readonly ? '关闭' :
+						'取消'
+					}}</el-button>
 					<el-button color="#284FE3" v-if="!readonly" type="primary" :loading="submitting"
 						:disabled="submitting" @click="handleSubmit">
 						确定
@@ -244,32 +231,43 @@
 <script setup>
 import { Close } from '@element-plus/icons-vue'
 
-import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import { computed, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'
 import { ElMessage } from 'element-plus'
 import { fwAreaDivideDetailApi, fwAreaDivideSubmitApi } from './partitionApi'
 import { saveOperationLog } from '@ztzf/apis'
 import { useRoute } from 'vue-router'
 import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils'
 import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue'
-import { DrawManager } from './shapeTools/DrawManager'
-import { EditManager } from './shapeTools/EditManager'
+import {
+	DrawManager,
+	EditManager,
+	addBufferRadiusLabelEntity,
+	buildEllipsePositions,
+} from '@/utils/cesium/shapeTools'
 import { cartesian3Convert } from '@/utils/cesium/mapUtil'
-import { buildEllipsePositions } from './shapeTools/shapeUtils'
-import * as turf from '@turf/turf'
+import {
+	createDeviceRangePrimitive,
+	getDeviceLngLat,
+	normalizeDeviceRange,
+	resolveDeviceTerrainHeight,
+} from '@/utils/cesium/deviceRange'
 import * as Cesium from 'cesium'
+import { AREA_TYPE_STYLE_MAP, BUFFER_LEVEL_STYLES, DEFAULT_AREA_STYLE } from '@ztzf/constants'
 import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
-import { fwDeviceListApi } from '@/views/basicManage/deviceStock/fwDevice'
+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: '', // 区域名称
-	areaSize: null, // 区域面积
-	areaType: '', // 区域类型
 	controlLevel: '', // 管控级别
 	deviceIds: '', // 关联设备ID
 	flyDateEnd: '', // 可飞行结束
 	flyDateStart: '', // 可飞行开始
-	latitude: null, // 区域中心纬度
-	longitude: null, // 区域中心经度
 	policeStationId: '', // 关联派出所id
 	responseMechanism: '', // 响应机制
 	triggerCondition: '', // 触发条件
@@ -279,7 +277,7 @@
 const formRef = ref(null) // 表单实例
 const formData = ref(initForm()) // 表单数据
 const visible = defineModel() // 弹框显隐
-const dialogMode = ref('add') // 弹框模式
+const dialogMode = ref() // 弹框模式
 const submitting = ref(false) // 提交中
 const readonly = computed(() => dialogMode.value === 'view')
 const route = useRoute()
@@ -289,10 +287,15 @@
 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)
 const currentShapeType = ref('polygon')
+const activeAreaType = ref('')
 const shapeList = ref([])
 const activeShapeId = ref(null)
 const drawTypeLabelMap = {
@@ -301,22 +304,9 @@
 	ellipse: '椭圆',
 	buffer: '缓冲圆',
 }
+const bufferAreaTypeValue = '1,2,3'
 const showTypePanel = ref(false)
 const suppressTypePanelOnce = ref(false)
-const areaTypeStyleMap = {
-	'1': {
-		fill: Cesium.Color.fromBytes(25, 178, 230, 128),
-		outline: Cesium.Color.fromBytes(0, 251, 255, 255),
-	},
-	'2': {
-		fill: Cesium.Color.fromBytes(255, 235, 59, 128),
-		outline: Cesium.Color.fromBytes(255, 235, 59, 255),
-	},
-	'3': {
-		fill: Cesium.Color.fromBytes(247, 20, 20, 128),
-		outline: Cesium.Color.fromBytes(255, 0, 0, 255),
-	},
-}
 let viewer
 let drawManager
 let editManager
@@ -325,13 +315,17 @@
 let viewEntity
 let shapeDisplaySource
 const deviceRangePrimitiveMap = new Map()
-let deviceRangeMaterialRegistered = false
+const deviceIconEntityMap = new Map()
 const activeToolMode = ref('')
+const suppressDeviceFly = ref(false)
+const isDialogInitializing = ref(false)
+let lastSelectedDeviceIds = []
+let deviceRangeRenderToken = 0
+let deviceListTimer = null
+let deviceListRequestToken = 0
 
 const rules = {
 	areaName: fieldRules(true, 50),
-	longitude: fieldRules(true),
-	areaType: fieldRules(true),
 	responseMechanism: fieldRules(true, 200),
 	triggerCondition: fieldRules(true, 200),
 	controlLevel: fieldRules(true),
@@ -384,19 +378,28 @@
 		return
 	}
 
-	console.log(shapeList.value, 1111111)
-
 	const isValid = await formRef.value?.validate().catch(() => false)
 	if (!isValid) return
-	if (dialogMode.value === 'add' && Number(formData.value.areaSize) < 0.5) {
-		ElMessage.warning('区域面积不能小于0.5km²,请重新绘制')
-		return
-	}
 	submitting.value = true
 	try {
-		let str = [...pointList, pointList[0]].map(item => `${item.longitude} ${item.latitude}`).join(',')
-		formData.value.geom = `POLYGON((${str}))`
-		await fwAreaDivideSubmitApi(formData.value)
+		const areaDivide = { ...formData.value }
+		delete areaDivide.geom
+		const areaDivideExtList = shapeList.value.map(shape => {
+			const areaTypeKey = shape?.areaType ?? ''
+			const areaTypeValue = getDictLabel(areaTypeKey, resolveAreaTypeOptions())
+			return {
+				id: shape?.extId,
+				areaDivideId: areaDivide?.id ?? '',
+				areaCode: areaDivide?.areaCode ?? '',
+				areaTypeKey,
+				areaTypeValue,
+				geomJson: JSON.stringify(shape),
+			}
+		})
+		await fwAreaDivideSubmitApi({
+			areaDivide,
+			areaDivideExtList,
+		})
 		const actionText = dialogMode.value === 'add' ? '新增' : '编辑'
 		saveOperationLog({
 			requestUri: route.path,
@@ -422,34 +425,56 @@
 	() => visible.value,
 	val => {
 		if (!val) {
+			if (deviceListTimer) {
+				clearTimeout(deviceListTimer)
+				deviceListTimer = null
+			}
+			if (policeStationSearchTimer.value) {
+				clearTimeout(policeStationSearchTimer.value)
+				policeStationSearchTimer.value = null
+			}
 			viewEntity && viewer?.entities?.remove(viewEntity)
 			viewEntity = null
 			clearActiveTool()
 			hideTypePanel()
 			clearShapeDisplay()
 			clearDeviceRangePrimitives()
+			clearDeviceIconEntities()
 		}
 	}
 )
 watch(
-	() => formData.value.areaType,
+	() => activeAreaType.value,
 	val => {
 		updateActiveShapeAreaType(val)
 	}
+)
+watch(
+	() => shapeList.value,
+	() => {
+		if (isDialogInitializing.value) return
+		queueDeviceListRefresh()
+	},
+	{ deep: true }
 )
 
 // 加载详情
 async function loadDetail () {
 	if (!formData.value.id) return
 	const res = await fwAreaDivideDetailApi({ id: formData.value.id })
-	formData.value = res?.data?.data ?? initForm()
+	const detail = res?.data?.data ?? {}
+	const nextForm = initForm()
+	Object.keys(nextForm).forEach(key => {
+		if (detail[key] !== undefined) {
+			nextForm[key] = detail[key]
+		}
+	})
+	if (detail?.id) {
+		nextForm.id = detail.id
+	}
+	formData.value = nextForm
 	flyDateRange.value = [formData.value.flyDateStart, formData.value.flyDateEnd].filter(Boolean)
-}
-
-// 格式化区域位置
-function formatLocation (row) {
-	if (row?.longitude == null || row?.latitude == null) return ''
-	return `${_.round(row.longitude, 6)}, ${_.round(row.latitude, 6)}`
+	syncShapeListFromDetail(detail)
 }
 
 // 格式化可飞行时间段
@@ -474,42 +499,6 @@
 	return shapeDisplaySource
 }
 
-function registerDeviceRangeMaterial () {
-	if (deviceRangeMaterialRegistered || !Cesium?.Material) return
-	deviceRangeMaterialRegistered = true
-	Cesium.Material._materialCache.addMaterial('DeviceRangeEllipsoid', {
-		fabric: {
-			type: 'DeviceRangeEllipsoid',
-			uniforms: {
-				color: new Cesium.Color(1.0, 0.0, 0.0, 0.7),
-				speed: 3.0,
-			},
-			source: `
-				uniform vec4 color;
-				uniform float speed;
-				czm_material czm_getMaterial(czm_materialInput materialInput){
-					czm_material material = czm_getDefaultMaterial(materialInput);
-					vec2 st = materialInput.st;
-					float time = fract(czm_frameNumber * speed / 1000.0);
-					float alpha = abs(smoothstep(0.5,1.,fract( -st.t - time)));
-					alpha += .1;
-					material.alpha = alpha;
-					material.diffuse = color.rgb;
-					return material;
-				}
-			`,
-		},
-		translucent: () => true,
-	})
-}
-
-function createDeviceRangeMaterial () {
-	registerDeviceRangeMaterial()
-	return Cesium.Material.fromType('DeviceRangeEllipsoid', {
-		color: Cesium.Color.fromBytes(0, 255, 255, 155),
-	})
-}
-
 function clearDeviceRangePrimitives () {
 	if (!viewer || !deviceRangePrimitiveMap.size) return
 	for (const primitive of deviceRangePrimitiveMap.values()) {
@@ -518,62 +507,126 @@
 	deviceRangePrimitiveMap.clear()
 }
 
-function normalizeDeviceRange (value) {
-	const range = Number(value)
-	if (!Number.isFinite(range) || range <= 0) return 2000
-	return range
+function clearDeviceIconEntities () {
+	if (!viewer || !deviceIconEntityMap.size) return
+	for (const entity of deviceIconEntityMap.values()) {
+		viewer.entities.remove(entity)
+	}
+	deviceIconEntityMap.clear()
 }
 
-function getDeviceLngLat (device) {
-	const lngRaw = device?.longitude ?? device?.lng ?? device?.lon
-	const latRaw = device?.latitude ?? device?.lat
-	const lng = Number(lngRaw)
-	const lat = Number(latRaw)
-	if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null
-	return { lng, lat }
+function clearDeviceListState () {
+	deviceOptions.value = []
+	selectedDeviceRows.value = []
+	formData.value.deviceIds = ''
+	clearDeviceRangePrimitives()
+	clearDeviceIconEntities()
+	if (deviceTableRef.value) {
+		deviceTableRef.value.clearSelection()
+	}
 }
 
-function createDeviceRangePrimitive (device) {
+function formatWktNumber (value) {
+	if (!Number.isFinite(Number(value))) return null
+	return Number(value).toFixed(6)
+}
+
+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 buildPolygonWktFromPoints (points) {
+	if (!Array.isArray(points) || points.length < 3) return null
+	const ring = points
+		.map(resolveLngLatPoint)
+		.filter(Boolean)
+		.map(point => ({ lng: point.lng, lat: point.lat }))
+	if (ring.length < 3) return null
+	const first = ring[0]
+	const last = ring[ring.length - 1]
+	if (!isSameWktPoint(first, last)) {
+		ring.push({ ...first })
+	}
+	const coordText = ring
+		.map(point => {
+			const lng = formatWktNumber(point.lng)
+			const lat = formatWktNumber(point.lat)
+			if (lat === null || lng === null) return null
+			return `${lng} ${lat}`
+		})
+		.filter(Boolean)
+		.join(', ')
+	if (!coordText) return null
+	return `POLYGON((${coordText}))`
+}
+
+function buildEllipseWktFromMeta (center, semiMajor, semiMinor) {
 	if (!viewer) return null
-	if (!device) return null
-	const position = getDeviceLngLat(device)
-	if (!position) return null
-	const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm)
-	const center = Cesium.Cartesian3.fromDegrees(position.lng, position.lat, 0)
-	const radii = new Cesium.Cartesian3(rangeMeters, rangeMeters, rangeMeters)
-	const geometry = new Cesium.EllipsoidGeometry({
-		radii,
-		vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED?.vertexFormat
-			|| Cesium.VertexFormat.POSITION_AND_NORMAL,
-	})
-	const instance = new Cesium.GeometryInstance({
-		geometry,
-		modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(center),
-	})
-	return new Cesium.Primitive({
-		geometryInstances: instance,
-		appearance: new Cesium.MaterialAppearance({
-			material: createDeviceRangeMaterial(),
-			closed: true,
-			translucent: true,
-			flat: true,
-			faceForward: true,
-		}),
-		asynchronous: false,
-	})
+	const resolvedCenter = resolveLngLatPoint(center)
+	if (!resolvedCenter) return null
+	if (!Number.isFinite(Number(semiMajor)) || !Number.isFinite(Number(semiMinor))) return null
+	const centerCartesian = Cesium.Cartesian3.fromDegrees(
+		resolvedCenter.lng,
+		resolvedCenter.lat,
+		resolvedCenter.height || 0
+	)
+	const positions = buildEllipsePositions(centerCartesian, Number(semiMajor), Number(semiMinor))
+	if (!positions.length) return null
+	const points = positions
+		.map(item => {
+			const converted = cartesian3Convert(item, viewer)
+			return { lng: converted.longitude, lat: converted.latitude }
+		})
+		.filter(Boolean)
+	return buildPolygonWktFromPoints(points)
 }
 
-function renderDeviceRangeSingle (device) {
+function resolveShapePolygonsWkt () {
+	const polygons = []
+	shapeList.value.forEach(shape => {
+		if (!shape) return
+		if (shape.drawType === 'buffer') {
+			const radii = Array.isArray(shape?.meta?.bufferRadii)
+				? shape.meta.bufferRadii.filter(item => Number.isFinite(item) && item > 0)
+				: []
+			const maxRadius = radii.length ? Math.max(...radii) : null
+			const wkt = buildEllipseWktFromMeta(shape?.meta?.center, maxRadius, maxRadius)
+			if (wkt) polygons.push(wkt)
+			return
+		}
+		if (shape.drawType === 'ellipse') {
+			const wkt = buildEllipseWktFromMeta(shape?.meta?.center, shape?.meta?.semiMajor, shape?.meta?.semiMinor)
+			if (wkt) {
+				polygons.push(wkt)
+				return
+			}
+		}
+		const points = getShapeDisplayPoints(shape)
+		const wkt = buildPolygonWktFromPoints(points)
+		if (wkt) polygons.push(wkt)
+	})
+	return polygons
+}
+
+async function renderDeviceRangeSingle (device) {
 	if (!viewer) return
 	clearDeviceRangePrimitives()
-	const primitive = createDeviceRangePrimitive(device)
+	clearDeviceIconEntities()
+	const position = getDeviceLngLat(device)
+	if (!position) return
+	addDeviceIconEntity(device, position)
+	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)
 }
 
-function renderDeviceRanges (rows) {
+async function renderDeviceRanges (rows) {
 	if (!viewer) return
+	deviceRangeRenderToken += 1
+	const renderToken = deviceRangeRenderToken
 	const selectedRows = Array.isArray(rows) ? rows : []
 	const nextIds = new Set(selectedRows.map(row => String(row?.id)))
 	for (const [id, primitive] of deviceRangePrimitiveMap.entries()) {
@@ -582,19 +635,77 @@
 			deviceRangePrimitiveMap.delete(id)
 		}
 	}
-	selectedRows.forEach(row => {
+	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)
-		if (!id || deviceRangePrimitiveMap.has(id)) return
-		const primitive = createDeviceRangePrimitive(row)
+		return id && !deviceRangePrimitiveMap.has(id)
+	})
+	if (!pendingRows.length) return
+	const primitives = await Promise.all(
+		pendingRows.map(async row => {
+			const position = getDeviceLngLat(row)
+			if (!position) return { row, primitive: null, position: null }
+			const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm)
+			const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters)
+			return { row, primitive, position }
+		})
+	)
+	if (renderToken !== deviceRangeRenderToken) return
+	primitives.forEach(({ row, primitive, position }) => {
+		if (position) {
+			addDeviceIconEntity(row, position)
+		}
 		if (!primitive) return
+		const id = String(row?.id)
+		if (!nextIds.has(id)) return
 		deviceRangePrimitiveMap.set(id, primitive)
 		viewer.scene.primitives.add(primitive)
+	})
+}
+
+async function flyToDevice (device) {
+	if (!viewer) return
+	const position = getDeviceLngLat(device)
+	if (!position) return
+	const height = await resolveDeviceTerrainHeight(viewer, position)
+	const rangeMeters = normalizeDeviceRange(device?.effectiveRangeKm)
+	const destination = Cesium.Cartesian3.fromDegrees(position.lng, position.lat, height + rangeMeters * 3)
+	viewer.camera.flyTo({
+		destination,
+		orientation: {
+			heading: viewer.camera.heading,
+			pitch: Cesium.Math.toRadians(-90),
+			roll: 0,
+		},
+		duration: 0.6,
 	})
 }
 
 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) {
@@ -661,14 +772,11 @@
 }
 
 function getAreaTypeStyle (areaType) {
-	return areaTypeStyleMap?.[String(areaType)] || {
-		fill: Cesium.Color.fromBytes(45, 140, 240, 99),
-		outline: Cesium.Color.fromBytes(45, 140, 240, 255),
-	}
+	return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
 }
 
 function renderShapeList () {
-	if (!viewer || !visible.value || readonly.value) {
+	if (!viewer || !visible.value) {
 		clearShapeDisplay()
 		return
 	}
@@ -683,22 +791,8 @@
 			const [radius1, radius2, radius3] = shape.meta.bufferRadii
 			const radii = [radius1, radius2, radius3].filter(item => Number.isFinite(item) && item > 0)
 			if (radii.length) {
-				const styles = [
-					{
-						fill: Cesium.Color.fromBytes(247, 20, 20, 128),
-						outline: Cesium.Color.fromBytes(255, 0, 0, 255),
-					},
-					{
-						fill: Cesium.Color.fromBytes(255, 235, 59, 128),
-						outline: Cesium.Color.fromBytes(255, 235, 59, 255),
-					},
-					{
-						fill: Cesium.Color.fromBytes(25, 178, 230, 128),
-						outline: Cesium.Color.fromBytes(0, 251, 255, 255),
-					},
-				]
 				radii.forEach((radius, index) => {
-					const style = styles[index] || styles[styles.length - 1]
+					const style = BUFFER_LEVEL_STYLES[index] || BUFFER_LEVEL_STYLES[BUFFER_LEVEL_STYLES.length - 1]
 					dataSource.entities.add({
 						name: 'shape-display',
 						customData: { shapeId: shape.id, drawType: shape.drawType, level: index + 1 },
@@ -721,6 +815,10 @@
 							width: 2,
 							material: style.outline,
 						},
+					})
+					addBufferRadiusLabelEntity(dataSource, centerCartesian, radius, {
+						name: 'shape-display',
+						customData: { drawType: 'buffer', label: 'radius' },
 					})
 				})
 			}
@@ -750,14 +848,75 @@
 	})
 }
 
+function pushRadiusBoundingPoints (positions, center, radius) {
+	if (!center || !Number.isFinite(Number(radius)) || radius <= 0) return
+	const resolved = resolveLngLatPoint(center) || {}
+	const lng = Number(resolved.lng ?? resolved.longitude)
+	const lat = Number(resolved.lat ?? resolved.latitude)
+	if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
+	const baseLat = Cesium.Math.toRadians(lat)
+	const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius
+	const deltaLat = radius / earthRadius
+	const deltaLng = radius / (earthRadius * Math.max(Math.cos(baseLat), 0.000001))
+	const points = [
+		{ lng: Cesium.Math.toDegrees(Cesium.Math.toRadians(lng) + deltaLng), lat },
+		{ lng: Cesium.Math.toDegrees(Cesium.Math.toRadians(lng) - deltaLng), lat },
+		{ lng, lat: Cesium.Math.toDegrees(baseLat + deltaLat) },
+		{ lng, lat: Cesium.Math.toDegrees(baseLat - deltaLat) },
+	]
+	points.forEach(point => {
+		positions.push(Cesium.Cartesian3.fromDegrees(point.lng, point.lat, resolved.height || 0))
+	})
+}
+
+function collectShapeBoundingPositions () {
+	const positions = []
+	if (!viewer || !shapeList.value.length) return positions
+	shapeList.value.forEach(shape => {
+		let points = getShapeDisplayPoints(shape)
+		if (!Array.isArray(points) || !points.length) {
+			points = resolveControlPoints(shape?.meta, shape?.drawType)
+		}
+		if (Array.isArray(points) && points.length) {
+			points.forEach(point => {
+				const resolved = resolveLngLatPoint(point)
+				const normalized = normalizeShapePoint(resolved)
+				if (!normalized) return
+				positions.push(Cesium.Cartesian3.fromDegrees(normalized.lng, normalized.lat, normalized.height))
+			})
+		}
+		if (shape?.drawType === 'buffer' && shape?.meta?.center && Array.isArray(shape?.meta?.bufferRadii)) {
+			const maxRadius = Math.max(...shape.meta.bufferRadii.filter(item => Number.isFinite(item)))
+			pushRadiusBoundingPoints(positions, shape.meta.center, maxRadius)
+		}
+		if (shape?.drawType === 'ellipse' && shape?.meta?.center) {
+			const major = Number(shape?.meta?.semiMajor)
+			const minor = Number(shape?.meta?.semiMinor)
+			const radius = Number.isFinite(major) && Number.isFinite(minor) ? Math.max(major, minor) : null
+			pushRadiusBoundingPoints(positions, shape.meta.center, radius)
+		}
+	})
+	return positions
+}
+
+function fitViewToShapeList () {
+	if (!viewer || !shapeList.value.length) return
+	const positions = collectShapeBoundingPositions()
+	if (positions.length < 2) return
+	const boundingSphere = Cesium.BoundingSphere.fromPoints(positions)
+	const range = Math.max(boundingSphere.radius * 2.4, 200)
+	viewer.camera.flyToBoundingSphere(boundingSphere, {
+		duration: 0,
+		offset: new Cesium.HeadingPitchRange(viewer.camera.heading, Cesium.Math.toRadians(-90), range),
+	})
+}
+
 // 绘制完成回调
-const drawFinished = async data => {
+const drawFinished = async (data, options = {}) => {
+	const shouldContinueDraw = options?.continueDraw || activeToolMode.value === 'draw'
 	const positions = Array.isArray(data?.positions) ? data.positions : data
 	if (!Array.isArray(positions) || positions.length < 3) {
 		pointList = []
-		formData.value.areaSize = null
-		formData.value.longitude = null
-		formData.value.latitude = null
 		hideTypePanel()
 		return
 	}
@@ -769,13 +928,15 @@
 		const val = cartesian3Convert(item, viewer)
 		return { ...val, lng: val.longitude, lat: val.latitude }
 	})
-	applyPolygonMetrics(pointList)
 	const controlPoints = resolveControlPoints(meta, currentShapeType.value)
-	if (!formData.value.areaType) {
+	if (!activeAreaType.value) {
 		const detectAreaType = getDetectAreaTypeKey()
 		if (detectAreaType) {
-			formData.value.areaType = detectAreaType
+			activeAreaType.value = detectAreaType
 		}
+	}
+	if (currentShapeType.value === 'buffer') {
+		activeAreaType.value = bufferAreaTypeValue.split(',')
 	}
 	if (!suppressTypePanelOnce.value) {
 		showTypePanelAtCurrent()
@@ -817,7 +978,7 @@
 	}
 	const shapePayload = {
 		id: activeShapeId.value,
-		areaType: formData.value.areaType,
+		areaType: currentShapeType.value === 'buffer' ? bufferAreaTypeValue : activeAreaType.value,
 		drawType: currentShapeType.value,
 		points: ['ellipse', 'buffer'].includes(currentShapeType.value) && controlPoints.length
 			? controlPoints
@@ -831,31 +992,94 @@
 		shapeList.value.splice(targetIndex, 1, shapePayload)
 	}
 	renderShapeList()
-}
-
-function applyPolygonMetrics(points) {
-	if (!points?.length) return
-	const polygon = turf.polygon([
-		[...points.map(item => [item.longitude, item.latitude]), [points[0].longitude, points[0].latitude]],
-	])
-	const center = turf.centerOfMass(polygon)
-	const areaSqm = turf.area(polygon) // 平方米
-	formData.value.areaSize = _.round(areaSqm / 1_000_000, 2) // 平方千米
-	formData.value.longitude = center.geometry.coordinates[0]
-	formData.value.latitude = center.geometry.coordinates[1]
+	if (shouldContinueDraw && !readonly.value) {
+		activeShapeId.value = null
+		pointList = []
+		clearActiveTool()
+		startDraw(currentShapeType.value || 'polygon')
+	}
 }
 
 function getDetectAreaTypeKey () {
-	const areaTypeOptions = Array.isArray(dictObj?.value?.areaType)
-		? dictObj.value.areaType
-		: dictObj?.areaType
-
-
-	console.log(areaTypeOptions, 1)
-
+	const areaTypeOptions = resolveAreaTypeOptions()
 	if (!Array.isArray(areaTypeOptions)) return ''
 	const target = areaTypeOptions.find(item => item?.dictKey === '1')
 	return target?.dictKey ?? ''
+}
+
+function resolveAreaTypeOptions () {
+	return Array.isArray(dictObj?.value?.areaType)
+		? dictObj.value.areaType
+		: dictObj?.areaType
+}
+
+function resolveShapeAreaTypeLabel (shape) {
+	return getDictLabel(shape?.areaType, resolveAreaTypeOptions())
+}
+
+function parseGeomJson (geomJson) {
+	if (!geomJson) return null
+	if (typeof geomJson === 'object') return geomJson
+	if (typeof geomJson !== 'string') return null
+	const trimmed = geomJson.trim()
+	if (!trimmed) return null
+	try {
+		return JSON.parse(trimmed)
+	} catch (error) {
+		const legacyPoints = geomAnalysis(trimmed)
+		if (Array.isArray(legacyPoints) && legacyPoints.length >= 3) {
+			return { drawType: 'polygon', points: legacyPoints }
+		}
+	}
+	return null
+}
+
+function syncShapeListFromDetail (detail) {
+	const extList = Array.isArray(detail?.fwAreaDivideExtList) ? detail.fwAreaDivideExtList : []
+	if (!extList.length) {
+		shapeList.value = []
+		activeAreaType.value = ''
+		currentShapeType.value = 'polygon'
+		return
+	}
+	shapeList.value = extList.map((item, index) => {
+		const isShapePayload = item?.drawType || item?.points
+		if (isShapePayload) {
+			const areaType = item?.drawType === 'buffer'
+				? bufferAreaTypeValue
+				: (item?.areaType ?? '')
+			return {
+				id: item?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+				extId: item?.extId ?? item?.id,
+				areaType,
+				drawType: item?.drawType ?? 'polygon',
+				points: Array.isArray(item?.points) ? item.points : [],
+				displayPoints: Array.isArray(item?.displayPoints) ? item.displayPoints : null,
+				meta: item?.meta ?? null,
+			}
+		}
+		const parsed = parseGeomJson(item?.geomJson) || {}
+		const drawType = parsed?.drawType || 'polygon'
+		const areaType = drawType === 'buffer'
+			? bufferAreaTypeValue
+			: (item?.areaTypeKey ?? parsed?.areaType ?? '')
+		const points = Array.isArray(parsed?.points) ? parsed.points : []
+		const displayPoints = Array.isArray(parsed?.displayPoints) ? parsed.displayPoints : null
+		const meta = parsed?.meta ?? null
+		return {
+			id: parsed?.id || `shape_${Date.now()}_${index}_${Math.random().toString(16).slice(2, 6)}`,
+			extId: item?.id,
+			areaType,
+			drawType,
+			points,
+			displayPoints,
+			meta,
+		}
+	})
+	activeAreaType.value = shapeList.value[0]?.drawType === 'buffer'
+		? bufferAreaTypeValue.split(',')
+		: (shapeList.value[0]?.areaType ?? '')
+	currentShapeType.value = shapeList.value[0]?.drawType ?? 'polygon'
 }
 
 function showTypePanelAtCurrent () {
@@ -874,11 +1098,13 @@
 	const positions = activeTool?.getPositions?.()
 	if (Array.isArray(positions) && positions.length >= 3) {
 		suppressTypePanelOnce.value = true
-		drawFinished(positions)
+		drawFinished(positions, { continueDraw: true })
+		return
 	}
 	if (positions?.positions?.length >= 3) {
 		suppressTypePanelOnce.value = true
-		drawFinished(positions)
+		drawFinished(positions, { continueDraw: true })
+		return
 	}
 	clearActiveTool()
 }
@@ -888,9 +1114,8 @@
 }
 
 function updateActiveShapeAreaType (value) {
-	if (currentShapeType.value !== 'buffer') {
-		activeTool?.setStyle?.(getAreaTypeStyle(value))
-	}
+	if (currentShapeType.value === 'buffer') return
+	activeTool?.setStyle?.(getAreaTypeStyle(value))
 	if (!activeShapeId.value) return
 	const targetIndex = shapeList.value.findIndex(item => item.id === activeShapeId.value)
 	if (targetIndex === -1) return
@@ -916,7 +1141,7 @@
 	currentShapeType.value = type
 	activeToolMode.value = 'draw'
 	drawManager ||= new DrawManager(viewer)
-	activeTool = drawManager.start(type, { style: getAreaTypeStyle(formData.value.areaType) })
+	activeTool = drawManager.start(type, { style: getAreaTypeStyle(activeAreaType.value) })
 	activeTool?.subscribe('getPolygonPositions', drawFinished)
 }
 
@@ -924,7 +1149,7 @@
 	currentShapeType.value = type
 	activeToolMode.value = 'edit'
 	editManager ||= new EditManager(viewer)
-	activeTool = editManager.start(type, points, { style: getAreaTypeStyle(formData.value.areaType) })
+	activeTool = editManager.start(type, points, { style: getAreaTypeStyle(activeAreaType.value) })
 	activeTool?.subscribe('getPolygonPositions', drawFinished)
 }
 
@@ -936,22 +1161,12 @@
 	viewEntity = null
 	hideTypePanel()
 	renderShapeList()
-	if (dialogMode.value === 'edit' && pointList.length >= 3) {
-		const editPoints = _.cloneDeep(pointList)
-		if (editPoints.length > 1) {
-			const first = editPoints[0]
-			const last = editPoints[editPoints.length - 1]
-			if (first.longitude === last.longitude && first.latitude === last.latitude) {
-				editPoints.pop()
-			}
-		}
-		activeShapeId.value = activeShapeId.value || (shapeList.value[0]?.id ?? null)
-		startEdit(type, editPoints)
-		return
-	}
+	pointList = []
 	const defaultAreaType = getDetectAreaTypeKey()
-	if (defaultAreaType) {
-		formData.value.areaType = defaultAreaType
+	if (type === 'buffer') {
+		activeAreaType.value = bufferAreaTypeValue
+	} else if (defaultAreaType) {
+		activeAreaType.value = defaultAreaType
 	}
 	activeShapeId.value = null
 	startDraw(type)
@@ -959,17 +1174,17 @@
 
 function handleClearShape () {
 	if (readonly.value) return
-	clearActiveTool()
 	viewEntity && viewer?.entities?.remove(viewEntity)
 	viewEntity = null
 	pointList = []
 	shapeList.value = []
 	activeShapeId.value = null
-	formData.value.areaSize = null
-	formData.value.longitude = null
-	formData.value.latitude = null
 	hideTypePanel()
 	clearShapeDisplay()
+	clearActiveTool()
+	if (viewer && currentShapeType.value) {
+		startDraw(currentShapeType.value)
+	}
 }
 
 function handleShapeEdit (row) {
@@ -977,7 +1192,7 @@
 	if (activeToolMode.value === 'edit' && activeShapeId.value === row.id) return
 	activeShapeId.value = row.id
 	currentShapeType.value = row.drawType
-	formData.value.areaType = row.areaType
+	activeAreaType.value = row.drawType === 'buffer' ? bufferAreaTypeValue.split(',') : row.areaType
 	const displayPoints = getShapeDisplayPoints(row)
 	pointList = _.cloneDeep(displayPoints)
 	clearActiveTool()
@@ -985,7 +1200,6 @@
 		? { points: displayPoints, meta: row.meta }
 		: row.points
 	startEdit(row.drawType, editPayload)
-	applyPolygonMetrics(pointList)
 	showTypePanelAtCurrent()
 	renderShapeList()
 }
@@ -998,15 +1212,18 @@
 		activeShapeId.value = null
 		clearActiveTool()
 		pointList = []
-		formData.value.areaSize = null
-		formData.value.longitude = null
-		formData.value.latitude = null
 		hideTypePanel()
 	}
 }
 
 // 查看面
 function viewPolygon () {
+	if (!viewer) return
+	if (shapeList.value.length) {
+		renderShapeList()
+		fitViewToShapeList()
+		return
+	}
 	if (!formData.value?.geom) return
 	pointList = geomAnalysis(formData.value.geom)
 	const result = pointList.map(item => [item.longitude, item.latitude]).flat()
@@ -1024,18 +1241,87 @@
 }
 
 // 获取派出所列表
-async function getPoliceStationList () {
-	const res = await fwPoliceStationListApi()
-	policeStationOptions.value = res?.data?.data ?? []
+async function getPoliceStationList (keyword = '') {
+	if (!visible.value) return
+	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
+	}
+}
+
+function handlePoliceStationSearch (query) {
+	policeStationSearchKeyword.value = (query || '').trim()
+	if (!policeStationSearchKeyword.value) {
+		policeStationOptions.value = [...policeStationAllOptions.value]
+		return
+	}
+	if (policeStationSearchTimer.value) {
+		clearTimeout(policeStationSearchTimer.value)
+	}
+	policeStationSearchTimer.value = setTimeout(() => {
+		policeStationSearchTimer.value = null
+		void getPoliceStationList(policeStationSearchKeyword.value)
+	}, 300)
 }
 
 // 获取设备列表
-async function getDeviceList () {
-	if (deviceOptions.value.length) return
-	const res = await fwDeviceListApi({ isAreaSelect: 1, areaId: formData.value.id, isTrack: 1 })
+function queueDeviceListRefresh () {
+	if (!visible.value) return
+	if (deviceListTimer) {
+		clearTimeout(deviceListTimer)
+	}
+	deviceListTimer = setTimeout(() => {
+		deviceListTimer = null
+		void getDeviceListByPolygons()
+	}, 200)
+}
+
+async function getDeviceListByPolygons () {
+	if (!visible.value) return
+	const polygons = resolveShapePolygonsWkt()
+	if (!polygons.length) {
+		clearDeviceListState()
+		return
+	}
+	deviceListRequestToken += 1
+	const requestToken = deviceListRequestToken
+	const res = await fwDeviceListByPolygonsApi({
+		areaId: dialogMode.value === 'add' ? '' : formData.value.id,
+		isAreaSelect: 1,
+		isTrack: 1,
+		polygons,
+	})
+	if (requestToken !== deviceListRequestToken) return
 	deviceOptions.value = res?.data?.data ?? []
-	if (dialogMode.value === 'view') {
-		deviceOptions.value = deviceOptions.value.filter(item => formData.value.deviceIds.includes(item.id))
+	if (dialogMode.value === 'view' && formData.value.deviceIds) {
+		const targetIds = formData.value.deviceIds.split(',')
+		deviceOptions.value = deviceOptions.value.filter(item => targetIds.includes(String(item.id)))
+	}
+	await nextTick()
+	const prevSuppressDeviceFly = suppressDeviceFly.value
+	suppressDeviceFly.value = true
+	syncDeviceSelection()
+	suppressDeviceFly.value = prevSuppressDeviceFly
+	if (!readonly.value) {
+		renderDeviceRanges(selectedDeviceRows.value)
 	}
 }
 
@@ -1045,79 +1331,127 @@
 	const ids = rows.map(item => item.id)
 	formData.value.deviceIds = ids.join(',')
 	renderDeviceRanges(rows)
+	if (suppressDeviceFly.value) {
+		lastSelectedDeviceIds = [...ids]
+		return
+	}
+	const prevSet = new Set(lastSelectedDeviceIds)
+	const addedRow = rows.find(row => !prevSet.has(row.id))
+	lastSelectedDeviceIds = [...ids]
+	if (addedRow) {
+		void flyToDevice(addedRow)
+	}
 }
 
 function handleDeviceRowClick (row) {
 	if (readonly.value) {
 		renderDeviceRangeSingle(row)
+		void flyToDevice(row)
 		return
 	}
 	renderDeviceRanges(selectedDeviceRows.value)
+	void flyToDevice(row)
 }
 
 // 同步选择状态
 function syncDeviceSelection () {
-	if (!deviceTableRef.value) return
-	deviceTableRef.value.clearSelection()
+	if (isDialogInitializing.value && !deviceOptions.value.length) return
 	const rows = []
-	const arr = formData.value.deviceIds.split(',')
+	const arr = formData.value.deviceIds ? formData.value.deviceIds.split(',') : []
 	deviceOptions.value.forEach(row => {
-		if (arr.includes(row.id)) {
-			deviceTableRef.value.toggleRowSelection(row, true)
+		if (arr.includes(String(row.id))) {
 			rows.push(row)
 		}
 	})
 	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 => {
+		deviceTableRef.value.toggleRowSelection(row, true)
+	})
 }
 
 // 打开弹框
 async function open ({ mode, row } = {}) {
+	isDialogInitializing.value = true
 	dialogMode.value = mode || 'add'
 	formData.value = dialogMode.value === 'add' ? initForm() : row
 	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 = ''
+	pointList = []
 	await nextTick()
 	initMap()
 	clearShapeDisplay()
 	clearDeviceRangePrimitives()
+	clearDeviceIconEntities()
 	viewEntity && viewer?.entities?.remove(viewEntity)
 	viewEntity = null
 	clearActiveTool()
-	await getDeviceList()
 	if (dialogMode.value === 'add') {
-		// default no draw mode
+		mapRef.value?.zoomToAdminBoundary?.()
+		handleDraw('polygon')
 	} else if (dialogMode.value === 'edit') {
 		await loadDetail()
-		pointList = geomAnalysis(formData.value.geom) || []
-		if (pointList.length) {
-			activeShapeId.value = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`
-			shapeList.value = [
-				{
-					id: activeShapeId.value,
-					areaType: formData.value.areaType,
-					drawType: currentShapeType.value,
-					points: _.cloneDeep(pointList),
-				},
-			]
-		}
 		viewPolygon()
+		activeShapeId.value = null
+		pointList = []
+		const firstShape = shapeList.value[0]
+		const initialType = firstShape?.drawType || 'polygon'
+		currentShapeType.value = initialType
+		if (initialType === 'buffer') {
+			activeAreaType.value = bufferAreaTypeValue.split(',')
+		} else if (firstShape?.areaType) {
+			activeAreaType.value = firstShape.areaType
+		} else {
+			const defaultAreaType = getDetectAreaTypeKey()
+			if (defaultAreaType) {
+				activeAreaType.value = defaultAreaType
+			}
+		}
+		clearActiveTool()
+		startDraw(initialType)
 	} else {
 		await loadDetail()
 		viewPolygon()
 	}
 	await nextTick()
+	suppressDeviceFly.value = true
 	syncDeviceSelection()
+	lastSelectedDeviceIds = selectedDeviceRows.value.map(row => row.id)
+	await nextTick()
+	suppressDeviceFly.value = false
 	if (!readonly.value) {
 		renderDeviceRanges(selectedDeviceRows.value)
 	}
+	isDialogInitializing.value = false
+	queueDeviceListRefresh()
+	void getPoliceStationList()
 }
 
-onMounted(() => {
-	getPoliceStationList()
-})
-
 onBeforeUnmount(() => {
+	if (deviceListTimer) {
+		clearTimeout(deviceListTimer)
+		deviceListTimer = null
+	}
+	if (policeStationSearchTimer.value) {
+		clearTimeout(policeStationSearchTimer.value)
+		policeStationSearchTimer.value = null
+	}
 	hideTypePanel()
 })
 
@@ -1179,52 +1513,40 @@
 	right: 16px;
 	display: flex;
 	flex-direction: column;
-	gap: 10px;
-	padding: 10px 8px;
-	background: rgba(19, 28, 36, 0.75);
+	gap: 14px;
+	padding: 32px 14px;
+	background: rgba(26, 25, 25, 0.6);
+	border-radius: 8px 8px 8px 8px;
 	backdrop-filter: blur(4px);
-	border-radius: 8px;
-	box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2);
 }
 
 .tool-btn {
+	padding: 0 !important;
+	width: 46px;
+	height: 46px;
 	display: flex;
 	flex-direction: column;
 	align-items: center;
-	gap: 6px;
-	width: 64px;
-	padding: 8px 6px;
-	background: rgba(255, 255, 255, 0.08);
-	border: 1px solid rgba(255, 255, 255, 0.18);
-	border-radius: 6px;
-	color: #ffffff;
-	font-size: 12px;
-	line-height: 1;
+	border-radius: 8px;
+	border: none !important;
+	box-shadow: none;
 	cursor: pointer;
-	transition: all 0.2s ease;
+	overflow: hidden;
+	background: transparent !important;
+	box-sizing: border-box;
 }
 
-.tool-btn:hover {
-	background: rgba(255, 255, 255, 0.16);
-	border-color: rgba(255, 255, 255, 0.35);
-	transform: translateY(-1px);
-}
-
-.tool-btn:active {
-	transform: translateY(0);
-}
-
-.tool-btn.danger {
-	border-color: rgba(255, 112, 112, 0.6);
-	color: #ffb3b3;
+.tool-btn.active {
+	border: 2px solid #FFFFFF !important;
 }
 
 .tool-icon {
 	position: relative;
-	width: 20px;
-	height: 20px;
-	border: 2px solid #ffffff;
-	border-radius: 3px;
+}
+
+.tool-icon-image {
+	width: 100%;
+	height: 100%;
 }
 
 .tool-icon.polygon {
@@ -1272,23 +1594,21 @@
 }
 
 .shape-table-container {
-	margin-top: 8px;
-	padding: 8px;
-	border-radius: 8px;
-	background: rgba(27, 34, 56, 0.6);
-}
+	margin-bottom: 20px;
 
-.shape-table-title {
-	display: flex;
-    align-items: center;
-    justify-content: flex-start;
-    padding-right: 1.2rem;
-    font-family: Source Han Sans CN, Source Han Sans CN;
-    font-weight: 500;
-    font-size: 1.4rem;
-    color: #D4D5D7;
-    font-style: normal;
-    text-transform: none;
-    box-sizing: border-box;
+	.shape-table-title {
+		margin-bottom: 20px;
+		display: flex;
+		align-items: center;
+		justify-content: flex-start;
+		padding-right: 1.2rem;
+		font-family: Source Han Sans CN, Source Han Sans CN;
+		font-weight: 500;
+		font-size: 1.4rem;
+		color: #D4D5D7;
+		font-style: normal;
+		text-transform: none;
+		box-sizing: border-box;
+	}
 }
 </style>

--
Gitblit v1.9.3