| | |
| | | <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" /> |
| | | :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> |
| | |
| | | <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"> |
| | | 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 class="map-toolbar"> |
| | | <el-tooltip content="多边形" placement="left"> |
| | | <button class="tool-btn" type="button" @click="handleDraw('polygon')"> |
| | | <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" type="button" @click="handleDraw('rectangle')"> |
| | | <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" type="button" @click="handleDraw('ellipse')"> |
| | | <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" type="button" @click="handleDraw('buffer')"> |
| | | <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-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> |
| | |
| | | <script setup> |
| | | import { Close } from '@element-plus/icons-vue' |
| | | |
| | | import { computed, inject, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' |
| | | import { computed, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { fwAreaDivideDetailApi, fwAreaDivideSubmitApi } from './partitionApi' |
| | | import { saveOperationLog } from '@ztzf/apis' |
| | | import { useRoute } from 'vue-router' |
| | | import { fieldRules, geomAnalysis, getDictLabel } from '@ztzf/utils' |
| | | import CommonCesiumMap from '@/components/map-container/common-cesium-map.vue' |
| | | import { DrawManager, EditManager, buildEllipsePositions } from '@/utils/cesium/shapeTools' |
| | | 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 { 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: '', // 区域名称 |
| | |
| | | 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 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) |
| | |
| | | let viewEntity |
| | | let shapeDisplaySource |
| | | const deviceRangePrimitiveMap = new Map() |
| | | let deviceRangeMaterialRegistered = false |
| | | const deviceIconEntityMap = new Map() |
| | | const activeToolMode = ref('') |
| | | const suppressDeviceFly = ref(false) |
| | | const isDialogInitializing = ref(false) |
| | | let lastSelectedDeviceIds = [] |
| | | let deviceRangeRenderToken = 0 |
| | | let deviceListTimer = null |
| | | let deviceListRequestToken = 0 |
| | | |
| | | const rules = { |
| | | areaName: fieldRules(true, 50), |
| | |
| | | () => 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() |
| | | } |
| | | } |
| | | ) |
| | |
| | | val => { |
| | | updateActiveShapeAreaType(val) |
| | | } |
| | | ) |
| | | watch( |
| | | () => shapeList.value, |
| | | () => { |
| | | if (isDialogInitializing.value) return |
| | | queueDeviceListRefresh() |
| | | }, |
| | | { deep: true } |
| | | ) |
| | | |
| | | // 加载详情 |
| | |
| | | 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()) { |
| | |
| | | deviceRangePrimitiveMap.clear() |
| | | } |
| | | |
| | | function normalizeDeviceRange (value) { |
| | | const range = Number(value) |
| | | if (!Number.isFinite(range) || range <= 0) return 2000 |
| | | return range |
| | | function clearDeviceIconEntities () { |
| | | if (!viewer || !deviceIconEntityMap.size) return |
| | | for (const entity of deviceIconEntityMap.values()) { |
| | | viewer.entities.remove(entity) |
| | | } |
| | | deviceIconEntityMap.clear() |
| | | } |
| | | |
| | | function getDeviceLngLat (device) { |
| | | const lngRaw = device?.longitude ?? device?.lng ?? device?.lon |
| | | const latRaw = device?.latitude ?? device?.lat |
| | | const lng = Number(lngRaw) |
| | | const lat = Number(latRaw) |
| | | if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null |
| | | return { lng, lat } |
| | | function clearDeviceListState () { |
| | | deviceOptions.value = [] |
| | | selectedDeviceRows.value = [] |
| | | formData.value.deviceIds = '' |
| | | clearDeviceRangePrimitives() |
| | | clearDeviceIconEntities() |
| | | if (deviceTableRef.value) { |
| | | deviceTableRef.value.clearSelection() |
| | | } |
| | | } |
| | | |
| | | function createDeviceRangePrimitive (device) { |
| | | function formatWktNumber (value) { |
| | | if (!Number.isFinite(Number(value))) return null |
| | | return Number(value).toFixed(6) |
| | | } |
| | | |
| | | function isSameWktPoint (left, right) { |
| | | if (!left || !right) return false |
| | | return Math.abs(left.lat - right.lat) < 0.000001 && Math.abs(left.lng - right.lng) < 0.000001 |
| | | } |
| | | |
| | | function buildPolygonWktFromPoints (points) { |
| | | if (!Array.isArray(points) || points.length < 3) return null |
| | | const ring = points |
| | | .map(resolveLngLatPoint) |
| | | .filter(Boolean) |
| | | .map(point => ({ lng: point.lng, lat: point.lat })) |
| | | if (ring.length < 3) return null |
| | | const first = ring[0] |
| | | const last = ring[ring.length - 1] |
| | | if (!isSameWktPoint(first, last)) { |
| | | ring.push({ ...first }) |
| | | } |
| | | const coordText = ring |
| | | .map(point => { |
| | | const lng = formatWktNumber(point.lng) |
| | | const lat = formatWktNumber(point.lat) |
| | | if (lat === null || lng === null) return null |
| | | return `${lng} ${lat}` |
| | | }) |
| | | .filter(Boolean) |
| | | .join(', ') |
| | | if (!coordText) return null |
| | | return `POLYGON((${coordText}))` |
| | | } |
| | | |
| | | function buildEllipseWktFromMeta (center, semiMajor, semiMinor) { |
| | | if (!viewer) return null |
| | | if (!device) return null |
| | | const position = getDeviceLngLat(device) |
| | | if (!position) return null |
| | | const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm) |
| | | const center = Cesium.Cartesian3.fromDegrees(position.lng, position.lat, 0) |
| | | const radii = new Cesium.Cartesian3(rangeMeters, rangeMeters, rangeMeters) |
| | | const geometry = new Cesium.EllipsoidGeometry({ |
| | | radii, |
| | | vertexFormat: Cesium.MaterialAppearance.MaterialSupport.TEXTURED?.vertexFormat |
| | | || Cesium.VertexFormat.POSITION_AND_NORMAL, |
| | | }) |
| | | const instance = new Cesium.GeometryInstance({ |
| | | geometry, |
| | | modelMatrix: Cesium.Transforms.eastNorthUpToFixedFrame(center), |
| | | }) |
| | | return new Cesium.Primitive({ |
| | | geometryInstances: instance, |
| | | appearance: new Cesium.MaterialAppearance({ |
| | | material: createDeviceRangeMaterial(), |
| | | closed: true, |
| | | translucent: true, |
| | | flat: true, |
| | | faceForward: true, |
| | | }), |
| | | asynchronous: false |
| | | }) |
| | | const resolvedCenter = resolveLngLatPoint(center) |
| | | if (!resolvedCenter) return null |
| | | if (!Number.isFinite(Number(semiMajor)) || !Number.isFinite(Number(semiMinor))) return null |
| | | const centerCartesian = Cesium.Cartesian3.fromDegrees( |
| | | resolvedCenter.lng, |
| | | resolvedCenter.lat, |
| | | resolvedCenter.height || 0 |
| | | ) |
| | | const positions = buildEllipsePositions(centerCartesian, Number(semiMajor), Number(semiMinor)) |
| | | if (!positions.length) return null |
| | | const points = positions |
| | | .map(item => { |
| | | const converted = cartesian3Convert(item, viewer) |
| | | return { lng: converted.longitude, lat: converted.latitude } |
| | | }) |
| | | .filter(Boolean) |
| | | return buildPolygonWktFromPoints(points) |
| | | } |
| | | |
| | | function renderDeviceRangeSingle (device) { |
| | | function resolveShapePolygonsWkt () { |
| | | const polygons = [] |
| | | shapeList.value.forEach(shape => { |
| | | if (!shape) return |
| | | if (shape.drawType === 'buffer') { |
| | | const radii = Array.isArray(shape?.meta?.bufferRadii) |
| | | ? shape.meta.bufferRadii.filter(item => Number.isFinite(item) && item > 0) |
| | | : [] |
| | | const maxRadius = radii.length ? Math.max(...radii) : null |
| | | const wkt = buildEllipseWktFromMeta(shape?.meta?.center, maxRadius, maxRadius) |
| | | if (wkt) polygons.push(wkt) |
| | | return |
| | | } |
| | | if (shape.drawType === 'ellipse') { |
| | | const wkt = buildEllipseWktFromMeta(shape?.meta?.center, shape?.meta?.semiMajor, shape?.meta?.semiMinor) |
| | | if (wkt) { |
| | | polygons.push(wkt) |
| | | return |
| | | } |
| | | } |
| | | const points = getShapeDisplayPoints(shape) |
| | | const wkt = buildPolygonWktFromPoints(points) |
| | | if (wkt) polygons.push(wkt) |
| | | }) |
| | | return polygons |
| | | } |
| | | |
| | | async function renderDeviceRangeSingle (device) { |
| | | if (!viewer) return |
| | | clearDeviceRangePrimitives() |
| | | const primitive = createDeviceRangePrimitive(device) |
| | | clearDeviceIconEntities() |
| | | const position = getDeviceLngLat(device) |
| | | if (!position) return |
| | | addDeviceIconEntity(device, position) |
| | | const rangeMeters = normalizeDeviceRange(device.effectiveRangeKm) |
| | | const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters) |
| | | if (!primitive) return |
| | | deviceRangePrimitiveMap.set(String(device?.id ?? Date.now()), primitive) |
| | | viewer.scene.primitives.add(primitive) |
| | | } |
| | | |
| | | function renderDeviceRanges (rows) { |
| | | async function renderDeviceRanges (rows) { |
| | | if (!viewer) return |
| | | deviceRangeRenderToken += 1 |
| | | const renderToken = deviceRangeRenderToken |
| | | const selectedRows = Array.isArray(rows) ? rows : [] |
| | | const nextIds = new Set(selectedRows.map(row => String(row?.id))) |
| | | for (const [id, primitive] of deviceRangePrimitiveMap.entries()) { |
| | |
| | | deviceRangePrimitiveMap.delete(id) |
| | | } |
| | | } |
| | | selectedRows.forEach(row => { |
| | | for (const [id, entity] of deviceIconEntityMap.entries()) { |
| | | if (!nextIds.has(id)) { |
| | | viewer.entities.remove(entity) |
| | | deviceIconEntityMap.delete(id) |
| | | } |
| | | } |
| | | const pendingRows = selectedRows.filter(row => { |
| | | const id = String(row?.id) |
| | | if (!id || deviceRangePrimitiveMap.has(id)) return |
| | | const primitive = createDeviceRangePrimitive(row) |
| | | return id && !deviceRangePrimitiveMap.has(id) |
| | | }) |
| | | if (!pendingRows.length) return |
| | | const primitives = await Promise.all( |
| | | pendingRows.map(async row => { |
| | | const position = getDeviceLngLat(row) |
| | | if (!position) return { row, primitive: null, position: null } |
| | | const rangeMeters = normalizeDeviceRange(row.effectiveRangeKm) |
| | | const primitive = await createDeviceRangePrimitive(viewer, position, rangeMeters) |
| | | return { row, primitive, position } |
| | | }) |
| | | ) |
| | | if (renderToken !== deviceRangeRenderToken) return |
| | | primitives.forEach(({ row, primitive, position }) => { |
| | | if (position) { |
| | | addDeviceIconEntity(row, position) |
| | | } |
| | | if (!primitive) return |
| | | const id = String(row?.id) |
| | | if (!nextIds.has(id)) return |
| | | deviceRangePrimitiveMap.set(id, primitive) |
| | | viewer.scene.primitives.add(primitive) |
| | | }) |
| | | } |
| | | |
| | | async function flyToDevice (device) { |
| | | if (!viewer) return |
| | | const position = getDeviceLngLat(device) |
| | | if (!position) return |
| | | const height = await resolveDeviceTerrainHeight(viewer, position) |
| | | const rangeMeters = normalizeDeviceRange(device?.effectiveRangeKm) |
| | | const destination = Cesium.Cartesian3.fromDegrees(position.lng, position.lat, height + rangeMeters * 3) |
| | | viewer.camera.flyTo({ |
| | | destination, |
| | | orientation: { |
| | | heading: viewer.camera.heading, |
| | | pitch: Cesium.Math.toRadians(-90), |
| | | roll: 0, |
| | | }, |
| | | duration: 0.6, |
| | | }) |
| | | } |
| | | |
| | | function clearShapeDisplay () { |
| | | if (!shapeDisplaySource) return |
| | | shapeDisplaySource.entities.removeAll() |
| | | } |
| | | |
| | | function addDeviceIconEntity (device, position) { |
| | | if (!viewer || !position) return |
| | | const id = String(device?.id ?? '') |
| | | if (!id || deviceIconEntityMap.has(id)) return |
| | | const entity = viewer.entities.add({ |
| | | position: Cesium.Cartesian3.fromDegrees(position.lng, position.lat, position.height || 0), |
| | | billboard: { |
| | | image: equipmentIcon, |
| | | width: 28, |
| | | height: 38, |
| | | verticalOrigin: Cesium.VerticalOrigin.BOTTOM, |
| | | heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, |
| | | }, |
| | | }) |
| | | deviceIconEntityMap.set(id, entity) |
| | | } |
| | | |
| | | function normalizeShapePoint (point) { |
| | |
| | | material: style.outline, |
| | | }, |
| | | }) |
| | | addBufferRadiusLabelEntity(dataSource, centerCartesian, radius, { |
| | | name: 'shape-display', |
| | | customData: { drawType: 'buffer', label: 'radius' }, |
| | | }) |
| | | }) |
| | | } |
| | | return |
| | |
| | | }) |
| | | } |
| | | |
| | | function pushRadiusBoundingPoints (positions, center, radius) { |
| | | if (!center || !Number.isFinite(Number(radius)) || radius <= 0) return |
| | | const resolved = resolveLngLatPoint(center) || {} |
| | | const lng = Number(resolved.lng ?? resolved.longitude) |
| | | const lat = Number(resolved.lat ?? resolved.latitude) |
| | | if (!Number.isFinite(lng) || !Number.isFinite(lat)) return |
| | | const baseLat = Cesium.Math.toRadians(lat) |
| | | const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius |
| | | const deltaLat = radius / earthRadius |
| | | const deltaLng = radius / (earthRadius * Math.max(Math.cos(baseLat), 0.000001)) |
| | | const points = [ |
| | | { lng: Cesium.Math.toDegrees(Cesium.Math.toRadians(lng) + deltaLng), lat }, |
| | | { lng: Cesium.Math.toDegrees(Cesium.Math.toRadians(lng) - deltaLng), lat }, |
| | | { lng, lat: Cesium.Math.toDegrees(baseLat + deltaLat) }, |
| | | { lng, lat: Cesium.Math.toDegrees(baseLat - deltaLat) }, |
| | | ] |
| | | points.forEach(point => { |
| | | positions.push(Cesium.Cartesian3.fromDegrees(point.lng, point.lat, resolved.height || 0)) |
| | | }) |
| | | } |
| | | |
| | | function collectShapeBoundingPositions () { |
| | | const positions = [] |
| | | if (!viewer || !shapeList.value.length) return positions |
| | | shapeList.value.forEach(shape => { |
| | | let points = getShapeDisplayPoints(shape) |
| | | if (!Array.isArray(points) || !points.length) { |
| | | points = resolveControlPoints(shape?.meta, shape?.drawType) |
| | | } |
| | | if (Array.isArray(points) && points.length) { |
| | | points.forEach(point => { |
| | | const resolved = resolveLngLatPoint(point) |
| | | const normalized = normalizeShapePoint(resolved) |
| | | if (!normalized) return |
| | | positions.push(Cesium.Cartesian3.fromDegrees(normalized.lng, normalized.lat, normalized.height)) |
| | | }) |
| | | } |
| | | if (shape?.drawType === 'buffer' && shape?.meta?.center && Array.isArray(shape?.meta?.bufferRadii)) { |
| | | const maxRadius = Math.max(...shape.meta.bufferRadii.filter(item => Number.isFinite(item))) |
| | | pushRadiusBoundingPoints(positions, shape.meta.center, maxRadius) |
| | | } |
| | | if (shape?.drawType === 'ellipse' && shape?.meta?.center) { |
| | | const major = Number(shape?.meta?.semiMajor) |
| | | const minor = Number(shape?.meta?.semiMinor) |
| | | const radius = Number.isFinite(major) && Number.isFinite(minor) ? Math.max(major, minor) : null |
| | | pushRadiusBoundingPoints(positions, shape.meta.center, radius) |
| | | } |
| | | }) |
| | | return positions |
| | | } |
| | | |
| | | function fitViewToShapeList () { |
| | | if (!viewer || !shapeList.value.length) return |
| | | const positions = collectShapeBoundingPositions() |
| | | if (positions.length < 2) return |
| | | const boundingSphere = Cesium.BoundingSphere.fromPoints(positions) |
| | | const range = Math.max(boundingSphere.radius * 2.4, 200) |
| | | viewer.camera.flyToBoundingSphere(boundingSphere, { |
| | | duration: 0, |
| | | offset: new Cesium.HeadingPitchRange(viewer.camera.heading, Cesium.Math.toRadians(-90), range), |
| | | }) |
| | | } |
| | | |
| | | // 绘制完成回调 |
| | | const drawFinished = async data => { |
| | | const drawFinished = async (data, options = {}) => { |
| | | const shouldContinueDraw = options?.continueDraw || activeToolMode.value === 'draw' |
| | | const positions = Array.isArray(data?.positions) ? data.positions : data |
| | | if (!Array.isArray(positions) || positions.length < 3) { |
| | | pointList = [] |
| | |
| | | shapeList.value.splice(targetIndex, 1, shapePayload) |
| | | } |
| | | renderShapeList() |
| | | if (shouldContinueDraw && !readonly.value) { |
| | | activeShapeId.value = null |
| | | pointList = [] |
| | | clearActiveTool() |
| | | startDraw(currentShapeType.value || 'polygon') |
| | | } |
| | | } |
| | | |
| | | function getDetectAreaTypeKey () { |
| | |
| | | const positions = activeTool?.getPositions?.() |
| | | if (Array.isArray(positions) && positions.length >= 3) { |
| | | suppressTypePanelOnce.value = true |
| | | drawFinished(positions) |
| | | drawFinished(positions, { continueDraw: true }) |
| | | return |
| | | } |
| | | if (positions?.positions?.length >= 3) { |
| | | suppressTypePanelOnce.value = true |
| | | drawFinished(positions) |
| | | drawFinished(positions, { continueDraw: true }) |
| | | return |
| | | } |
| | | clearActiveTool() |
| | | } |
| | |
| | | |
| | | function handleClearShape () { |
| | | if (readonly.value) return |
| | | clearActiveTool() |
| | | viewEntity && viewer?.entities?.remove(viewEntity) |
| | | viewEntity = null |
| | | pointList = [] |
| | | shapeList.value = [] |
| | | activeShapeId.value = null |
| | | activeAreaType.value = '' |
| | | hideTypePanel() |
| | | clearShapeDisplay() |
| | | clearActiveTool() |
| | | if (viewer && currentShapeType.value) { |
| | | startDraw(currentShapeType.value) |
| | | } |
| | | } |
| | | |
| | | function handleShapeEdit (row) { |
| | |
| | | if (!viewer) return |
| | | if (shapeList.value.length) { |
| | | renderShapeList() |
| | | fitViewToShapeList() |
| | | return |
| | | } |
| | | if (!formData.value?.geom) return |
| | |
| | | } |
| | | |
| | | // 获取派出所列表 |
| | | async function getPoliceStationList () { |
| | | const res = await fwPoliceStationListApi() |
| | | policeStationOptions.value = res?.data?.data ?? [] |
| | | async function getPoliceStationList (keyword = '') { |
| | | if (!visible.value) return |
| | | policeStationLoading.value = true |
| | | try { |
| | | const res = await fwPoliceStationListApi({ |
| | | stationName: keyword || undefined, |
| | | }) |
| | | const list = res?.data?.data ?? [] |
| | | policeStationOptions.value = list |
| | | if (!keyword) { |
| | | policeStationAllOptions.value = list |
| | | } |
| | | await nextTick() |
| | | if (!keyword) { |
| | | const matched = policeStationOptions.value.find( |
| | | item => String(item.id) === String(formData.value.policeStationId) |
| | | ) |
| | | if (!matched) { |
| | | formData.value.policeStationId = '' |
| | | } |
| | | } |
| | | } finally { |
| | | policeStationLoading.value = false |
| | | } |
| | | } |
| | | |
| | | function handlePoliceStationSearch (query) { |
| | | policeStationSearchKeyword.value = (query || '').trim() |
| | | if (!policeStationSearchKeyword.value) { |
| | | policeStationOptions.value = [...policeStationAllOptions.value] |
| | | return |
| | | } |
| | | if (policeStationSearchTimer.value) { |
| | | clearTimeout(policeStationSearchTimer.value) |
| | | } |
| | | policeStationSearchTimer.value = setTimeout(() => { |
| | | policeStationSearchTimer.value = null |
| | | void getPoliceStationList(policeStationSearchKeyword.value) |
| | | }, 300) |
| | | } |
| | | |
| | | // 获取设备列表 |
| | | async function getDeviceList () { |
| | | if (deviceOptions.value.length) return |
| | | const res = await fwDeviceListApi({ isAreaSelect: 1, areaId: formData.value.id, isTrack: 1 }) |
| | | function queueDeviceListRefresh () { |
| | | if (!visible.value) return |
| | | if (deviceListTimer) { |
| | | clearTimeout(deviceListTimer) |
| | | } |
| | | deviceListTimer = setTimeout(() => { |
| | | deviceListTimer = null |
| | | void getDeviceListByPolygons() |
| | | }, 200) |
| | | } |
| | | |
| | | async function getDeviceListByPolygons () { |
| | | if (!visible.value) return |
| | | const polygons = resolveShapePolygonsWkt() |
| | | if (!polygons.length) { |
| | | clearDeviceListState() |
| | | return |
| | | } |
| | | deviceListRequestToken += 1 |
| | | const requestToken = deviceListRequestToken |
| | | const res = await fwDeviceListByPolygonsApi({ |
| | | areaId: dialogMode.value === 'add' ? '' : formData.value.id, |
| | | isAreaSelect: 1, |
| | | isTrack: 1, |
| | | polygons, |
| | | }) |
| | | if (requestToken !== deviceListRequestToken) return |
| | | deviceOptions.value = res?.data?.data ?? [] |
| | | if (dialogMode.value === 'view') { |
| | | deviceOptions.value = deviceOptions.value.filter(item => formData.value.deviceIds.includes(item.id)) |
| | | if (dialogMode.value === 'view' && formData.value.deviceIds) { |
| | | const targetIds = formData.value.deviceIds.split(',') |
| | | deviceOptions.value = deviceOptions.value.filter(item => targetIds.includes(String(item.id))) |
| | | } |
| | | await nextTick() |
| | | const prevSuppressDeviceFly = suppressDeviceFly.value |
| | | suppressDeviceFly.value = true |
| | | syncDeviceSelection() |
| | | suppressDeviceFly.value = prevSuppressDeviceFly |
| | | if (!readonly.value) { |
| | | renderDeviceRanges(selectedDeviceRows.value) |
| | | } |
| | | } |
| | | |
| | |
| | | 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.split(',') |
| | | const arr = formData.value.deviceIds ? formData.value.deviceIds.split(',') : [] |
| | | deviceOptions.value.forEach(row => { |
| | | if (arr.includes(row.id)) { |
| | | 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 |
| | |
| | | |
| | | // 打开弹框 |
| | | 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 = '' |
| | |
| | | initMap() |
| | | clearShapeDisplay() |
| | | clearDeviceRangePrimitives() |
| | | clearDeviceIconEntities() |
| | | viewEntity && viewer?.entities?.remove(viewEntity) |
| | | viewEntity = null |
| | | clearActiveTool() |
| | | await getDeviceList() |
| | | if (dialogMode.value === 'add') { |
| | | // default no draw mode |
| | | mapRef.value?.zoomToAdminBoundary?.() |
| | | handleDraw('polygon') |
| | | } else if (dialogMode.value === 'edit') { |
| | | await loadDetail() |
| | | viewPolygon() |
| | | activeShapeId.value = null |
| | | pointList = [] |
| | | const firstShape = shapeList.value[0] |
| | | const initialType = firstShape?.drawType || 'polygon' |
| | | currentShapeType.value = initialType |
| | | if (initialType === 'buffer') { |
| | | activeAreaType.value = bufferAreaTypeValue.split(',') |
| | | } else if (firstShape?.areaType) { |
| | | activeAreaType.value = firstShape.areaType |
| | | } else { |
| | | const defaultAreaType = getDetectAreaTypeKey() |
| | | if (defaultAreaType) { |
| | | activeAreaType.value = defaultAreaType |
| | | } |
| | | } |
| | | clearActiveTool() |
| | | startDraw(initialType) |
| | | } else { |
| | | await loadDetail() |
| | | viewPolygon() |
| | | } |
| | | await nextTick() |
| | | suppressDeviceFly.value = true |
| | | syncDeviceSelection() |
| | | lastSelectedDeviceIds = selectedDeviceRows.value.map(row => row.id) |
| | | await nextTick() |
| | | suppressDeviceFly.value = false |
| | | if (!readonly.value) { |
| | | renderDeviceRanges(selectedDeviceRows.value) |
| | | } |
| | | isDialogInitializing.value = false |
| | | queueDeviceListRefresh() |
| | | void getPoliceStationList() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | getPoliceStationList() |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (deviceListTimer) { |
| | | clearTimeout(deviceListTimer) |
| | | deviceListTimer = null |
| | | } |
| | | if (policeStationSearchTimer.value) { |
| | | clearTimeout(policeStationSearchTimer.value) |
| | | policeStationSearchTimer.value = null |
| | | } |
| | | hideTypePanel() |
| | | }) |
| | | |
| | |
| | | right: 16px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | padding: 10px 8px; |
| | | background: rgba(19, 28, 36, 0.75); |
| | | gap: 14px; |
| | | padding: 32px 14px; |
| | | background: rgba(26, 25, 25, 0.6); |
| | | border-radius: 8px 8px 8px 8px; |
| | | backdrop-filter: blur(4px); |
| | | border-radius: 8px; |
| | | box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2); |
| | | } |
| | | |
| | | .tool-btn { |
| | | padding: 0; |
| | | padding: 0 !important; |
| | | width: 46px; |
| | | height: 46px; |
| | | 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; |
| | | border: none !important; |
| | | box-shadow: none; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | overflow: hidden; |
| | | background: transparent !important; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .tool-btn:hover { |
| | | background: rgba(255, 255, 255, 0.16); |
| | | border-color: rgba(255, 255, 255, 0.35); |
| | | transform: translateY(-1px); |
| | | } |
| | | |
| | | .tool-btn:active { |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | .tool-btn.danger { |
| | | border-color: rgba(255, 112, 112, 0.6); |
| | | color: #ffb3b3; |
| | | .tool-btn.active { |
| | | border: 2px solid #FFFFFF !important; |
| | | } |
| | | |
| | | .tool-icon { |
| | |
| | | } |
| | | |
| | | .tool-icon-image { |
| | | width: 36px; |
| | | height: 36px; |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .tool-icon.polygon { |