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 | 1485 +++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 files changed, 1,334 insertions(+), 151 deletions(-)
diff --git a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
index 32318d9..342b43c 100644
--- a/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
+++ b/applications/drone-command/src/views/areaManage/partition/FormDiaLog.vue
@@ -1,24 +1,66 @@
-<template>
- <el-dialog class="command-page-map-view-dialog" v-model="visible" :show-close="false"
- :close-on-click-modal="false">
+<template>
+ <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"
- />
+ <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>
+ </div>
+ <div class="panel-body">
+ <span class="panel-label">区域类型</span>
+ <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">
+ <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">
@@ -29,20 +71,10 @@
<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>
- <div class="val">{{ formData.triggerCondition }}</div>
+ <div class="val">
+ {{ getDictLabel(formData.triggerCondition, dictObj.triggeringCondition) }}
+ </div>
</el-col>
<el-col :span="24">
<div class="label">响应机制</div>
@@ -62,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">
+ <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 }">
@@ -81,29 +131,19 @@
<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-input class="command-input" v-model="formData.triggerCondition"
- maxlength="200" placeholder="请输入" clearable />
+ <el-select class="command-select" popper-class="command-select-popper"
+ v-model="formData.triggerCondition" placeholder="请选择" clearable>
+ <el-option v-for="item in dictObj.triggeringCondition" :key="item.dictKey"
+ :label="item.dictValue" :value="item.dictKey" />
+ </el-select>
</el-form-item>
<el-form-item label="管控级别" prop="controlLevel">
<el-select class="command-select" popper-class="command-select-popper"
@@ -113,18 +153,41 @@
</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="结束时间"
+ <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>
</el-form-item>
+
+ <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-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-column label="操作" class-name="operation-btns" width="120">
+ <template v-slot="{ row }">
+ <el-link @click="handleShapeEdit(row)">编辑</el-link>
+ <el-link @click="handleShapeDelete(row)">删除</el-link>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
<div class="search-table-container">
<div class="search-box">
@@ -132,14 +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">
+ @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="类型">
@@ -153,7 +215,9 @@
</el-form>
<div class="footer">
- <el-button 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">
确定
@@ -167,28 +231,43 @@
<script setup>
import { Close } from '@element-plus/icons-vue'
-import { computed, inject, nextTick, 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 { DrawPolygon } from '@/utils/cesium/DrawPolygon'
+import {
+ DrawManager,
+ EditManager,
+ addBufferRadiusLabelEntity,
+ buildEllipsePositions,
+} from '@/utils/cesium/shapeTools'
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
-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: '', // 触发条件
@@ -198,32 +277,93 @@
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()
const titleEnum = ref({ edit: '编辑区域', view: '查看区域', add: '新增区域' })
const flyDateRange = ref([]) // 飞行时间
const deviceTableRef = ref(null)
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 = {
+ polygon: '多边形',
+ rectangle: '矩形',
+ ellipse: '椭圆',
+ buffer: '缓冲圆',
+}
+const bufferAreaTypeValue = '1,2,3'
+const showTypePanel = ref(false)
+const suppressTypePanelOnce = ref(false)
let viewer
-let drawPolygonExample
+let drawManager
+let editManager
+let activeTool
let pointList = []
+let viewEntity
+let shapeDisplaySource
+const deviceRangePrimitiveMap = new Map()
+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),
flyDateStart: fieldRules(true),
policeStationId: fieldRules(true),
- deviceIds: fieldRules(true),
+ deviceIds: fieldRules(false),
+}
+
+function isSameDay (left, right) {
+ return (
+ left.getFullYear() === right.getFullYear() &&
+ left.getMonth() === right.getMonth() &&
+ left.getDate() === right.getDate()
+ )
+}
+
+const disabledFlyDate = time => {
+ const startOfToday = new Date()
+ startOfToday.setHours(0, 0, 0, 0)
+ return time.getTime() < startOfToday.getTime()
+}
+
+const disabledFlyTime = date => {
+ if (!date) return {}
+ const now = new Date()
+ if (!isSameDay(date, now)) return {}
+ const currentHour = now.getHours()
+ const currentMinute = now.getMinutes()
+ const currentSecond = now.getSeconds()
+ return {
+ disabledHours: () => Array.from({ length: currentHour }, (_, index) => index),
+ disabledMinutes: hour =>
+ hour === currentHour ? Array.from({ length: currentMinute }, (_, index) => index) : [],
+ disabledSeconds: (hour, minute) =>
+ hour === currentHour && minute === currentMinute
+ ? Array.from({ length: currentSecond }, (_, index) => index)
+ : [],
+ }
}
// 关闭弹框
@@ -233,17 +373,39 @@
// 提交新增/编辑
async function handleSubmit () {
- 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²,请重新绘制')
+ if (!shapeList.value.length) {
+ ElMessage.warning('区域不可为空,请先绘制区域')
return
}
+
+ const isValid = await formRef.value?.validate().catch(() => false)
+ if (!isValid) 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,
+ title: `${route.name || '区域划分'}-${actionText}`,
+ type: 1,
+ })
ElMessage.success(dialogMode.value === 'add' ? '新增成功' : '更新成功')
visible.value = false
emit('success')
@@ -259,19 +421,60 @@
formData.value.flyDateEnd = flyDateRange.value[1] || ''
}
)
+watch(
+ () => 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(
+ () => 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)
}
// 格式化可飞行时间段
@@ -287,66 +490,744 @@
viewer = map?.viewer || null
}
+function ensureShapeDisplaySource () {
+ if (!viewer) return null
+ if (!shapeDisplaySource) {
+ shapeDisplaySource = new Cesium.CustomDataSource('partition-shape-display')
+ viewer.dataSources.add(shapeDisplaySource)
+ }
+ return shapeDisplaySource
+}
+
+function clearDeviceRangePrimitives () {
+ if (!viewer || !deviceRangePrimitiveMap.size) return
+ for (const primitive of deviceRangePrimitiveMap.values()) {
+ viewer.scene.primitives.remove(primitive)
+ }
+ 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 = []
+ formData.value.deviceIds = ''
+ clearDeviceRangePrimitives()
+ clearDeviceIconEntities()
+ if (deviceTableRef.value) {
+ deviceTableRef.value.clearSelection()
+ }
+}
+
+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
+ 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 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()
+ 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)
+}
+
+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()) {
+ if (!nextIds.has(id)) {
+ viewer.scene.primitives.remove(primitive)
+ 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)
+ })
+ 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) {
+ const lng = point?.lng ?? point?.longitude
+ const lat = point?.lat ?? point?.latitude
+ if (!Number.isFinite(Number(lng)) || !Number.isFinite(Number(lat))) return null
+ return {
+ lng: Number(lng),
+ lat: Number(lat),
+ height: Number.isFinite(Number(point?.height)) ? Number(point.height) : 0,
+ }
+}
+
+function resolveLngLatPoint (point) {
+ if (!point) return null
+ const lng = point?.lng ?? point?.longitude
+ const lat = point?.lat ?? point?.latitude
+ if (Number.isFinite(Number(lng)) && Number.isFinite(Number(lat))) {
+ return {
+ lng: Number(lng),
+ lat: Number(lat),
+ height: Number.isFinite(Number(point?.height)) ? Number(point.height) : 0,
+ }
+ }
+ if (!viewer || !Number.isFinite(point?.x) || !Number.isFinite(point?.y) || !Number.isFinite(point?.z)) {
+ return null
+ }
+ const val = cartesian3Convert(point, viewer)
+ return {
+ lng: val.longitude,
+ lat: val.latitude,
+ height: Number.isFinite(val.height) ? val.height : 0,
+ }
+}
+
+function resolveControlPoints (meta, drawType) {
+ if (!meta) return []
+ if (Array.isArray(meta?.controlPoints)) {
+ return meta.controlPoints.map(resolveLngLatPoint).filter(Boolean)
+ }
+ if (drawType === 'ellipse') {
+ return [meta.center, meta.majorPoint, meta.minorPoint].map(resolveLngLatPoint).filter(Boolean)
+ }
+ if (drawType === 'buffer') {
+ return [meta.center, meta.radiusPoint1, meta.radiusPoint2, meta.radiusPoint3]
+ .map(resolveLngLatPoint)
+ .filter(Boolean)
+ }
+ return []
+}
+
+function buildShapePositions (points = []) {
+ const normalized = points.map(normalizeShapePoint).filter(Boolean)
+ return normalized.map(point =>
+ Cesium.Cartesian3.fromDegrees(point.lng, point.lat, point.height)
+ )
+}
+
+function getShapeDisplayPoints (shape) {
+ if (Array.isArray(shape?.displayPoints) && shape.displayPoints.length) {
+ return shape.displayPoints
+ }
+ return shape?.points || []
+}
+
+function getAreaTypeStyle (areaType) {
+ return AREA_TYPE_STYLE_MAP?.[String(areaType)] || DEFAULT_AREA_STYLE
+}
+
+function renderShapeList () {
+ if (!viewer || !visible.value) {
+ clearShapeDisplay()
+ return
+ }
+ const dataSource = ensureShapeDisplaySource()
+ if (!dataSource) return
+ dataSource.entities.removeAll()
+ shapeList.value.forEach(shape => {
+ if (activeToolMode.value === 'edit' && shape.id === activeShapeId.value) return
+ if (shape.drawType === 'buffer' && shape.meta?.bufferRadii?.length && shape.meta?.center) {
+ const center = shape.meta.center
+ const centerCartesian = Cesium.Cartesian3.fromDegrees(center.lng, center.lat, center.height || 0)
+ const [radius1, radius2, radius3] = shape.meta.bufferRadii
+ const radii = [radius1, radius2, radius3].filter(item => Number.isFinite(item) && item > 0)
+ if (radii.length) {
+ radii.forEach((radius, index) => {
+ 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 },
+ position: centerCartesian,
+ ellipse: {
+ semiMajorAxis: radius,
+ semiMinorAxis: radius,
+ material: style.fill,
+ outline: false,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ })
+ const positions = buildEllipsePositions(centerCartesian, radius, radius)
+ dataSource.entities.add({
+ name: 'shape-display',
+ customData: { shapeId: shape.id, drawType: shape.drawType, level: index + 1, outline: true },
+ polyline: {
+ positions: positions.length ? [...positions, positions[0]] : positions,
+ clampToGround: true,
+ width: 2,
+ material: style.outline,
+ },
+ })
+ addBufferRadiusLabelEntity(dataSource, centerCartesian, radius, {
+ name: 'shape-display',
+ customData: { drawType: 'buffer', label: 'radius' },
+ })
+ })
+ }
+ return
+ }
+
+ if (!shape?.points?.length) return
+ const positions = buildShapePositions(getShapeDisplayPoints(shape))
+ if (positions.length < 3) return
+ const style = getAreaTypeStyle(shape.areaType)
+ dataSource.entities.add({
+ name: 'shape-display',
+ customData: { shapeId: shape.id, drawType: shape.drawType },
+ polygon: {
+ hierarchy: new Cesium.PolygonHierarchy(positions),
+ material: style.fill,
+ outline: false,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ },
+ polyline: {
+ positions: positions.length ? [...positions, positions[0]] : positions,
+ clampToGround: true,
+ width: 2,
+ material: style.outline,
+ },
+ })
+ })
+}
+
+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 => {
- pointList = _.cloneDeep(data).map(item => {
+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 = []
+ hideTypePanel()
+ return
+ }
+ const meta = data?.meta
+ const bufferRadii = Array.isArray(meta?.bufferRadii) ? meta.bufferRadii : null
+ const centerCartesian = meta?.center
+ const centerLngLat = centerCartesian ? cartesian3Convert(centerCartesian, viewer) : null
+ pointList = _.cloneDeep(positions).map(item => {
const val = cartesian3Convert(item, viewer)
return { ...val, lng: val.longitude, lat: val.latitude }
})
- const polygon = turf.polygon([
- [...pointList.map(item => [item.longitude, item.latitude]), [pointList[0].longitude, pointList[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]
-}
-
-// 新增模式绘制
-function addPolygon () {
- drawPolygonExample = new DrawPolygon(viewer)
- drawPolygonExample.initHandler(viewer)
- drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
-}
-
-// 编辑面
-function editPolygon () {
- if (!formData.value?.geom) return
- pointList = geomAnalysis(formData.value.geom)
- drawPolygonExample?.destroy()
- drawPolygonExample = new DrawPolygon(viewer)
- let pointList1 = _.cloneDeep(pointList)
- pointList1.pop()
- drawPolygonExample.initPolygon(
- viewer,
- pointList.map(item => ({ lng: item.longitude, lat: item.latitude }))
- )
- drawPolygonExample.subscribe('getPolygonPositions', drawFinished)
- if (pointList1.length) {
- const result = pointList1.map(item => [item.longitude, item.latitude]).flat()
- const tempEntity = viewer.entities?.add({
- position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
- polyline: {
- positions: Cesium.Cartesian3.fromDegreesArray(result),
- clampToGround: true,
- width: 1,
- material: Cesium.Color.TRANSPARENT,
- },
- })
- if (tempEntity) {
- viewer.flyTo(tempEntity, { duration: 0 })
- viewer.entities.remove(tempEntity)
+ const controlPoints = resolveControlPoints(meta, currentShapeType.value)
+ if (!activeAreaType.value) {
+ const detectAreaType = getDetectAreaTypeKey()
+ if (detectAreaType) {
+ activeAreaType.value = detectAreaType
}
+ }
+ if (currentShapeType.value === 'buffer') {
+ activeAreaType.value = bufferAreaTypeValue.split(',')
+ }
+ if (!suppressTypePanelOnce.value) {
+ showTypePanelAtCurrent()
+ }
+ suppressTypePanelOnce.value = false
+
+ if (!activeShapeId.value) {
+ activeShapeId.value = `shape_${Date.now()}_${Math.random().toString(16).slice(2, 6)}`
+ }
+ const targetIndex = shapeList.value.findIndex(item => item.id === activeShapeId.value)
+ const displayPoints = ['ellipse', 'buffer'].includes(currentShapeType.value)
+ ? _.cloneDeep(pointList)
+ : null
+ let shapeMeta = null
+ if (currentShapeType.value === 'buffer' && bufferRadii?.length && centerLngLat) {
+ shapeMeta = {
+ bufferRadii,
+ center: { lng: centerLngLat.longitude, lat: centerLngLat.latitude, height: centerLngLat.height ?? 0 },
+ }
+ if (controlPoints.length) {
+ shapeMeta.controlPoints = controlPoints
+ }
+ } else if (currentShapeType.value === 'ellipse') {
+ const centerPoint = resolveLngLatPoint(meta?.center)
+ const majorPoint = resolveLngLatPoint(meta?.majorPoint)
+ const minorPoint = resolveLngLatPoint(meta?.minorPoint)
+ if (centerPoint || majorPoint || minorPoint) {
+ shapeMeta = {
+ center: centerPoint,
+ majorPoint,
+ minorPoint,
+ semiMajor: Number.isFinite(Number(meta?.semiMajor)) ? Number(meta.semiMajor) : null,
+ semiMinor: Number.isFinite(Number(meta?.semiMinor)) ? Number(meta.semiMinor) : null,
+ }
+ if (controlPoints.length) {
+ shapeMeta.controlPoints = controlPoints
+ }
+ }
+ }
+ const shapePayload = {
+ id: activeShapeId.value,
+ areaType: currentShapeType.value === 'buffer' ? bufferAreaTypeValue : activeAreaType.value,
+ drawType: currentShapeType.value,
+ points: ['ellipse', 'buffer'].includes(currentShapeType.value) && controlPoints.length
+ ? controlPoints
+ : _.cloneDeep(pointList),
+ displayPoints,
+ meta: shapeMeta,
+ }
+ if (targetIndex === -1) {
+ shapeList.value.push(shapePayload)
+ } else {
+ shapeList.value.splice(targetIndex, 1, shapePayload)
+ }
+ renderShapeList()
+ if (shouldContinueDraw && !readonly.value) {
+ activeShapeId.value = null
+ pointList = []
+ clearActiveTool()
+ startDraw(currentShapeType.value || 'polygon')
+ }
+}
+
+function getDetectAreaTypeKey () {
+ 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 () {
+ if (readonly.value) return
+ showTypePanel.value = true
+}
+
+function hideTypePanel () {
+ showTypePanel.value = false
+}
+
+function handleTypePanelClose () {
+ hideTypePanel()
+ if (readonly.value) return
+ if (activeToolMode.value !== 'edit') return
+ const positions = activeTool?.getPositions?.()
+ if (Array.isArray(positions) && positions.length >= 3) {
+ suppressTypePanelOnce.value = true
+ drawFinished(positions, { continueDraw: true })
+ return
+ }
+ if (positions?.positions?.length >= 3) {
+ suppressTypePanelOnce.value = true
+ drawFinished(positions, { continueDraw: true })
+ return
+ }
+ clearActiveTool()
+}
+
+function handleAreaTypeChange (value) {
+ updateActiveShapeAreaType(value)
+}
+
+function updateActiveShapeAreaType (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
+ const target = shapeList.value[targetIndex]
+ shapeList.value.splice(targetIndex, 1, {
+ ...target,
+ areaType: value,
+ })
+ renderShapeList()
+}
+
+// Tool lifecycle
+function clearActiveTool () {
+ activeTool?.destroy?.()
+ activeTool = null
+ drawManager?.destroy()
+ editManager?.destroy()
+ activeToolMode.value = ''
+ renderShapeList()
+}
+
+function startDraw (type) {
+ currentShapeType.value = type
+ activeToolMode.value = 'draw'
+ drawManager ||= new DrawManager(viewer)
+ activeTool = drawManager.start(type, { style: getAreaTypeStyle(activeAreaType.value) })
+ activeTool?.subscribe('getPolygonPositions', drawFinished)
+}
+
+function startEdit (type, points) {
+ currentShapeType.value = type
+ activeToolMode.value = 'edit'
+ editManager ||= new EditManager(viewer)
+ activeTool = editManager.start(type, points, { style: getAreaTypeStyle(activeAreaType.value) })
+ activeTool?.subscribe('getPolygonPositions', drawFinished)
+}
+
+function handleDraw (type) {
+ if (readonly.value) return
+ if (!viewer) return
+ clearActiveTool()
+ viewEntity && viewer?.entities?.remove(viewEntity)
+ viewEntity = null
+ hideTypePanel()
+ renderShapeList()
+ pointList = []
+ const defaultAreaType = getDetectAreaTypeKey()
+ if (type === 'buffer') {
+ activeAreaType.value = bufferAreaTypeValue
+ } else if (defaultAreaType) {
+ activeAreaType.value = defaultAreaType
+ }
+ activeShapeId.value = null
+ startDraw(type)
+}
+
+function handleClearShape () {
+ if (readonly.value) return
+ viewEntity && viewer?.entities?.remove(viewEntity)
+ viewEntity = null
+ pointList = []
+ shapeList.value = []
+ activeShapeId.value = null
+ hideTypePanel()
+ clearShapeDisplay()
+ clearActiveTool()
+ if (viewer && currentShapeType.value) {
+ startDraw(currentShapeType.value)
+ }
+}
+
+function handleShapeEdit (row) {
+ if (!row?.points?.length) return
+ if (activeToolMode.value === 'edit' && activeShapeId.value === row.id) return
+ activeShapeId.value = row.id
+ currentShapeType.value = row.drawType
+ activeAreaType.value = row.drawType === 'buffer' ? bufferAreaTypeValue.split(',') : row.areaType
+ const displayPoints = getShapeDisplayPoints(row)
+ pointList = _.cloneDeep(displayPoints)
+ clearActiveTool()
+ const editPayload = ['buffer', 'ellipse'].includes(row.drawType)
+ ? { points: displayPoints, meta: row.meta }
+ : row.points
+ startEdit(row.drawType, editPayload)
+ showTypePanelAtCurrent()
+ renderShapeList()
+}
+
+function handleShapeDelete (row) {
+ if (!row) return
+ shapeList.value = shapeList.value.filter(item => item.id !== row.id)
+ renderShapeList()
+ if (activeShapeId.value === row.id) {
+ activeShapeId.value = null
+ clearActiveTool()
+ pointList = []
+ 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()
- const mian = viewer.entities?.add({
+ viewEntity = viewer.entities?.add({
customType: 'control_group',
position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
polyline: {
@@ -356,22 +1237,91 @@
material: Cesium.Color.RED,
},
})
- viewer.flyTo(mian, { duration: 0 })
+ viewer.flyTo(viewEntity, { duration: 0 })
}
// 获取派出所列表
-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 })
+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)
}
}
@@ -380,47 +1330,129 @@
selectedDeviceRows.value = rows
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()
- await getDeviceList()
+ clearShapeDisplay()
+ clearDeviceRangePrimitives()
+ clearDeviceIconEntities()
+ viewEntity && viewer?.entities?.remove(viewEntity)
+ viewEntity = null
+ clearActiveTool()
if (dialogMode.value === 'add') {
- addPolygon()
+ mapRef.value?.zoomToAdminBoundary?.()
+ handleDraw('polygon')
} else if (dialogMode.value === 'edit') {
await loadDetail()
- editPolygon()
+ 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()
- getDeviceList()
+onBeforeUnmount(() => {
+ if (deviceListTimer) {
+ clearTimeout(deviceListTimer)
+ deviceListTimer = null
+ }
+ if (policeStationSearchTimer.value) {
+ clearTimeout(policeStationSearchTimer.value)
+ policeStationSearchTimer.value = null
+ }
+ hideTypePanel()
})
defineExpose({
@@ -428,4 +1460,155 @@
})
</script>
-<style scoped lang="scss"></style>
+<style scoped lang="scss">
+.left-container {
+ position: relative;
+}
+
+.shape-type-panel {
+ position: absolute;
+ right: 16px;
+ bottom: 16px;
+ z-index: 6;
+ min-width: 260px;
+ background: rgba(22, 33, 44, 0.9);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 8px;
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(6px);
+ color: #e5e7f0;
+}
+
+.panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.panel-close {
+ cursor: pointer;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.panel-body {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 12px 12px;
+}
+
+.panel-label {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.75);
+ white-space: nowrap;
+}
+
+.map-toolbar {
+ position: absolute;
+ top: 64px;
+ right: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ padding: 32px 14px;
+ background: rgba(26, 25, 25, 0.6);
+ border-radius: 8px 8px 8px 8px;
+ backdrop-filter: blur(4px);
+}
+
+.tool-btn {
+ padding: 0 !important;
+ width: 46px;
+ height: 46px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border-radius: 8px;
+ border: none !important;
+ box-shadow: none;
+ cursor: pointer;
+ overflow: hidden;
+ background: transparent !important;
+ box-sizing: border-box;
+}
+
+.tool-btn.active {
+ border: 2px solid #FFFFFF !important;
+}
+
+.tool-icon {
+ position: relative;
+}
+
+.tool-icon-image {
+ width: 100%;
+ height: 100%;
+}
+
+.tool-icon.polygon {
+ border-radius: 4px;
+ transform: rotate(15deg);
+}
+
+.tool-icon.rect {
+ border-radius: 2px;
+}
+
+.tool-icon.ellipse {
+ border-radius: 50%;
+}
+
+.tool-icon.buffer {
+ border-radius: 50%;
+ box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.5);
+}
+
+.tool-icon.trash {
+ border: none;
+ box-shadow: none;
+}
+
+.tool-icon.trash::before,
+.tool-icon.trash::after {
+ content: "";
+ position: absolute;
+ left: 4px;
+ right: 4px;
+ background: #ffffff;
+}
+
+.tool-icon.trash::before {
+ top: 4px;
+ height: 10px;
+ border-radius: 2px;
+}
+
+.tool-icon.trash::after {
+ top: 1px;
+ height: 3px;
+ border-radius: 2px;
+}
+
+.shape-table-container {
+ margin-bottom: 20px;
+
+ .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