<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" :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>
|
</div>
|
|
<div class="content" v-if="readonly">
|
<div class="detail-title">区域详情</div>
|
<el-row>
|
<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">
|
{{ getDictLabel(formData.triggerCondition, dictObj.triggeringCondition) }}
|
</div>
|
</el-col>
|
<el-col :span="24">
|
<div class="label">响应机制</div>
|
<div class="val">{{ formData.responseMechanism }}</div>
|
</el-col>
|
<el-col :span="24">
|
<div class="label">管控级别</div>
|
<div class="val">{{ getDictLabel(formData.controlLevel, dictObj.controlLevel) }}</div>
|
</el-col>
|
<el-col :span="24">
|
<div class="label">关联派出所</div>
|
<div class="val">{{ formData.policeStationName }}</div>
|
</el-col>
|
<el-col :span="24">
|
<div class="label">可飞行时间段</div>
|
<div class="val">{{ formatFlyDate(formData) }}</div>
|
</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-column prop="deviceName" label="设备名称" />
|
<el-table-column prop="deviceType" label="类型">
|
<template v-slot="{ row }">
|
{{ getDictLabel(row.deviceType, dictObj.deviceType) }}
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</div>
|
</div>
|
|
<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="responseMechanism">
|
<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"
|
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"
|
v-model="formData.controlLevel" placeholder="请选择" clearable>
|
<el-option v-for="item in dictObj.controlLevel" :key="item.dictKey" :label="item.dictValue"
|
:value="item.dictKey" />
|
</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"
|
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 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">
|
<div class="label">
|
关联设备
|
</div>
|
|
<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">
|
<el-table-column type="selection" width="55" :reserve-selection="true" />
|
<el-table-column prop="deviceName" label="设备名称" />
|
<el-table-column prop="deviceType" label="类型">
|
<template v-slot="{ row }">
|
{{ getDictLabel(row.deviceType, dictObj.deviceType) }}
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-form-item>
|
</div>
|
</el-form>
|
|
<div class="footer">
|
<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">
|
确定
|
</el-button>
|
</div>
|
</div>
|
</div>
|
</el-dialog>
|
</template>
|
|
<script setup>
|
import { Close } from '@element-plus/icons-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,
|
EditManager,
|
addBufferRadiusLabelEntity,
|
buildEllipsePositions,
|
} from '@/utils/cesium/shapeTools'
|
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
|
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 { 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: '', // 区域名称
|
controlLevel: '', // 管控级别
|
deviceIds: '', // 关联设备ID
|
flyDateEnd: '', // 可飞行结束
|
flyDateStart: '', // 可飞行开始
|
policeStationId: '', // 关联派出所id
|
responseMechanism: '', // 响应机制
|
triggerCondition: '', // 触发条件
|
})
|
|
const emit = defineEmits(['success'])
|
const formRef = ref(null) // 表单实例
|
const formData = ref(initForm()) // 表单数据
|
const visible = defineModel() // 弹框显隐
|
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 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),
|
responseMechanism: fieldRules(true, 200),
|
triggerCondition: fieldRules(true, 200),
|
controlLevel: fieldRules(true),
|
flyDateStart: fieldRules(true),
|
policeStationId: 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)
|
: [],
|
}
|
}
|
|
// 关闭弹框
|
function handleCancel () {
|
visible.value = false
|
}
|
|
// 提交新增/编辑
|
async function handleSubmit () {
|
if (!shapeList.value.length) {
|
ElMessage.warning('区域不可为空,请先绘制区域')
|
return
|
}
|
|
const isValid = await formRef.value?.validate().catch(() => false)
|
if (!isValid) return
|
submitting.value = true
|
try {
|
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')
|
} finally {
|
submitting.value = false
|
}
|
}
|
|
watch(
|
() => flyDateRange.value,
|
() => {
|
formData.value.flyDateStart = flyDateRange.value[0] || ''
|
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 })
|
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)
|
syncShapeListFromDetail(detail)
|
}
|
|
// 格式化可飞行时间段
|
function formatFlyDate (row) {
|
if (!row?.flyDateStart && !row?.flyDateEnd) return ''
|
return `${row.flyDateStart || '-'} ~ ${row.flyDateEnd || '-'}`
|
}
|
|
// 初始化地图实例
|
function initMap () {
|
if (viewer) return
|
const map = mapRef.value?.getMap()
|
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, 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 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()
|
viewEntity = viewer.entities?.add({
|
customType: 'control_group',
|
position: Cesium.Cartesian3.fromDegrees(result[0], result[1]),
|
polyline: {
|
positions: Cesium.Cartesian3.fromDegreesArray(result),
|
clampToGround: true,
|
width: 3,
|
material: Cesium.Color.RED,
|
},
|
})
|
viewer.flyTo(viewEntity, { duration: 0 })
|
}
|
|
// 获取派出所列表
|
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)
|
}
|
|
// 获取设备列表
|
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' && 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)
|
}
|
}
|
|
// 关联设备变更
|
function handleDeviceSelectionChange (rows) {
|
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 (isDialogInitializing.value && !deviceOptions.value.length) return
|
const rows = []
|
const arr = formData.value.deviceIds ? formData.value.deviceIds.split(',') : []
|
deviceOptions.value.forEach(row => {
|
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()
|
if (dialogMode.value === 'add') {
|
mapRef.value?.zoomToAdminBoundary?.()
|
handleDraw('polygon')
|
} else if (dialogMode.value === 'edit') {
|
await loadDetail()
|
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()
|
}
|
|
onBeforeUnmount(() => {
|
if (deviceListTimer) {
|
clearTimeout(deviceListTimer)
|
deviceListTimer = null
|
}
|
if (policeStationSearchTimer.value) {
|
clearTimeout(policeStationSearchTimer.value)
|
policeStationSearchTimer.value = null
|
}
|
hideTypePanel()
|
})
|
|
defineExpose({
|
open,
|
})
|
</script>
|
|
<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>
|