<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" />
|
<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" 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" 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" 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" 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>
|
<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, onMounted, 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, buildEllipsePositions } from '@/utils/cesium/shapeTools'
|
import { cartesian3Convert } from '@/utils/cesium/mapUtil'
|
import * as Cesium from 'cesium'
|
import { fwPoliceStationListApi } from '@/views/areaManage/precinctInfo/precinctInfoApi'
|
import { fwDeviceListApi } 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'
|
|
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('add') // 弹框模式
|
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 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)
|
const areaTypeStyleMap = {
|
'1': {
|
fill: Cesium.Color.fromBytes(25, 178, 230, 128),
|
outline: Cesium.Color.fromBytes(0, 251, 255, 255),
|
},
|
'2': {
|
fill: Cesium.Color.fromBytes(255, 235, 59, 128),
|
outline: Cesium.Color.fromBytes(255, 235, 59, 255),
|
},
|
'3': {
|
fill: Cesium.Color.fromBytes(247, 20, 20, 128),
|
outline: Cesium.Color.fromBytes(255, 0, 0, 255),
|
},
|
}
|
let viewer
|
let drawManager
|
let editManager
|
let activeTool
|
let pointList = []
|
let viewEntity
|
let shapeDisplaySource
|
const deviceRangePrimitiveMap = new Map()
|
let deviceRangeMaterialRegistered = false
|
const activeToolMode = ref('')
|
|
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) {
|
viewEntity && viewer?.entities?.remove(viewEntity)
|
viewEntity = null
|
clearActiveTool()
|
hideTypePanel()
|
clearShapeDisplay()
|
clearDeviceRangePrimitives()
|
}
|
}
|
)
|
watch(
|
() => activeAreaType.value,
|
val => {
|
updateActiveShapeAreaType(val)
|
}
|
)
|
|
// 加载详情
|
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 registerDeviceRangeMaterial () {
|
if (deviceRangeMaterialRegistered || !Cesium?.Material) return
|
deviceRangeMaterialRegistered = true
|
Cesium.Material._materialCache.addMaterial('DeviceRangeEllipsoid', {
|
fabric: {
|
type: 'DeviceRangeEllipsoid',
|
uniforms: {
|
color: new Cesium.Color(1.0, 0.0, 0.0, 0.7),
|
speed: 3.0,
|
},
|
source: `
|
uniform vec4 color;
|
uniform float speed;
|
czm_material czm_getMaterial(czm_materialInput materialInput){
|
czm_material material = czm_getDefaultMaterial(materialInput);
|
vec2 st = materialInput.st;
|
float time = fract(czm_frameNumber * speed / 1000.0);
|
float alpha = abs(smoothstep(0.5,1.,fract( -st.t - time)));
|
alpha += .1;
|
material.alpha = alpha;
|
material.diffuse = color.rgb;
|
return material;
|
}
|
`,
|
},
|
translucent: () => true,
|
})
|
}
|
|
function createDeviceRangeMaterial () {
|
registerDeviceRangeMaterial()
|
return Cesium.Material.fromType('DeviceRangeEllipsoid', {
|
color: Cesium.Color.fromBytes(0, 255, 255, 155),
|
})
|
}
|
|
function clearDeviceRangePrimitives () {
|
if (!viewer || !deviceRangePrimitiveMap.size) return
|
for (const primitive of deviceRangePrimitiveMap.values()) {
|
viewer.scene.primitives.remove(primitive)
|
}
|
deviceRangePrimitiveMap.clear()
|
}
|
|
function normalizeDeviceRange (value) {
|
const range = Number(value)
|
if (!Number.isFinite(range) || range <= 0) return 2000
|
return range
|
}
|
|
function getDeviceLngLat (device) {
|
const lngRaw = device?.longitude ?? device?.lng ?? device?.lon
|
const latRaw = device?.latitude ?? device?.lat
|
const lng = Number(lngRaw)
|
const lat = Number(latRaw)
|
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null
|
return { lng, lat }
|
}
|
|
function createDeviceRangePrimitive (device) {
|
if (!viewer) return null
|
if (!device) return null
|
const position = getDeviceLngLat(device)
|
if (!position) return null
|
const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm)
|
const center = Cesium.Cartesian3.fromDegrees(position.lng, position.lat, 0)
|
const radii = new Cesium.Cartesian3(rangeMeters, rangeMeters, rangeMeters)
|
const geometry = new Cesium.EllipsoidGeometry({
|
radii,
|
vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED?.vertexFormat
|
|| Cesium.VertexFormat.POSITION_AND_NORMAL,
|
})
|
const instance = new Cesium.GeometryInstance({
|
geometry,
|
modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(center),
|
})
|
return new Cesium.Primitive({
|
geometryInstances: instance,
|
appearance: new Cesium.MaterialAppearance({
|
material: createDeviceRangeMaterial(),
|
closed: true,
|
translucent: true,
|
flat: true,
|
faceForward: true,
|
}),
|
asynchronous: false
|
})
|
}
|
|
function renderDeviceRangeSingle (device) {
|
if (!viewer) return
|
clearDeviceRangePrimitives()
|
const primitive = createDeviceRangePrimitive(device)
|
if (!primitive) return
|
deviceRangePrimitiveMap.set(String(device?.id ?? Date.now()), primitive)
|
viewer.scene.primitives.add(primitive)
|
}
|
|
function renderDeviceRanges (rows) {
|
if (!viewer) return
|
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)
|
}
|
}
|
selectedRows.forEach(row => {
|
const id = String(row?.id)
|
if (!id || deviceRangePrimitiveMap.has(id)) return
|
const primitive = createDeviceRangePrimitive(row)
|
if (!primitive) return
|
deviceRangePrimitiveMap.set(id, primitive)
|
viewer.scene.primitives.add(primitive)
|
})
|
}
|
|
function clearShapeDisplay () {
|
if (!shapeDisplaySource) return
|
shapeDisplaySource.entities.removeAll()
|
}
|
|
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 areaTypeStyleMap?.[String(areaType)] || {
|
fill: Cesium.Color.fromBytes(45, 140, 240, 99),
|
outline: Cesium.Color.fromBytes(45, 140, 240, 255),
|
}
|
}
|
|
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) {
|
const styles = [
|
{
|
fill: Cesium.Color.fromBytes(247, 20, 20, 128),
|
outline: Cesium.Color.fromBytes(255, 0, 0, 255),
|
},
|
{
|
fill: Cesium.Color.fromBytes(255, 235, 59, 128),
|
outline: Cesium.Color.fromBytes(255, 235, 59, 255),
|
},
|
{
|
fill: Cesium.Color.fromBytes(25, 178, 230, 128),
|
outline: Cesium.Color.fromBytes(0, 251, 255, 255),
|
},
|
]
|
radii.forEach((radius, index) => {
|
const style = styles[index] || styles[styles.length - 1]
|
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,
|
},
|
})
|
})
|
}
|
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,
|
},
|
})
|
})
|
}
|
|
// 绘制完成回调
|
const drawFinished = async data => {
|
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()
|
}
|
|
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)
|
}
|
if (positions?.positions?.length >= 3) {
|
suppressTypePanelOnce.value = true
|
drawFinished(positions)
|
}
|
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
|
clearActiveTool()
|
viewEntity && viewer?.entities?.remove(viewEntity)
|
viewEntity = null
|
pointList = []
|
shapeList.value = []
|
activeShapeId.value = null
|
activeAreaType.value = ''
|
hideTypePanel()
|
clearShapeDisplay()
|
}
|
|
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()
|
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 () {
|
const res = await fwPoliceStationListApi()
|
policeStationOptions.value = res?.data?.data ?? []
|
}
|
|
// 获取设备列表
|
async function getDeviceList () {
|
if (deviceOptions.value.length) return
|
const res = await fwDeviceListApi({ isAreaSelect: 1, areaId: formData.value.id, isTrack: 1 })
|
deviceOptions.value = res?.data?.data ?? []
|
if (dialogMode.value === 'view') {
|
deviceOptions.value = deviceOptions.value.filter(item => formData.value.deviceIds.includes(item.id))
|
}
|
}
|
|
// 关联设备变更
|
function handleDeviceSelectionChange (rows) {
|
selectedDeviceRows.value = rows
|
const ids = rows.map(item => item.id)
|
formData.value.deviceIds = ids.join(',')
|
renderDeviceRanges(rows)
|
}
|
|
function handleDeviceRowClick (row) {
|
if (readonly.value) {
|
renderDeviceRangeSingle(row)
|
return
|
}
|
renderDeviceRanges(selectedDeviceRows.value)
|
}
|
|
// 同步选择状态
|
function syncDeviceSelection () {
|
const rows = []
|
const arr = formData.value.deviceIds.split(',')
|
deviceOptions.value.forEach(row => {
|
if (arr.includes(row.id)) {
|
rows.push(row)
|
}
|
})
|
selectedDeviceRows.value = rows
|
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 } = {}) {
|
dialogMode.value = mode || 'add'
|
formData.value = dialogMode.value === 'add' ? initForm() : row
|
selectedDeviceRows.value = []
|
shapeList.value = []
|
activeShapeId.value = null
|
activeAreaType.value = ''
|
pointList = []
|
await nextTick()
|
initMap()
|
clearShapeDisplay()
|
clearDeviceRangePrimitives()
|
viewEntity && viewer?.entities?.remove(viewEntity)
|
viewEntity = null
|
clearActiveTool()
|
await getDeviceList()
|
if (dialogMode.value === 'add') {
|
// default no draw mode
|
} else if (dialogMode.value === 'edit') {
|
await loadDetail()
|
viewPolygon()
|
} else {
|
await loadDetail()
|
viewPolygon()
|
}
|
await nextTick()
|
syncDeviceSelection()
|
if (!readonly.value) {
|
renderDeviceRanges(selectedDeviceRows.value)
|
}
|
}
|
|
onMounted(() => {
|
getPoliceStationList()
|
})
|
|
onBeforeUnmount(() => {
|
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: 10px;
|
padding: 10px 8px;
|
background: rgba(19, 28, 36, 0.75);
|
backdrop-filter: blur(4px);
|
border-radius: 8px;
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2);
|
}
|
|
.tool-btn {
|
padding: 0;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
gap: 6px;
|
background: rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
border-radius: 8px;
|
color: #ffffff;
|
font-size: 12px;
|
line-height: 1;
|
cursor: pointer;
|
transition: all 0.2s ease;
|
overflow: hidden;
|
}
|
|
.tool-btn:hover {
|
background: rgba(255, 255, 255, 0.16);
|
border-color: rgba(255, 255, 255, 0.35);
|
transform: translateY(-1px);
|
}
|
|
.tool-btn:active {
|
transform: translateY(0);
|
}
|
|
.tool-btn.danger {
|
border-color: rgba(255, 112, 112, 0.6);
|
color: #ffb3b3;
|
}
|
|
.tool-icon {
|
position: relative;
|
}
|
|
.tool-icon-image {
|
width: 36px;
|
height: 36px;
|
}
|
|
.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>
|